PHPackages                             gowelle/laravel-beem-africa - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. gowelle/laravel-beem-africa

ActiveLibrary[Payment Processing](/categories/payments)

gowelle/laravel-beem-africa
===========================

Laravel package for Beem API integration - SMS, Airtime, OTP, Payment Checkout, Disbursements, Collections, USSD, Contacts, Moja, and International SMS services

v2.0.0(1mo ago)131MITPHPPHP ^8.3CI passing

Since Dec 11Pushed 1mo agoCompare

[ Source](https://github.com/gowelle/laravel-beem-africa)[ Packagist](https://packagist.org/packages/gowelle/laravel-beem-africa)[ Docs](https://github.com/gowelle/beem-africa-php)[ RSS](/packages/gowelle-laravel-beem-africa/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (10)Dependencies (26)Versions (20)Used By (0)

Beem Laravel Package
====================

[](#beem-laravel-package)

[![Latest Version on Packagist](https://camo.githubusercontent.com/9a2807942b692f464b36f761860603c334eb7bad1b46c86150a67954e2a511cd/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f676f77656c6c652f6c61726176656c2d6265656d2d6166726963612e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/gowelle/laravel-beem-africa)[![Tests](https://camo.githubusercontent.com/2f599b4dc17334fd3f4352a54e3b2759d58588ca6a1ce291253724bc72fb468d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f676f77656c6c652f6c61726176656c2d6265656d2d6166726963612f74657374732e796d6c3f6272616e63683d6d6173746572266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/gowelle/laravel-beem-africa/actions/workflows/tests.yml)[![Total Downloads](https://camo.githubusercontent.com/4bd1ccba70f3e2bcac75e5cee7c81d0fc4637b0f3d71efe281e435427ce8086c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f676f77656c6c652f6c61726176656c2d6265656d2d6166726963612e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/gowelle/laravel-beem-africa)

A comprehensive Laravel package for integrating with Beem's APIs. This package provides a unified interface for **SMS**, **Airtime**, **OTP**, **Payment Checkout**, **Disbursements**, **Collections**, **USSD**, **Contacts**, **Moja** (multi-channel messaging), and **International SMS** services.

Table of Contents
-----------------

[](#table-of-contents)

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage](#usage)
    - [Payment Checkout](#using-payment-checkout)
    - [OTP (One-Time Password)](#using-otp-one-time-password)
    - [Airtime Top-Up](#using-airtime-top-up)
    - [SMS](#using-sms)
    - [Disbursements](#using-disbursements)
    - [Collections](#using-collections)
    - [USSD Hub](#using-ussd-hub)
    - [Contacts](#using-contacts)
    - [Moja (Multi-Channel Messaging)](#using-moja-multi-channel-messaging)
    - [Multicountry SMS and SMPP](#using-multicountry-sms-and-smpp)
- [UI Components](#ui-components)
    - [Livewire Components](#livewire-components)
    - [Vue/InertiaJS Components](#vueinertiajs-components)
- [Testing](#testing)
- [Security](#security)
- [Credits](#credits)
- [License](#license)

Features
--------

[](#features)

### Payment Checkout

[](#payment-checkout)

- 🔄 **Redirect Checkout** - Redirect users to Beem's hosted checkout page
- 🖼️ **Iframe Checkout** - Embed checkout within your application
- 🔔 **Webhook Handling** - Automatic webhook processing with Laravel events
- 🛡️ **Secure Token Validation** - Optional webhook signature verification
- 💾 **Transaction Storage** - Optional database storage for payment records

### OTP (One-Time Password)

[](#otp-one-time-password)

- 📱 **Send OTP** - Send verification codes via SMS
- ✅ **Verify OTP** - Validate user-entered codes
- 🔐 **Phone Verification** - Secure phone number verification flow
- 🎯 **Error Codes** - 18 detailed error codes for precise handling

### Airtime Top-Up

[](#airtime-top-up)

- 💰 **Transfer Airtime** - Send mobile credit across 40+ African networks
- 📊 **Check Balance** - Monitor your airtime credit balance
- 🔍 **Transaction Status** - Track airtime transfer status
- 🔔 **Callback Support** - Receive real-time transfer notifications
- 🎯 **Response Codes** - 16 detailed error codes for precise handling

### SMS

[](#sms)

- 📨 **Send SMS** - Send single or bulk SMS to 22+ regions
- 📋 **Sender Names** - Manage custom sender IDs
- 📄 **Templates** - Use pre-configured message templates
- 📊 **Balance Check** - Monitor SMS credit balance
- 📬 **Delivery Reports** - Track message delivery status
- 📲 **Two Way SMS** - Receive inbound SMS messages
- ⏰ **Scheduled Messages** - Schedule SMS for future delivery
- 🎯 **Error Codes** - 9 detailed error codes for precise handling

### Disbursements

[](#disbursements)

- 💸 **Mobile Money Payouts** - Transfer funds to mobile wallets
- 🏦 **Multiple Wallets** - Support for various mobile money providers
- ⏰ **Scheduled Transfers** - Schedule disbursements for later
- 🎯 **Error Codes** - 14 detailed error codes for precise handling

### Collections

[](#collections)

- 💳 **Receive Payments** - Accept mobile money payments from subscribers
- 🔔 **Webhook Callbacks** - Real-time payment notifications
- 📊 **Balance Check** - Monitor collection balance
- 🏪 **Multiple Paybills** - Support for various paybill/merchant numbers

### USSD Hub

[](#ussd-hub)

- 📱 **Interactive Menus** - Design and run USSD menus via API
- 🔄 **Session Management** - Handle initiate/continue/terminate flows
- 📊 **Balance Check** - Monitor USSD credit balance
- 🌐 **Multi-Network** - Single API for multiple mobile networks

### Contacts

[](#contacts)

- 📇 **AddressBook Management** - Create and manage multiple contact address books
- 👥 **Contact Management** - Full CRUD operations for contacts
- 🔍 **Search &amp; Filter** - Search contacts by name or phone number
- 📄 **Pagination** - Built-in pagination support for large contact lists
- ✅ **Validation** - Input validation for phone numbers, email, and dates
- 📋 **Comprehensive Fields** - Support for name, phone, email, address, birth date, and more

### Moja (Multi-Channel Messaging)

[](#moja-multi-channel-messaging)

- 💬 **Multi-Channel Support** - WhatsApp, Facebook, Instagram, Google Business Messaging
- 📱 **Six Message Types** - Text, Image, Document, Video, Audio, Location
- 🔄 **Active Sessions** - Monitor and manage active chat sessions
- 📋 **WhatsApp Templates** - Fetch, manage, and send template messages
- 🔔 **Webhook Handling** - Real-time incoming messages and delivery reports
- 📊 **Delivery Tracking** - Track message delivery status (sent, delivered, read, failed)
- 🎯 **Error Handling** - Comprehensive error codes and error handling with MojaException

### International SMS

[](#international-sms)

- 🌍 **Global Reach** - Send SMS to international numbers
- 🔢 **Binary Support** - Send Unicode/Hex messages (flash messages, etc.)
- 📋 **Multiple Recipients** - Send to multiple destinations in one request
- 📊 **Balance Check** - Monitor International SMS credit balance
- 🔔 **DLR Webhooks** - Real-time delivery reports

### UI Components

[](#ui-components)

- 🎨 **Livewire v3 Components** - Ready-to-use checkout, OTP, and SMS components
- ⚡ **Vue 3 + TypeScript** - Type-safe InertiaJS components with composables
- 🧪 **Fully Tested** - 104 component tests (75 Vue + 29 Livewire)
- 🎯 **Beem Branded** - Styled with official Beem colors

### Developer Experience

[](#developer-experience)

- 📦 **DTOs** - Type-safe data transfer objects for requests and responses
- 🧪 **Fully Tested** - Comprehensive test coverage with Pest
- 🚀 **CI/CD Ready** - GitHub Actions workflows included

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11.0+, 12.0+, or 13.0+

Installation
------------

[](#installation)

Install the package via Composer:

```
composer require gowelle/laravel-beem-africa
```

### Quick Install (Recommended)

[](#quick-install-recommended)

The package includes an interactive install command that sets up everything for you:

```
php artisan beem-africa:install
```

This command will:

- ✅ Publish the configuration file
- ✅ Publish database migrations
- ✅ Ask if you want to run migrations
- ✅ Optionally star the repository on GitHub

### Manual Installation

[](#manual-installation)

If you prefer to publish assets manually:

```
# Publish the configuration file
php artisan vendor:publish --tag="beem-africa-config"

# Publish database migrations (optional)
php artisan vendor:publish --tag="beem-africa-migrations"
php artisan migrate
```

**Available publishable tags:**

- `beem-africa-config` - Publishes the configuration file
- `beem-africa-migrations` - Publishes the database migration (optional, for transaction storage)
- `beem-africa-views` - Publishes the Blade views (optional, for customization)
- `beem-africa-components` - Publishes the Blade components (optional, for customization)
- `beem-africa-translations` - Publishes the translation files (optional, for localization)
- `beem-africa-vue` - Publishes the Vue/InertiaJS components to `resources/js/vendor/beem-africa`

### Localization

[](#localization)

All Blade and Livewire components fully support localization. The package includes built-in translations in three languages:

- 🇬🇧 **English** (en) - Default
- 🇹🇿 **Swahili** (sw) - Kiswahili
- 🇫🇷 **French** (fr) - Français

To use a different language, set your application locale in `config/app.php`:

```
'locale' => 'sw', // Use Swahili
```

Or publish and customize translations:

```
php artisan vendor:publish --tag="beem-africa-translations"
```

#### Vue Components

[](#vue-components)

Vue components (Inertia/Standalone) support localization via the `labels` prop. You can pass an object with the specific keys you want to customize.

Example with manual strings:

```

```

Example using Laravel translations (Blade):

```

```

Configuration
-------------

[](#configuration)

Add your Beem credentials to your `.env` file:

```
BEEM_API_KEY=your_api_key
BEEM_SECRET_KEY=your_secret_key
BEEM_WEBHOOK_SECRET=optional_webhook_secret

BEEM_INTERNATIONAL_SMS_USERNAME=your_int_sms_username
BEEM_INTERNATIONAL_SMS_PASSWORD=your_int_sms_password
```

### Configuration Options

[](#configuration-options)

```
// config/beem-africa.php

return [
    'api_key' => env('BEEM_API_KEY'),
    'secret_key' => env('BEEM_SECRET_KEY'),
    'base_url' => env('BEEM_BASE_URL', 'https://checkout.beem.africa/v1'),

    'webhook' => [
        'path' => env('BEEM_WEBHOOK_PATH', 'beem/webhook'),
        'secret' => env('BEEM_WEBHOOK_SECRET'),
        'middleware' => [],
    ],

    'store_transactions' => env('BEEM_STORE_TRANSACTIONS', false),

    'otp' => [
        'base_url' => env('BEEM_OTP_BASE_URL', 'https://apiotp.beem.africa/v1'),
        'app_id' => env('BEEM_OTP_APP_ID'),
    ],

    'international_sms' => [
        'username' => env('BEEM_INTERNATIONAL_SMS_USERNAME'),
        'password' => env('BEEM_INTERNATIONAL_SMS_PASSWORD'),
        'base_url' => 'https://api.blsmsgw.com:8443/bin',
        'portal_url' => 'https://www.blsmsgw.com/portal/api',
        'dlr_url' => env('BEEM_INTERNATIONAL_SMS_DLR_URL'),
    ],
];
```

Usage
-----

[](#usage)

### Using Payment Checkout

[](#using-payment-checkout)

#### Redirect Method

[](#redirect-method)

The simplest way to accept payments is to redirect users to Beem's hosted checkout page:

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\DTOs\CheckoutRequest;

// In your controller
public function checkout()
{
    $request = new CheckoutRequest(
        amount: 1000.00,
        transactionId: 'TXN-' . uniqid(),
        referenceNumber: 'ORDER-001',
        mobile: '255712345678', // Optional
    );

    // Option 1: Redirect directly
    return Beem::redirect($request);

    // Option 2: Get the URL and redirect manually
    $checkoutUrl = Beem::getCheckoutUrl($request);
    return redirect()->away($checkoutUrl);
}
```

#### Iframe Method

[](#iframe-method)

For a seamless checkout experience, embed the checkout button in your page:

##### 1. Whitelist Your Domain

[](#1-whitelist-your-domain)

Before using the iframe method, whitelist your domain:

```
use Gowelle\BeemAfrica\Facades\Beem;

// Run this once (e.g., in a setup command or controller)
Beem::whitelistDomain('https://yourapp.com');
```

##### 2. Add the Checkout Button

[](#2-add-the-checkout-button)

Use the included Blade component:

```

```

> **Tip:** To customize the component, publish it with:
>
> ```
> php artisan vendor:publish --tag="beem-africa-components"
> ```

Or manually add the button:

```

```

#### Error Handling

[](#error-handling)

The package provides structured error handling for Beem API errors. All payment-related operations throw `PaymentException` when errors occur.

##### Available Error Codes

[](#available-error-codes)

Based on [Beem API documentation](https://docs.beem.africa/payments-checkout/index.html#api-ERROR), the following error codes are supported:

CodeDescriptionHelper Method100Invalid Mobile Number`isInvalidMobileNumber()`101Invalid Amount`isInvalidAmount()`102Invalid Transaction ID`isInvalidTransactionId()`120Invalid Authentication Parameters`isInvalidAuthentication()`##### Handling Payment Errors

[](#handling-payment-errors)

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\DTOs\CheckoutRequest;
use Gowelle\BeemAfrica\Exceptions\PaymentException;
use Gowelle\BeemAfrica\Enums\BeemErrorCode;

try {
    $request = new CheckoutRequest(
        amount: 1000.00,
        transactionId: 'TXN-123',
        referenceNumber: 'ORDER-001',
        mobile: '255712345678',
    );

    return Beem::redirect($request);
} catch (PaymentException $e) {
    // Get the Beem-specific error code
    $beemErrorCode = $e->getBeemErrorCode();

    // Check for specific error types
    if ($e->isInvalidMobileNumber()) {
        return back()->withErrors(['mobile' => 'Invalid mobile number format']);
    }

    if ($e->isInvalidAmount()) {
        return back()->withErrors(['amount' => 'Invalid amount provided']);
    }

    if ($e->isInvalidTransactionId()) {
        return back()->withErrors(['transaction_id' => 'Transaction ID already exists or is invalid']);
    }

    if ($e->isInvalidAuthentication()) {
        Log::error('Beem authentication failed - check API credentials');
        return back()->withErrors(['error' => 'Payment service unavailable']);
    }

    // Generic error handling
    Log::error('Payment error', [
        'message' => $e->getMessage(),
        'beem_code' => $beemErrorCode?->value,
        'http_status' => $e->getHttpStatusCode(),
    ]);

    return back()->withErrors(['error' => 'Payment failed. Please try again.']);
}
```

##### Checking Error Codes Programmatically

[](#checking-error-codes-programmatically)

```
use Gowelle\BeemAfrica\Exceptions\PaymentException;
use Gowelle\BeemAfrica\Enums\BeemErrorCode;

try {
    // Your payment operation
} catch (PaymentException $e) {
    // Check if a specific error code is present
    if ($e->hasErrorCode(BeemErrorCode::INVALID_MOBILE_NUMBER)) {
        // Handle invalid mobile number
    }

    // Get the error code enum
    $errorCode = $e->getBeemErrorCode();

    if ($errorCode === BeemErrorCode::INVALID_AMOUNT) {
        // Handle invalid amount
    }

    // Access error code details
    if ($errorCode) {
        echo $errorCode->description(); // "Invalid Mobile Number"
        echo $errorCode->message();     // Detailed error message
        echo $errorCode->value;         // 100 (the numeric code)
    }
}
```

#### Handling Webhooks

[](#handling-webhooks)

The package automatically registers a webhook route at `/webhooks/beem`. When Beem sends a payment notification, the package dispatches Laravel events.

##### Webhook Security

[](#webhook-security)

The package supports webhook authentication using Beem's secure token. Configure your webhook secret in `.env`:

```
BEEM_WEBHOOK_SECRET=your_webhook_secret_from_beem
```

**Two authentication methods are available:**

1. **Built-in validation** - The webhook controller automatically validates the `beem-secure-token` header
2. **Middleware approach** - Apply the provided middleware for more control:

```
// config/beem-africa.php

'webhook' => [
    'path' => env('BEEM_WEBHOOK_PATH', 'beem/webhook'),
    'secret' => env('BEEM_WEBHOOK_SECRET'),
    'middleware' => [
        \Gowelle\BeemAfrica\Http\Middleware\VerifyBeemSignature::class,
    ],
],
```

> **Note:** If you use the middleware approach, the controller will still perform validation. You can use either or both methods depending on your security requirements. If no `BEEM_WEBHOOK_SECRET` is configured, both will allow requests through.

##### 1. Create Event Listeners

[](#1-create-event-listeners)

```
// app/Listeners/HandleSuccessfulPayment.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\PaymentSucceeded;

class HandleSuccessfulPayment
{
    public function handle(PaymentSucceeded $event): void
    {
        $transactionId = $event->getTransactionId();
        $amount = $event->getAmount();
        $reference = $event->getReferenceNumber();
        $mobile = $event->getMsisdn();

        // Update your order/payment status
        Order::where('reference', $reference)->update([
            'status' => 'paid',
            'paid_at' => now(),
        ]);
    }
}
```

```
// app/Listeners/HandleFailedPayment.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\PaymentFailed;

class HandleFailedPayment
{
    public function handle(PaymentFailed $event): void
    {
        $transactionId = $event->getTransactionId();
        $reference = $event->getReferenceNumber();

        // Handle the failed payment
        Order::where('reference', $reference)->update([
            'status' => 'failed',
        ]);
    }
}
```

##### 2. Register the Listeners

[](#2-register-the-listeners)

```
// app/Providers/EventServiceProvider.php

use Gowelle\BeemAfrica\Events\PaymentSucceeded;
use Gowelle\BeemAfrica\Events\PaymentFailed;
use App\Listeners\HandleSuccessfulPayment;
use App\Listeners\HandleFailedPayment;

protected $listen = [
    PaymentSucceeded::class => [
        HandleSuccessfulPayment::class,
    ],
    PaymentFailed::class => [
        HandleFailedPayment::class,
    ],
];
```

##### Using the Callback Payload

[](#using-the-callback-payload)

The event payload provides access to all webhook data:

```
public function handle(PaymentSucceeded $event): void
{
    $payload = $event->payload;

    $payload->amount;           // '1000.00'
    $payload->referenceNumber;  // 'ORDER-001'
    $payload->status;           // 'success'
    $payload->timestamp;        // '2024-01-15T10:30:00Z'
    $payload->transactionId;    // 'TXN-123'
    $payload->msisdn;           // '255712345678'

    // Helper methods
    $payload->isSuccessful();          // true
    $payload->isFailed();              // false
    $payload->getAmountAsFloat();      // 1000.00
    $payload->getTimestampAsDateTime(); // DateTimeImmutable
}
```

#### Transaction Storage (Optional)

[](#transaction-storage-optional)

The package can automatically store transactions in your database. This is useful for tracking payment history and reconciliation.

##### 1. Publish and Run Migrations

[](#1-publish-and-run-migrations)

```
php artisan vendor:publish --tag="beem-africa-migrations"
php artisan migrate
```

> **Note for UUID/ULID Users:** If your `users` table uses `uuid` or `ulid` as the primary key instead of `bigint`, you need to modify the published migration before running it:
>
> ```
> // For UUID:
> $table->uuid('user_id')->nullable()->constrained()->nullOnDelete();
>
> // For ULID:
> $table->ulid('user_id')->nullable()->constrained()->nullOnDelete();
>
> // Or remove the constraint entirely and handle it manually:
> $table->string('user_id', 36)->nullable();
> ```
>
>
>
> You can also configure the user model in `config/beem.php`:
>
> ```
> 'user_model' => 'App\\Models\\User',
> ```

##### 2. Enable Transaction Storage

[](#2-enable-transaction-storage)

Add to your `.env`:

```
BEEM_STORE_TRANSACTIONS=true
```

##### 3. Access Stored Transactions

[](#3-access-stored-transactions)

```
use Gowelle\BeemAfrica\Models\BeemTransaction;

// Find by transaction ID
$transaction = BeemTransaction::where('transaction_id', 'TXN-123')->first();

// Find by reference
$transactions = BeemTransaction::byReference('ORDER-001')->get();

// Query by status
$successful = BeemTransaction::successful()->get();
$failed = BeemTransaction::failed()->get();
$pending = BeemTransaction::pending()->get();

// Create a pending transaction before redirect
$transaction = BeemTransaction::createPending(
    transactionId: 'TXN-' . uniqid(),
    referenceNumber: 'ORDER-001',
    amount: 1000.00,
    msisdn: '255712345678',
    userId: auth()->id(),
);
```

##### 4. Access Transaction in Event Listeners

[](#4-access-transaction-in-event-listeners)

When transaction storage is enabled, the transaction model is available in events:

```
public function handle(PaymentSucceeded $event): void
{
    $transaction = $event->getTransaction(); // BeemTransaction model or null

    if ($transaction) {
        // Update with additional data
        $transaction->update(['user_id' => $userId]);
    }
}
```

### Using OTP (One-Time Password)

[](#using-otp-one-time-password)

The package supports Beem's OTP service for phone number verification.

#### 1. Configure OTP

[](#1-configure-otp)

Add your OTP App ID to `.env`:

```
BEEM_OTP_APP_ID=your_app_id_from_beem_dashboard
```

#### 2. Request OTP

[](#2-request-otp)

Send an OTP to a user's phone number:

```
use Gowelle\BeemAfrica\Facades\Beem;

// Request OTP
$response = Beem::otp()->request('255712345678');

if ($response->isSuccessful()) {
    $pinId = $response->getPinId();

    // Store the PIN ID in session or database for verification
    session(['otp_pin_id' => $pinId]);
}
```

#### 3. Verify OTP

[](#3-verify-otp)

Verify the OTP entered by the user:

```
use Gowelle\BeemAfrica\Facades\Beem;

$pinId = session('otp_pin_id');
$userPin = $request->input('otp_code'); // e.g., '1234'

$result = Beem::otp()->verify($pinId, $userPin);

if ($result->isValid()) {
    // OTP is valid - proceed with verification
    session()->forget('otp_pin_id');

    // Mark phone number as verified
    auth()->user()->update(['phone_verified_at' => now()]);
} else {
    // OTP is invalid
    return back()->withErrors(['otp_code' => 'Invalid OTP code']);
}
```

#### 4. OTP Error Handling

[](#4-otp-error-handling)

The package provides detailed error handling with 18 response codes for precise OTP error management.

##### Available Error Codes

[](#available-error-codes-1)

Based on [Beem OTP API documentation](https://docs.beem.africa/bl-otp/index.html#api--ERROR_CODES), the following error codes are supported:

CodeDescriptionHelper Method100SMS sent successfully`isSuccess()`101Failed to send SMS`isFailure()`102Invalid phone number`isInvalidPhoneNumber()`103Phone number missing`isFailure()`104Application ID missing`isApplicationIdMissing()`106Application not found`isApplicationNotFound()`107Application is inactive`isFailure()`108No channel found`isNoChannelFound()`109Placeholder not found`isFailure()`110Username or Password missing`isFailure()`111PIN missing`isFailure()`112PIN ID missing`isFailure()`113PIN ID not found`isPinIdNotFound()`114Incorrect PIN`isIncorrectPin()`115PIN timeout`isPinTimeout()`116Attempts exceeded`isAttemptsExceeded()`117Valid PIN`isSuccess()`118Duplicate PIN`isFailure()`> See [OtpResponseCode](src/Enums/OtpResponseCode.php) for all 18 response codes.

##### Handling OTP Request Errors

[](#handling-otp-request-errors)

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\Exceptions\OtpRequestException;
use Gowelle\BeemAfrica\Enums\OtpResponseCode;

try {
    $response = Beem::otp()->request('255712345678');

    // Access response code from successful response
    $code = $response->getCode();
    if ($code === OtpResponseCode::SMS_SENT_SUCCESSFULLY) {
        echo "OTP sent successfully!";
    }
} catch (OtpRequestException $e) {
    // Get the OTP response code
    $otpResponseCode = $e->getOtpResponseCode();

    // Check for specific error types
    if ($e->isInvalidPhoneNumber()) {
        return back()->withErrors(['phone' => 'Invalid phone number format']);
    }

    if ($e->isApplicationIdMissing()) {
        Log::error('OTP App ID not configured');
        return back()->withErrors(['error' => 'OTP service configuration error']);
    }

    if ($e->isApplicationNotFound()) {
        Log::error('OTP Application not found - check App ID');
        return back()->withErrors(['error' => 'OTP service unavailable']);
    }

    if ($e->isNoChannelFound()) {
        Log::error('OTP channel not configured in Beem dashboard');
        return back()->withErrors(['error' => 'OTP service configuration error']);
    }

    // Generic error handling
    Log::error('OTP request failed', [
        'message' => $e->getMessage(),
        'code' => $otpResponseCode?->value,
        'http_status' => $e->getHttpStatusCode(),
    ]);

    return back()->withErrors(['error' => 'Failed to send OTP. Please try again.']);
}
```

##### Handling OTP Verification Errors

[](#handling-otp-verification-errors)

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\Exceptions\OtpVerificationException;
use Gowelle\BeemAfrica\Enums\OtpResponseCode;

try {
    $result = Beem::otp()->verify($pinId, $userPin);

    // Access response code from verification result
    $code = $result->getCode();
    if ($code === OtpResponseCode::VALID_PIN) {
        // OTP is valid
        session()->forget('otp_pin_id');
        auth()->user()->update(['phone_verified_at' => now()]);
    }
} catch (OtpVerificationException $e) {
    // Get the OTP response code
    $otpResponseCode = $e->getOtpResponseCode();

    // Check for specific error types
    if ($e->isIncorrectPin()) {
        return back()->withErrors(['otp_code' => 'Incorrect OTP code. Please try again.']);
    }

    if ($e->isPinTimeout()) {
        return back()->withErrors(['otp_code' => 'OTP code has expired. Please request a new one.']);
    }

    if ($e->isAttemptsExceeded()) {
        return back()->withErrors(['otp_code' => 'Too many failed attempts. Please request a new OTP.']);
    }

    if ($e->isPinIdNotFound()) {
        return back()->withErrors(['otp_code' => 'Invalid verification session. Please request a new OTP.']);
    }

    // Generic error handling
    Log::error('OTP verification failed', [
        'message' => $e->getMessage(),
        'code' => $otpResponseCode?->value,
        'http_status' => $e->getHttpStatusCode(),
    ]);

    return back()->withErrors(['otp_code' => 'Verification failed. Please try again.']);
}
```

##### Checking Error Codes Programmatically

[](#checking-error-codes-programmatically-1)

```
use Gowelle\BeemAfrica\Exceptions\OtpRequestException;
use Gowelle\BeemAfrica\Exceptions\OtpVerificationException;
use Gowelle\BeemAfrica\Enums\OtpResponseCode;

try {
    // Your OTP operation
} catch (OtpRequestException $e) {
    // Check if a specific error code is present
    if ($e->hasResponseCode(OtpResponseCode::INVALID_PHONE_NUMBER)) {
        // Handle invalid phone number
    }

    // Get the error code enum
    $errorCode = $e->getOtpResponseCode();

    if ($errorCode === OtpResponseCode::FAILED_TO_SEND_SMS) {
        // Handle SMS send failure
    }

    // Access error code details
    if ($errorCode) {
        echo $errorCode->description(); // "Invalid phone number"
        echo $errorCode->message();     // Detailed error message
        echo $errorCode->value;         // 102 (the numeric code)
    }
}

try {
    // Your verification operation
} catch (OtpVerificationException $e) {
    // Check for specific verification errors
    if ($e->hasResponseCode(OtpResponseCode::INCORRECT_PIN)) {
        // Handle incorrect PIN
    }

    // Get the error code enum
    $errorCode = $e->getOtpResponseCode();

    if ($errorCode === OtpResponseCode::PIN_TIMEOUT) {
        // Handle PIN timeout
    }
}
```

##### Accessing Response Codes from DTOs

[](#accessing-response-codes-from-dtos)

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\Enums\OtpResponseCode;

// Request OTP
$response = Beem::otp()->request('255712345678');

// Get response code from DTO
$code = $response->getCode();
$codeValue = $response->getCodeValue(); // Integer value (100, 101, etc.)

if ($code === OtpResponseCode::SMS_SENT_SUCCESSFULLY) {
    $pinId = $response->getPinId();
}

// Verify OTP
$result = Beem::otp()->verify($pinId, $userPin);

// Get response code from verification result
$code = $result->getCode();
if ($code === OtpResponseCode::VALID_PIN) {
    // PIN is valid
}
```

### Using Airtime Top-Up

[](#using-airtime-top-up)

The package supports Beem's Airtime API for mobile credit top-ups across Africa.

#### 1. Transfer Airtime

[](#1-transfer-airtime)

Send airtime to a mobile number:

```
use Gowelle\BeemAfrica\Facades\Beem;

$response = Beem::airtime()->transfer(
    destAddr: '255712345678',      // International format, no +
    amount: 1000.00,                // Amount in local currency
    referenceId: 'ORDER-'.uniqid(), // Your unique reference
);

if ($response->isSuccessful()) {
    $transactionId = $response->getTransactionId();

    // Store transaction ID for status checking
    session(['airtime_txn_id' => $transactionId]);
}
```

#### 2. Check Transaction Status

[](#2-check-transaction-status)

Manually check the status of an airtime transfer:

```
$status = Beem::airtime()->checkStatus($transactionId);

if ($status->isSuccessful()) {
    // Transfer completed successfully
    $amount = $status->getAmountAsFloat();
    $destAddr = $status->getDestAddr();
} else {
    // Transfer failed or pending
    $code = $status->getCode();
    $message = $status->message;
}
```

#### 3. Check Balance

[](#3-check-balance)

Check your airtime credit balance:

```
$balance = Beem::airtime()->checkBalance();

echo "Balance: {$balance->getBalance()} {$balance->getCurrency()}";
```

#### 4. Airtime Error Handling

[](#4-airtime-error-handling)

The package provides detailed error handling with 16 response codes:

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\Exceptions\AirtimeException;
use Gowelle\BeemAfrica\Enums\AirtimeResponseCode;

try {
    $response = Beem::airtime()->transfer(
        destAddr: '255712345678',
        amount: 1000.00,
        referenceId: 'REF-001',
    );
} catch (AirtimeException $e) {
    // Check specific error types
    if ($e->isInsufficientBalance()) {
        return back()->withErrors(['amount' => 'Insufficient airtime balance']);
    }

    if ($e->isInvalidPhoneNumber()) {
        return back()->withErrors(['phone' => 'Invalid phone number format']);
    }

    if ($e->isInvalidAuthentication()) {
        Log::error('Beem authentication failed - check API credentials');
        return back()->withErrors(['error' => 'Service unavailable']);
    }

    // Get the response code enum
    $responseCode = $e->getResponseCode();
    if ($responseCode) {
        Log::error('Airtime transfer failed', [
            'code' => $responseCode->value,
            'description' => $responseCode->description(),
            'is_failure' => $responseCode->isFailure(),
        ]);
    }
}
```

**Available Response Codes:**

CodeDescriptionHelper Method100Disbursement successful`isSuccess()`101Disbursement failed`isFailure()`102Invalid phone number`isInvalidPhoneNumber()`103Insufficient balance`isInsufficientBalance()`104Network timeout`isNetworkTimeout()`105Invalid parameters`isInvalidParameters()`106Amount too large`isAmountTooLarge()`114Disbursement Pending`isPending()`120Invalid Authentication`isInvalidAuthentication()`> See [AirtimeResponseCode](src/Enums/AirtimeResponseCode.php) for all 16 response codes.

#### 5. Airtime Callbacks

[](#5-airtime-callbacks)

Beem sends async callbacks with the final transfer status. Configure your callback URL in the **Beem Airtime dashboard**.

**Create an event listener:**

```
// app/Listeners/HandleAirtimeCallback.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\AirtimeTransferCompleted;

class HandleAirtimeCallback
{
    public function handle(AirtimeTransferCompleted $event): void
    {
        $transactionId = $event->getTransactionId();
        $amount = $event->getAmount();
        $destAddr = $event->getDestAddr();
        $referenceId = $event->getReferenceId();

        if ($event->isSuccessful()) {
            // Update your records
            AirtimeTransaction::where('reference_id', $referenceId)->update([
                'status' => 'completed',
                'transaction_id' => $transactionId,
                'completed_at' => now(),
            ]);
        } else {
            // Handle failure
            $code = $event->getCode();
            Log::warning("Airtime transfer failed: {$code}", [
                'reference_id' => $referenceId,
            ]);
        }
    }
}
```

**Register the listener:**

```
// app/Providers/EventServiceProvider.php

use Gowelle\BeemAfrica\Events\AirtimeTransferCompleted;
use App\Listeners\HandleAirtimeCallback;

protected $listen = [
    AirtimeTransferCompleted::class => [
        HandleAirtimeCallback::class,
    ],
];
```

### Using SMS

[](#using-sms)

The package supports Beem's SMS API for sending text messages across 22+ regions.

#### 1. Configure SMS

[](#1-configure-sms)

Add your SMS sender ID to `.env` (optional):

```
BEEM_SMS_SENDER_ID=MYAPP
```

#### 2. Send SMS

[](#2-send-sms)

Send SMS to one or more recipients:

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\DTOs\SmsRequest;
use Gowelle\BeemAfrica\DTOs\SmsRecipient;

// Single recipient
$request = new SmsRequest(
    sourceAddr: 'MYAPP',                // Sender ID (max 11 chars)
    message: 'Hello from Beem!',
    recipients: [
        new SmsRecipient('REC-001', '255712345678'),
    ]
);

$response = Beem::sms()->send($request);

if ($response->isSuccessful()) {
    $requestId = $response->getRequestId();
    $validCount = $response->getValidCount();

    // Store request ID for delivery tracking
    session(['sms_request_id' => $requestId]);
}
```

**Bulk SMS:**

```
$request = new SmsRequest(
    sourceAddr: 'MYAPP',
    message: 'Bulk message to multiple recipients',
    recipients: [
        new SmsRecipient('REC-001', '255712345678'),
        new SmsRecipient('REC-002', '255787654321'),
        new SmsRecipient('REC-003', '254712345678'),
    ]
);

$response = Beem::sms()->send($request);

echo "Valid: {$response->getValidCount()}, Invalid: {$response->getInvalidCount()}";
```

**Scheduled SMS:**

```
$request = new SmsRequest(
    sourceAddr: 'MYAPP',
    message: 'Scheduled message',
    recipients: [new SmsRecipient('REC-001', '255712345678')],
    scheduleTime: '2025-12-25 09:00'  // GMT+0 timezone
);

$response = Beem::sms()->send($request);
```

**Unicode SMS:**

```
$request = new SmsRequest(
    sourceAddr: 'MYAPP',
    message: 'مرحبا بك',  // Arabic text
    recipients: [new SmsRecipient('REC-001', '255712345678')],
    encoding: 8  // UCS2/Unicode encoding
);

$response = Beem::sms()->send($request);
```

#### 3. Check SMS Balance

[](#3-check-sms-balance)

Check your SMS credit balance:

```
$balance = Beem::sms()->checkBalance();

echo "SMS Credits: {$balance->getCreditBalance()}";
```

#### 4. Get Delivery Reports

[](#4-get-delivery-reports)

Poll for delivery status of sent messages:

```
$report = Beem::sms()->getDeliveryReport(
    destAddr: '255712345678',
    requestId: 12345
);

if ($report->isDelivered()) {
    echo "Message delivered successfully";
} elseif ($report->isFailed()) {
    echo "Message delivery failed";
} elseif ($report->isPending()) {
    echo "Message delivery pending";
}
```

#### 5. Get Sender Names

[](#5-get-sender-names)

List your registered sender IDs:

```
// Get all sender names
$senderNames = Beem::sms()->getSenderNames();

foreach ($senderNames as $sender) {
    echo "{$sender->getName()}: {$sender->getStatus()}\n";

    if ($sender->isActive()) {
        // Use this sender ID
    }
}

// Filter by status
$activeSenders = Beem::sms()->getSenderNames(status: 'active');

// Search by name
$results = Beem::sms()->getSenderNames(query: 'MYAPP');
```

#### 6. Get SMS Templates

[](#6-get-sms-templates)

List your pre-configured templates:

```
$templates = Beem::sms()->getSmsTemplates();

foreach ($templates as $template) {
    echo "Template: {$template->getName()}\n";
    echo "Content: {$template->getContent()}\n";
}
```

#### 7. SMS Error Handling

[](#7-sms-error-handling)

The package provides detailed error handling with 9 response codes:

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\Exceptions\SmsException;
use Gowelle\BeemAfrica\Enums\SmsResponseCode;

try {
    $request = new SmsRequest(
        sourceAddr: 'MYAPP',
        message: 'Test message',
        recipients: [new SmsRecipient('REC-001', '255712345678')]
    );

    $response = Beem::sms()->send($request);
} catch (SmsException $e) {
    // Check specific error types
    if ($e->isInsufficientBalance()) {
        return back()->withErrors(['error' => 'Insufficient SMS credits']);
    }

    if ($e->isInvalidPhoneNumber()) {
        return back()->withErrors(['phone' => 'Invalid phone number format']);
    }

    if ($e->isInvalidAuthentication()) {
        Log::error('Beem authentication failed - check API credentials');
        return back()->withErrors(['error' => 'Service unavailable']);
    }

    // Get the response code enum
    $responseCode = $e->getResponseCode();
    if ($responseCode) {
        Log::error('SMS send failed', [
            'code' => $responseCode->value,
            'description' => $responseCode->description(),
        ]);
    }
}
```

**Available Response Codes:**

CodeDescriptionHelper Method100Message Submitted Successfully`isSuccess()`101Invalid phone number`isInvalidPhoneNumber()`102Insufficient balance`isInsufficientBalance()`103Network timeout`isNetworkTimeout()`104Please provide all required parameters`isMissingParameters()`105Account not found`isAccountNotFound()`106No route mapping to your account`isNoRoute()`107No authorization headers`isInvalidAuthentication()`108Invalid token`isInvalidAuthentication()`> See [SmsResponseCode](src/Enums/SmsResponseCode.php) for all 9 response codes.

#### 8. SMS Webhooks

[](#8-sms-webhooks)

The package automatically registers webhook routes for SMS delivery reports and inbound messages.

**Delivery Report Webhook:**

Configure your delivery report webhook URL in the Beem SMS dashboard to point to:

```
https://yourapp.com/webhooks/beem/sms/delivery

```

**Create an event listener:**

```
// app/Listeners/HandleSmsDelivery.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\SmsDeliveryReceived;

class HandleSmsDelivery
{
    public function handle(SmsDeliveryReceived $event): void
    {
        $report = $event->getReport();

        if ($event->isDelivered()) {
            // Update your records
            SmsLog::where('request_id', $report->getRequestId())
                ->where('dest_addr', $report->getDestAddr())
                ->update(['status' => 'delivered']);
        } elseif ($event->isFailed()) {
            // Handle failure
            Log::warning('SMS delivery failed', [
                'dest_addr' => $report->getDestAddr(),
                'request_id' => $report->getRequestId(),
            ]);
        }
    }
}
```

**Inbound SMS Webhook (Two Way SMS):**

Configure your inbound SMS webhook URL in the Beem SMS dashboard to point to:

```
https://yourapp.com/webhooks/beem/sms/inbound

```

**Create an event listener:**

```
// app/Listeners/HandleInboundSms.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\InboundSmsReceived;

class HandleInboundSms
{
    public function handle(InboundSmsReceived $event): void
    {
        $from = $event->getFrom();
        $message = $event->getMessage();
        $timestamp = $event->getTimestamp();

        // Process inbound message
        InboundMessage::create([
            'from' => $from,
            'message' => $message,
            'received_at' => $timestamp,
        ]);

        // Auto-reply logic
        if (str_contains(strtolower($message), 'help')) {
            // Send help message
        }
    }
}
```

**Register the listeners:**

```
// app/Providers/EventServiceProvider.php

use Gowelle\BeemAfrica\Events\SmsDeliveryReceived;
use Gowelle\BeemAfrica\Events\InboundSmsReceived;
use App\Listeners\HandleSmsDelivery;
use App\Listeners\HandleInboundSms;

protected $listen = [
    SmsDeliveryReceived::class => [
        HandleSmsDelivery::class,
    ],
    InboundSmsReceived::class => [
        HandleInboundSms::class,
    ],
];
```

### Using Disbursements

[](#using-disbursements)

The package supports Beem's Disbursement API for mobile money payouts.

#### 1. Transfer Funds

[](#1-transfer-funds)

Disburse funds to a mobile money wallet:

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\DTOs\DisbursementRequest;

$request = new DisbursementRequest(
    amount: '10000',                    // Amount to transfer
    walletNumber: '255712345678',       // Destination mobile (international format)
    walletCode: 'ABC12345',             // Mobile money wallet code
    accountNo: 'your-bpay-account',     // Your Bpay wallet account number
    clientReferenceId: 'REF-'.uniqid(), // Your unique reference
);

$response = Beem::disbursement()->transfer($request);

if ($response->isSuccessful()) {
    $transactionId = $response->getTransactionId();
    echo "Transfer successful! ID: {$transactionId}";
}
```

#### 2. Scheduled Transfers

[](#2-scheduled-transfers)

Schedule a disbursement for later:

```
$request = new DisbursementRequest(
    amount: '10000',
    walletNumber: '255712345678',
    walletCode: 'ABC12345',
    accountNo: 'your-bpay-account',
    clientReferenceId: 'REF-001',
    scheduledTimeUtc: '2025-12-25 10:30:00'  // UTC timezone
);

$response = Beem::disbursement()->transfer($request);
```

> **Note:** Scheduling functionality may not be available in all environments.

#### 3. Error Handling

[](#3-error-handling)

The package provides detailed error handling with 14 response codes:

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\Exceptions\DisbursementException;

try {
    $response = Beem::disbursement()->transfer($request);
} catch (DisbursementException $e) {
    if ($e->isInsufficientBalance()) {
        return back()->withErrors(['error' => 'Insufficient wallet balance']);
    }

    if ($e->isInvalidPhoneNumber()) {
        return back()->withErrors(['phone' => 'Invalid phone number']);
    }

    if ($e->isAmountTooLarge()) {
        return back()->withErrors(['amount' => 'Amount exceeds limit']);
    }

    if ($e->isInvalidAuthentication()) {
        Log::error('Beem authentication failed');
        return back()->withErrors(['error' => 'Service unavailable']);
    }
}
```

**Available Response Codes:**

CodeDescriptionHelper Method100Disbursement successful`isSuccess()`101Disbursement failed`isFailure()`102Invalid phone number`isInvalidPhoneNumber()`103Insufficient balance`isInsufficientBalance()`104Network timeout`isNetworkTimeout()`105Invalid parameters`isInvalidParameters()`106Amount too large`isAmountTooLarge()`107Account not found`isAccountNotFound()`108No route mapping`isNoRoute()`109No authorization headers`isInvalidAuthentication()`110Invalid token`isInvalidAuthentication()`111Missing Destination MSISDN`isMissingMsisdn()`112Missing Disbursement Amount`isInvalidAmount()`113Invalid Disbursement Amount`isInvalidAmount()`> See [DisbursementResponseCode](src/Enums/DisbursementResponseCode.php) for all 14 response codes.

### Using Collections

[](#using-collections)

The package supports Beem's Payment Collections API for receiving mobile money payments.

#### 1. Check Balance

[](#1-check-balance)

Check your collection balance:

```
use Gowelle\BeemAfrica\Facades\Beem;

$balance = Beem::collection()->checkBalance();

echo "Balance: " . $balance->getFormattedBalance(); // e.g. "5,300.00"
echo "Raw: " . $balance->getBalanceAsFloat();       // e.g. 5300.0
```

#### 2. Handling Payment Callbacks

[](#2-handling-payment-callbacks)

When a subscriber makes a payment, Beem sends a callback to your webhook endpoint. The package dispatches a `CollectionReceived` event:

```
// app/Listeners/HandleCollectionPayment.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\CollectionReceived;

class HandleCollectionPayment
{
    public function handle(CollectionReceived $event): void
    {
        $transactionId = $event->getTransactionId();
        $amount = $event->getAmount();
        $phone = $event->getSubscriberMsisdn();
        $reference = $event->getReferenceNumber();

        // Process the payment (credit user account, fulfill order, etc.)
        Payment::create([
            'transaction_id' => $transactionId,
            'amount' => $amount,
            'phone' => $phone,
            'reference' => $reference,
            'status' => 'completed',
        ]);
    }
}
```

Register the listener:

```
// app/Providers/EventServiceProvider.php

use Gowelle\BeemAfrica\Events\CollectionReceived;
use App\Listeners\HandleCollectionPayment;

protected $listen = [
    CollectionReceived::class => [
        HandleCollectionPayment::class,
    ],
];
```

#### Collection Payload Data

[](#collection-payload-data)

The collection callback includes:

FieldDescription`transaction_id`Unique transaction ID from Beem`amount_collected`Payment amount`subscriber_msisdn`Payer's phone number`reference_number`Reference entered by subscriber`paybill_number`Your merchant/paybill number`network_name`Mobile network (Vodacom, Airtel, etc.)`source_currency`Source currency (TZS)`target_currency`Target currency (TZS)### Using USSD Hub

[](#using-ussd-hub)

The package supports Beem's USSD Hub for interactive menus.

#### 1. Check Balance

[](#1-check-balance-1)

```
use Gowelle\BeemAfrica\Facades\Beem;

$balance = Beem::ussd()->checkBalance();
echo "Balance: " . $balance->getFormattedBalance();
```

#### 2. Handling USSD Sessions

[](#2-handling-ussd-sessions)

When a subscriber dials your USSD code, Beem sends callbacks. Create a listener:

```
// app/Listeners/HandleUssdSession.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\UssdSessionReceived;

class HandleUssdSession
{
    public function handle(UssdSessionReceived $event): void
    {
        if ($event->isInitiate()) {
            // First menu
            $event->continueWith("Welcome!\n1. Check Balance\n2. Buy Airtime");
            return;
        }

        if ($event->isContinue()) {
            $response = $event->getSubscriberResponse();

            match ($response) {
                '1' => $event->terminateWith("Your balance: TZS 5,000"),
                '2' => $event->continueWith("Enter amount:"),
                default => $event->terminateWith("Invalid option"),
            };
        }
    }
}
```

Register the listener:

```
use Gowelle\BeemAfrica\Events\UssdSessionReceived;
use App\Listeners\HandleUssdSession;

protected $listen = [
    UssdSessionReceived::class => [
        HandleUssdSession::class,
    ],
];
```

#### USSD Commands

[](#ussd-commands)

CommandDescription`initiate`First invocation of session`continue`Ongoing session with subscriber response`terminate`Close the USSD session### Using Contacts

[](#using-contacts)

The package supports Beem's Contacts API for managing address books and contacts.

#### 1. AddressBook Management

[](#1-addressbook-management)

**List AddressBooks**

```
use Gowelle\BeemAfrica\Facades\Beem;

// List all address books
$response = Beem::contacts()->listAddressBooks();

foreach ($response->getAddressBooks() as $addressBook) {
    echo "{$addressBook->getAddressbook()}: {$addressBook->getContactsCount()} contacts\n";
}

// Access pagination data
$pagination = $response->getPagination();
echo "Total: {$pagination->getTotalItems()}\n";
echo "Page {$pagination->getCurrentPage()} of {$pagination->getTotalPages()}\n";
```

**Search AddressBooks**

```
// Search by name
$response = Beem::contacts()->listAddressBooks(query: 'Marketing');
```

**Create AddressBook**

```
use Gowelle\BeemAfrica\DTOs\AddressBookRequest;

$request = new AddressBookRequest(
    addressbook: 'VIP Customers',
    description: 'High value customer list'
);

$response = Beem::contacts()->createAddressBook($request);

if ($response->isSuccessful()) {
    $addressBookId = $response->getId();
    echo "Created: {$response->getMessage()}\n";
}
```

**Update AddressBook**

```
$request = new AddressBookRequest(
    addressbook: 'VIP Customers - Updated',
    description: 'Premium customer list'
);

$response = Beem::contacts()->updateAddressBook($addressBookId, $request);
```

**Delete AddressBook**

> **Note:** You cannot delete the 'Default' address book.

```
$response = Beem::contacts()->deleteAddressBook($addressBookId);

if ($response->isSuccessful()) {
    echo $response->getMessage();
}
```

#### 2. Contact Management

[](#2-contact-management)

**List Contacts**

```
// List all contacts in an address book
$response = Beem::contacts()->listContacts($addressBookId);

foreach ($response->getContacts() as $contact) {
    echo "{$contact->getFullName()}: {$contact->getMobileNumber()}\n";
    echo "Email: {$contact->getEmail()}\n";
}

// Search contacts by name or phone
$response = Beem::contacts()->listContacts($addressBookId, query: 'John');
```

**Create Contact**

```
use Gowelle\BeemAfrica\DTOs\ContactRequest;
use Gowelle\BeemAfrica\Enums\Gender;
use Gowelle\BeemAfrica\Enums\Title;

$request = new ContactRequest(
    mob_no: '255712345678',              // Required: Primary mobile number
    addressbook_id: [$addressBookId],    // Required: Array of address book IDs
    fname: 'John',                       // Optional: First name
    lname: 'Doe',                        // Optional: Last name
    title: Title::MR,                    // Optional: Title::MR / Title::MRS / Title::MS (or string 'Mr.' / 'Mrs.' / 'Ms.')
    gender: Gender::MALE,                // Optional: Gender::MALE / Gender::FEMALE (or string 'male' / 'female')
    email: 'john.doe@example.com',       // Optional: Email address
    mob_no2: '255787654321',             // Optional: Secondary mobile number
    country: 'Tanzania',                 // Optional: Country
    city: 'Dar es Salaam',               // Optional: City
    area: 'Kisutu',                      // Optional: Area/Locality
    birth_date: '1990-01-15'             // Optional: yyyy-mm-dd format
);

$response = Beem::contacts()->createContact($request);

if ($response->isSuccessful()) {
    $contactId = $response->getId();
    echo "Contact created: {$response->getMessage()}\n";
}
```

**Using Enums (Recommended)**

```
use Gowelle\BeemAfrica\Enums\Gender;
use Gowelle\BeemAfrica\Enums\Title;

// Gender enum
$request = new ContactRequest(
    mob_no: '255712345678',
    addressbook_id: [$addressBookId],
    gender: Gender::MALE,     // or Gender::FEMALE
);

// Title enum
$request = new ContactRequest(
    mob_no: '255712345678',
    addressbook_id: [$addressBookId],
    title: Title::MR,         // or Title::MRS, Title::MS
);

// Check gender
if ($request->gender === Gender::MALE) {
    // ...
}

// Get label
echo Gender::MALE->label();   // "Male"
echo Gender::FEMALE->label(); // "Female"
```

**Using Strings (Backward Compatible)**

```
// String values still work
$request = new ContactRequest(
    mob_no: '255712345678',
    addressbook_id: [$addressBookId],
    title: 'Mr.',      // 'Mr.' / 'Mrs.' / 'Ms.'
    gender: 'male',    // 'male' / 'female'
);
```

**Add Contact to Multiple AddressBooks**

```
// Add a contact to multiple address books at once
$request = new ContactRequest(
    mob_no: '255712345678',
    addressbook_id: [$addressBookId1, $addressBookId2, $addressBookId3],
    fname: 'Jane',
    lname: 'Smith'
);

$response = Beem::contacts()->createContact($request);
```

**Update Contact**

```
$request = new ContactRequest(
    mob_no: '255712345678',
    addressbook_id: [$addressBookId],
    fname: 'John',
    lname: 'Doe Updated',
    email: 'john.updated@example.com'
);

$response = Beem::contacts()->updateContact($contactId, $request);
```

**Delete Contacts**

```
use Gowelle\BeemAfrica\Facades\Beem;

// Delete specific contacts from specific address books
$response = Beem::contacts()->deleteContacts(
    addressBookIds: [$addressBookId],
    contactIds: [$contactId1, $contactId2]
);

if ($response->isSuccessful()) {
    echo $response->getMessage();
}
```

#### 3. Working with Contact Data

[](#3-working-with-contact-data)

```
// Access contact details
$response = Beem::contacts()->listContacts($addressBookId);

foreach ($response->getContacts() as $contact) {
    // Basic info
    $fullName = $contact->getFullName();        // "John Doe"
    $firstName = $contact->getFirstName();       // "John"
    $lastName = $contact->getLastName();         // "Doe"

    // Contact details
    $mobile = $contact->getMobileNumber();       // "255712345678"
    $mobile2 = $contact->getSecondaryMobileNumber();
    $email = $contact->getEmail();

    // Demographics
    $title = $contact->getTitle();               // "Mr."
    $gender = $contact->getGender();             // "male"
    $birthDate = $contact->getBirthDate();       // "1990-01-15"

    // Location
    $country = $contact->getCountry();
    $city = $contact->getCity();
    $area = $contact->getArea();

    // Metadata
    $createdAt = $contact->getCreated();         // ISO 8601 timestamp
    $contactId = $contact->getId();
}
```

#### 4. Pagination

[](#4-pagination)

Both AddressBooks and Contacts endpoints support pagination:

```
$response = Beem::contacts()->listContacts($addressBookId);

$pagination = $response->getPagination();

// Pagination info
echo "Total Items: {$pagination->getTotalItems()}\n";
echo "Current Page: {$pagination->getCurrentPage()}\n";
echo "Page Size: {$pagination->getPageSize()}\n";
echo "Total Pages: {$pagination->getTotalPages()}\n";

// Check for more pages
if ($pagination->hasMorePages()) {
    $nextPage = $pagination->getNextPage();
    echo "Next page: {$nextPage}\n";
}
```

#### 5. Error Handling

[](#5-error-handling)

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\DTOs\ContactRequest;
use Gowelle\BeemAfrica\Exceptions\ContactsException;

try {
    $request = new ContactRequest(
        mob_no: '255712345678',
        addressbook_id: [$addressBookId],
        fname: 'John'
    );

    $response = Beem::contacts()->createContact($request);
} catch (ContactsException $e) {
    // Handle API errors
    Log::error('Contact creation failed: ' . $e->getMessage());

    // HTTP status code
    $statusCode = $e->getCode();
} catch (\InvalidArgumentException $e) {
    // Handle validation errors
    Log::error('Invalid input: ' . $e->getMessage());
}
```

**Common Validation Errors:**

- Invalid phone number format (must be 10-15 digits, international format without +)
- Empty address book ID array
- Invalid birth date format (must be yyyy-mm-dd)
- Invalid gender (must be Gender::MALE, Gender::FEMALE, or strings 'male' / 'female')
- Invalid title (must be Title::MR, Title::MRS, Title::MS, or strings 'Mr.' / 'Mrs.' / 'Ms.')

**Available Enums:**

```
use Gowelle\BeemAfrica\Enums\Gender;
use Gowelle\BeemAfrica\Enums\Title;

// Gender Enum
Gender::MALE     // 'male'
Gender::FEMALE   // 'female'

// Gender methods
Gender::MALE->label()     // "Male"
Gender::MALE->isMale()    // true
Gender::MALE->isFemale()  // false

// Title Enum
Title::MR    // 'Mr.'
Title::MRS   // 'Mrs.'
Title::MS    // 'Ms.'

// Title methods
Title::MR->isMr()    // true
Title::MR->isMrs()   // false
Title::MR->isMs()    // false
```

#### 6. Best Practices

[](#6-best-practices)

**Phone Number Format**

```
// ✅ Correct - International format without +
'255712345678'   // Tanzania
'254712345678'   // Kenya
'256712345678'   // Uganda

// ❌ Incorrect
'+255712345678'  // Don't include +
'0712345678'     // Don't use local format
```

**Multiple AddressBooks**

```
// Add contact to multiple address books
$request = new ContactRequest(
    mob_no: '255712345678',
    addressbook_id: [$personalId, $workId, $familyId],
    fname: 'John'
);
```

**Batch Operations**

```
// Delete multiple contacts at once
Beem::contacts()->deleteContacts(
    addressBookIds: [$addressBookId],
    contactIds: [$contact1, $contact2, $contact3, $contact4]
);
```

### Using Moja (Multi-Channel Messaging)

[](#using-moja-multi-channel-messaging)

The package supports Beem's Moja API for multi-channel messaging with support for six message types across multiple channels.

#### 1. Configure Moja

[](#1-configure-moja)

Add your Moja credentials to `.env`:

```
BEEM_API_KEY=your_api_key
BEEM_SECRET_KEY=your_secret_key
```

#### 2. Get Active Sessions

[](#2-get-active-sessions)

Retrieve list of active chat sessions:

```
use Gowelle\BeemAfrica\Facades\Beem;

// Get all active sessions
$response = Beem::moja()->getActiveSessions();

foreach ($response->getSessions() as $session) {
    echo "Session: {$session->username} on {$session->channel}\n";
    echo "From: {$session->from_addr}\n";
}
```

#### 3. Send Messages - Six Types Supported

[](#3-send-messages---six-types-supported)

**Text Message**

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\DTOs\MojaMessageRequest;
use Gowelle\BeemAfrica\Enums\MojaChannel;
use Gowelle\BeemAfrica\Enums\MojaMessageType;

$request = new MojaMessageRequest(
    from: '255701000000',
    to: '255701000001',
    channel: MojaChannel::WHATSAPP,
    message_type: MojaMessageType::TEXT,
    text: 'Hello from Moja API!'
);

$response = Beem::moja()->sendMessage($request);

if ($response->isSuccess()) {
    echo "Message sent successfully!";
}
```

**Image Message**

```
use Gowelle\BeemAfrica\DTOs\MojaMediaObject;

$image = new MojaMediaObject(
    mime_type: 'image/jpeg',
    url: 'https://example.com/image.jpg'
);

$request = new MojaMessageRequest(
    from: '255701000000',
    to: '255701000001',
    channel: MojaChannel::WHATSAPP,
    message_type: MojaMessageType::IMAGE,
    image: $image,
    text: 'Check out this image!'  // Optional caption
);

$response = Beem::moja()->sendMessage($request);
```

**Document Message**

```
$document = new MojaMediaObject(
    mime_type: 'application/pdf',
    url: 'https://example.com/document.pdf'
);

$request = new MojaMessageRequest(
    from: '255701000000',
    to: '255701000001',
    channel: MojaChannel::WHATSAPP,
    message_type: MojaMessageType::DOCUMENT,
    document: $document
);

$response = Beem::moja()->sendMessage($request);
```

**Video Message**

```
$video = new MojaMediaObject(
    mime_type: 'video/mp4',
    url: 'https://example.com/video.mp4'
);

$request = new MojaMessageRequest(
    from: '255701000000',
    to: '255701000001',
    channel: MojaChannel::WHATSAPP,
    message_type: MojaMessageType::VIDEO,
    video: $video,
    text: 'Watch this video!'  // Optional caption
);

$response = Beem::moja()->sendMessage($request);
```

**Audio Message**

```
$audio = new MojaMediaObject(
    mime_type: 'audio/mpeg',
    url: 'https://example.com/audio.mp3'
);

$request = new MojaMessageRequest(
    from: '255701000000',
    to: '255701000001',
    channel: MojaChannel::WHATSAPP,
    message_type: MojaMessageType::AUDIO,
    audio: $audio
);

$response = Beem::moja()->sendMessage($request);
```

**Location Message**

```
use Gowelle\BeemAfrica\DTOs\MojaLocationObject;

$location = new MojaLocationObject(
    latitude: '-6.7924',
    longitude: '39.2083'
);

$request = new MojaMessageRequest(
    from: '255701000000',
    to: '255701000001',
    channel: MojaChannel::WHATSAPP,
    message_type: MojaMessageType::LOCATION,
    location: $location
);

$response = Beem::moja()->sendMessage($request);
```

#### 4. Channels Supported

[](#4-channels-supported)

```
use Gowelle\BeemAfrica\Enums\MojaChannel;

MojaChannel::WHATSAPP                    // WhatsApp
MojaChannel::FACEBOOK                    // Facebook Messenger
MojaChannel::INSTAGRAM                   // Instagram Direct Messages
MojaChannel::GOOGLE_BUSINESS_MESSAGING   // Google Business Messaging
```

#### 5. WhatsApp Templates

[](#5-whatsapp-templates)

**Fetch Available Templates**

```
// Get all templates
$response = Beem::moja()->fetchTemplates();

foreach ($response->getTemplates() as $template) {
    echo "Template: {$template->name}\n";
    echo "Category: {$template->category}\n";
    echo "Status: {$template->status}\n";
}

// Filter templates
$response = Beem::moja()->fetchTemplates([
    'category' => 'AUTHENTICATION',
    'status' => 'approved'
]);
```

**Send Template Message**

```
use Gowelle\BeemAfrica\DTOs\MojaTemplateRequest;

$request = new MojaTemplateRequest(
    from_addr: '255701000000',
    destination_addr: [
        [
            'phoneNumber' => '255712345678',
            'params' => ['John', '123456']  // Template parameters
        ]
    ],
    template_id: 1024
);

$response = Beem::moja()->sendTemplate($request);

if ($response->allRecipientsValid()) {
    echo "All {$response->validCounts} recipients are valid\n";
}
```

#### 6. Moja Error Handling

[](#6-moja-error-handling)

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\Exceptions\MojaException;

try {
    $response = Beem::moja()->sendMessage($request);
} catch (MojaException $e) {
    // Check for specific error types
    if ($e->isSessionExpired()) {
        return back()->withErrors(['error' => 'Chat session expired']);
    }

    if ($e->isAuthenticationError()) {
        Log::error('Moja authentication failed - check API credentials');
        return back()->withErrors(['error' => 'Service unavailable']);
    }

    if ($e->isRateLimited()) {
        return back()->withErrors(['error' => 'Too many requests, please try later']);
    }

    // Generic error handling
    Log::error('Moja error', [
        'message' => $e->getMessage(),
        'code' => $e->getCode(),
    ]);

    return back()->withErrors(['error' => 'Failed to send message']);
}
```

#### 7. Moja Webhooks

[](#7-moja-webhooks)

The package automatically registers webhook routes for Moja incoming messages and delivery reports.

**Incoming Message Webhook:**

Configure your incoming message webhook URL in Beem dashboard to point to:

```
https://yourapp.com/webhooks/beem/moja/incoming

```

**Create an event listener:**

```
// app/Listeners/HandleMojaIncomingMessage.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\MojaIncomingMessageReceived;

class HandleMojaIncomingMessage
{
    public function handle(MojaIncomingMessageReceived $event): void
    {
        $message = $event->message;

        if ($message->isTextMessage()) {
            // Handle text message
            ChatMessage::create([
                'from' => $message->from,
                'to' => $message->to,
                'channel' => $message->channel,
                'text' => $message->text,
            ]);
        } elseif ($message->hasMedia()) {
            // Handle media message
            if ($message->image) {
                // Process image
            } elseif ($message->document) {
                // Process document
            }
        }
    }
}
```

**Delivery Report Webhook:**

Configure your delivery report webhook URL to:

```
https://yourapp.com/webhooks/beem/moja/dlr

```

**Create an event listener:**

```
// app/Listeners/HandleMojaDeliveryReport.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\MojaDeliveryReportReceived;

class HandleMojaDeliveryReport
{
    public function handle(MojaDeliveryReportReceived $event): void
    {
        $report = $event->report;

        if ($report->isRead()) {
            // Message was read by recipient
            ChatMessage::where('message_id', $report->message_id)
                ->update(['status' => 'read']);
        } elseif ($report->isFailed()) {
            // Message delivery failed
            ChatMessage::where('message_id', $report->message_id)
                ->update(['status' => 'failed']);
        }
    }
}
```

**Register the listeners:**

```
// app/Providers/EventServiceProvider.php

use Gowelle\BeemAfrica\Events\MojaIncomingMessageReceived;
use Gowelle\BeemAfrica\Events\MojaDeliveryReportReceived;
use App\Listeners\HandleMojaIncomingMessage;
use App\Listeners\HandleMojaDeliveryReport;

protected $listen = [
    MojaIncomingMessageReceived::class => [
        HandleMojaIncomingMessage::class,
    ],
    MojaDeliveryReportReceived::class => [
        HandleMojaDeliveryReport::class,
    ],
];
```

### Using Multicountry SMS and SMPP

[](#using-multicountry-sms-and-smpp)

#### 1. Configure Credentials

[](#1-configure-credentials)

Add to `.env`:

```
BEEM_INTERNATIONAL_SMS_USERNAME=...
BEEM_INTERNATIONAL_SMS_PASSWORD=...
BEEM_INTERNATIONAL_SMS_DLR_URL=...  # Optional: Your DLR webhook URL
```

#### 2. Send International SMS

[](#2-send-international-sms)

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\DTOs\InternationalSmsRequest;

$request = new InternationalSmsRequest(
    sourceAddr: 'Gowelle',
    destAddr: '255712345678', // International format
    message: 'Hello World',
);

$response = Beem::internationalSms()->send($request);

if ($response->isSuccessful()) {
    echo $response->getFirstMessageId();
}
```

#### 3. Send Binary Message

[](#3-send-binary-message)

```
$request = InternationalSmsRequest::createBinary(
    sourceAddr: 'Gowelle',
    destAddr: '255712345678',
    hexMessage: '00480065006C006C006F' // "Hello" in UTF-16BE Hex
);
Beem::internationalSms()->send($request);
```

#### 4. Check Balance

[](#4-check-balance)

```
$balance = Beem::internationalSms()->checkBalance();
echo $balance->balance . ' ' . $balance->currency;
```

#### 5. Send to Multiple Recipients

[](#5-send-to-multiple-recipients)

Send the same message to multiple destinations in a single request:

```
use Gowelle\BeemAfrica\DTOs\InternationalSmsRequest;

$request = new InternationalSmsRequest(
    sourceAddr: 'Gowelle',
    destAddr: [
        '255712345678',  // Tanzania
        '254712345678',  // Kenya
        '256712345678',  // Uganda
    ],
    message: 'Hello to multiple recipients!',
);

$response = Beem::internationalSms()->send($request);

if ($response->isSuccessful()) {
    // Access all message IDs from results
    foreach ($response->results as $result) {
        echo "Status: {$result['status']}, Message ID: {$result['msgid']}\n";
    }
}
```

#### 6. Encoding Options

[](#6-encoding-options)

The `encoding` parameter controls the message format. Supported values:

ValueTypeDescriptionUse Case0TextPlain text message (default)Regular SMS1FlashFlash message (displays immediately)Urgent notifications2Binary/UnicodeUnicode or hex-encoded messageSpecial characters, emoji3ISO-8859-1Latin alphabet encodingEuropean languages**Example - Flash Message:**

```
$request = new InternationalSmsRequest(
    sourceAddr: 'Gowelle',
    destAddr: '255712345678',
    message: 'URGENT: System Alert',
    encoding: 1,  // Flash message
);

$response = Beem::internationalSms()->send($request);
```

**Example - Unicode Message:**

```
use Gowelle\BeemAfrica\DTOs\InternationalSmsRequest;

// Using the static factory method for binary messages
$request = InternationalSmsRequest::createBinary(
    sourceAddr: 'Gowelle',
    destAddr: '255712345678',
    hexMessage: '0410043704380432043E',  // "Привет" (Hello in Russian) in UTF-16BE
);

$response = Beem::internationalSms()->send($request);
```

#### 7. Response Handling

[](#7-response-handling)

The response object provides access to detailed information about the send operation:

```
$response = Beem::internationalSms()->send($request);

// Check overall success
if ($response->isSuccessful()) {
    echo "At least one message sent successfully\n";
}

// Access individual results
foreach ($response->results as $result) {
    $status = $result['status'];      // "0" = OK, other values = error
    $msgid = $result['msgid'];        // Unique message ID
    $statustext = $result['statustext'];  // Status description (e.g., "OK")

    if ($status === '0') {
        echo "Message {$msgid} sent successfully\n";
    } else {
        echo "Message failed with status {$status}: {$statustext}\n";
    }
}

// Check account balance after sending
if ($response->balance !== null) {
    echo "Remaining balance: {$response->balance}\n";
}

// Get the first message ID (useful for single recipient)
$firstMessageId = $response->getFirstMessageId();
```

#### 8. Error Handling

[](#8-error-handling)

The package provides structured error handling using `SmsException`:

```
use Gowelle\BeemAfrica\Facades\Beem;
use Gowelle\BeemAfrica\Exceptions\SmsException;
use Gowelle\BeemAfrica\Enums\SmsResponseCode;

try {
    $request = new InternationalSmsRequest(
        sourceAddr: 'Gowelle',
        destAddr: '255712345678',
        message: 'Hello World',
    );

    $response = Beem::internationalSms()->send($request);
} catch (SmsException $e) {
    // Check for specific error types
    if ($e->isInsufficientBalance()) {
        return back()->withErrors(['error' => 'Insufficient International SMS credits']);
    }

    if ($e->isInvalidPhoneNumber()) {
        return back()->withErrors(['phone' => 'Invalid phone number format']);
    }

    if ($e->isInvalidAuthentication()) {
        Log::error('International SMS authentication failed - check credentials');
        return back()->withErrors(['error' => 'Service unavailable']);
    }

    if ($e->isNetworkTimeout()) {
        return back()->withErrors(['error' => 'Network timeout - please try again']);
    }

    // Get the response code enum for additional details
    $responseCode = $e->getResponseCode();
    if ($responseCode) {
        Log::error('International SMS error', [
            'code' => $responseCode->value,
            'description' => $responseCode->description(),
        ]);
    }

    return back()->withErrors(['error' => 'Failed to send SMS. Please try again.']);
}
```

**Available Error Codes:**

CodeDescriptionHelper Method100Message Submitted Successfully`isSuccess()`101Invalid phone number`isInvalidPhoneNumber()`102Insufficient balance`isInsufficientBalance()`103Network timeout`isNetworkTimeout()`104Please provide all required parameters`isMissingParameters()`105Account not found`isAccountNotFound()`106No route mapping to your account`isNoRoute()`107No authorization headers`isInvalidAuthentication()`108Invalid token`isInvalidAuthentication()`> See [SmsResponseCode](src/Enums/SmsResponseCode.php) for all 9 response codes.

#### 9. Webhook Handling (DLR - Delivery Report)

[](#9-webhook-handling-dlr---delivery-report)

Beem sends delivery reports (DLR) to your webhook endpoint when messages are delivered or fail.

##### Register the Webhook Route

[](#register-the-webhook-route)

In your routes file or service provider, register the International SMS webhook macro:

```
// routes/web.php or routes/api.php

Route::beemInternationalWebhook('webhooks/beem/international');
// This registers: POST /webhooks/beem/international
```

Or use a custom URL:

```
Route::beemInternationalWebhook('custom/international/dlr');
// This registers: POST /custom/international/dlr
```

##### Create an Event Listener

[](#create-an-event-listener)

```
// app/Listeners/HandleInternationalDlr.php

namespace App\Listeners;

use Gowelle\BeemAfrica\Events\InternationalDlrReceived;
use Illuminate\Support\Facades\Log;

class HandleInternationalDlr
{
    public function handle(InternationalDlrReceived $event): void
    {
        $dlrId = $event->getDlrId();
        $from = $event->getSourceAddr();
        $to = $event->getDestAddr();
        $message = $event->getMessage();
        $payload = $event->payload;

        // Access full payload for additional data
        Log::info('International DLR Received', [
            'dlr_id' => $dlrId,
            'from' => $from,
            'to' => $to,
            'payload' => $payload,
        ]);

        // Update your database with delivery status
        // Example: Find the SMS record and update its status
        InternationalSmsLog::updateOrCreate(
            ['dlr_id' => $dlrId],
            [
                'status' => $payload['status'] ?? 'received',
                'delivered_at' => now(),
            ]
        );
    }
}
```

##### Register the Listener

[](#register-the-listener)

```
// app/Providers/EventServiceProvider.php

use Gowelle\BeemAfrica\Events\InternationalDlrReceived;
use App\Listeners\HandleInternationalDlr;

protected $listen = [
    InternationalDlrReceived::class => [
        HandleInternationalDlr::class,
    ],
];
```

##### Access Webhook Payload Data

[](#access-webhook-payload-data)

The `InternationalDlrReceived` event provides helper methods to access common fields:

```
public function handle(InternationalDlrReceived $event): void
{
    // Helper methods (case-insensitive field access)
    $dlrId = $event->getDlrId();           // DLRID or dlrid
    $from = $event->getSourceAddr();       // SOURCEADDR or from
    $to = $event->getDestAddr();           // DESTADDR or to
    $message = $event->getMessage();       // MESSAGE or text

    // Full payload access for any field
    $fullPayload = $event->payload;
    $status = $fullPayload['status'] ?? $fullPayload['STATUS'] ?? null;
    $timestamp = $fullPayload['timestamp'] ?? $fullPayload['TIMESTAMP'] ?? null;
}
```

##### Configure DLR URL in .env

[](#configure-dlr-url-in-env)

```
BEEM_INTERNATIONAL_SMS_DLR_URL=https://yourapp.com/webhooks/beem/international
```

Then use it in your send request (optional):

```
$request = new InternationalSmsRequest(
    sourceAddr: 'Gowelle',
    destAddr: '255712345678',
    message: 'Hello World',
    dlrAddress: env('BEEM_INTERNATIONAL_SMS_DLR_URL'),  // Set webhook URL per request
);

$response = Beem::internationalSms()->send($request);
```

Or configure it globally in `config/beem-africa.php`:

```
'international_sms' => [
    'username' => env('BEEM_INTERNATIONAL_SMS_USERNAME'),
    'password' => env('BEEM_INTERNATIONAL_SMS_PASSWORD'),
    'base_url' => 'https://api.blsmsgw.com:8443/bin',
    'portal_url' => 'https://www.blsmsgw.com/portal/api',
    'dlr_url' => env('BEEM_INTERNATIONAL_SMS_DLR_URL'),
],
```

UI Components
-------------

[](#ui-components-1)

The package includes ready-to-use UI components for both Livewire and Vue/InertiaJS applications, with full TypeScript support and localization.

### Quick Links

[](#quick-links)

- [Livewire Components](#livewire-components)
- [Vue/InertiaJS Components](#vueinertiajs-components)
- [Vue Composables](#composables)
- [Backend Routes for Vue](#backend-routes-for-vue-components)
- [Styling Customization](#styling-customization)
- [Labels Reference](#labels-reference)

---

### Livewire Components

[](#livewire-components)

Livewire v3 components are automatically registered when Livewire is installed. No additional setup required.

#### BeemCheckout

[](#beemcheckout)

A payment checkout component with amount input, reference, and mobile number fields.

```
{{-- Basic usage with pre-set amount --}}

{{-- Dynamic form (amount editable by user) --}}

```

##### Props

[](#props)

PropTypeDefaultDescription`amount``float``0`Payment amount. If `0`, displays an editable input field`reference``string``''`Order reference. If empty, displays an editable input field`mobile``?string``null`Pre-filled mobile number (optional)##### Public Properties

[](#public-properties)

PropertyTypeDescription`isProcessing``bool``true` while checkout is being initiated`errorMessage``?string`Error message if checkout fails`checkoutUrl``?string`Generated Beem checkout URL##### Events

[](#events)

EventPayloadDescription`beem-checkout-initiated``{url, reference, amount}`Checkout URL successfully generated`beem-checkout-error``{message, code}`Checkout failed. `code` is the Beem error code##### Listening to Events

[](#listening-to-events)

```

document.addEventListener('beem-checkout-initiated', (event) => {
    console.log('Checkout URL:', event.detail.url);
    // Optionally redirect or show modal
});

```

---

#### BeemOtpVerification

[](#beemotpverification)

A two-step OTP verification component with phone input and code verification.

```
{{-- Basic usage --}}

{{-- With pre-filled phone number --}}

```

##### Props

[](#props-1)

PropTypeDefaultDescription`phone``?string``null`Pre-filled phone number##### Public Properties

[](#public-properties-1)

PropertyTypeDescription`phone``string`Current phone number`otpCode``string`User-entered OTP code`pinId``?string`Beem PIN ID for verification`isRequesting``bool``true` while requesting OTP`isVerifying``bool``true` while verifying OTP`isVerified``bool``true` after successful verification`otpSent``bool``true` after OTP is sent`resendCooldown``int`Seconds until resend is allowed (60s default)##### Events

[](#events-1)

EventPayloadDescription`beem-otp-sent``{phone}`OTP sent successfully`beem-otp-verified``{phone}`Phone number verified`beem-otp-error``{message, code}`OTP request failed`beem-otp-verification-failed``{message}`Verification failed##### Complete Example

[](#complete-example)

```

        ✓ Phone verified! You can now proceed.

```

---

#### BeemSmsForm

[](#beemsmsform)

A full-featured SMS form with recipient management, character counting, and scheduling.

```

```

##### Public Properties

[](#public-properties-2)

PropertyTypeDescription`senderName``string`Sender ID (max 11 characters)`message``string`SMS message content`recipients``array`List of phone numbers`newRecipient``string`Input field for adding recipients`scheduleTime``?string`Optional schedule datetime`isSending``bool``true` while sending SMS##### Computed Properties

[](#computed-properties)

PropertyTypeDescription`characterCount``int`Current message length`smsSegments``int`Number of SMS segments (160 chars = 1, 153 chars per additional)`remainingCharacters``int`Characters remaining in current segment##### Events

[](#events-2)

EventPayloadDescription`beem-sms-sent``{recipients, segments}`SMS sent successfully`beem-sms-error``{message}`SMS sending failed##### Features

[](#features-1)

- **Recipient Management**: Add/remove multiple recipients with validation
- **Character Counter**: Real-time count with SMS segment display
- **Scheduling**: Optional datetime picker for scheduled messages
- **Validation**: Phone format validation (10-15 digits)

---

### Vue/InertiaJS Components

[](#vueinertiajs-components)

TypeScript Vue 3 components for InertiaJS applications with full type safety.

#### Publishing Components

[](#publishing-components)

```
php artisan vendor:publish --tag="beem-africa-vue"
```

This publishes components to `resources/js/vendor/beem-africa/`:

```
resources/js/vendor/beem-africa/
├── Components/
│   ├── BeemCheckoutButton.vue
│   ├── BeemOtpVerification.vue
│   └── BeemSmsForm.vue
├── Composables/
│   └── useBeem.ts
├── index.ts
└── types.d.ts

```

---

#### BeemCheckoutButton

[](#beemcheckoutbutton)

A payment button that redirects to Beem's checkout page.

```

import { BeemCheckoutButton } from '@/vendor/beem-africa';

const handleCheckout = (event: { checkoutUrl: string }) => {
  console.log('Redirecting to:', event.checkoutUrl);
};

const handleError = (event: { message: string }) => {
  console.error('Checkout error:', event.message);
};

```

##### Props

[](#props-2)

PropTypeRequiredDefaultDescription`amount``number`✅-Payment amount`token``string`✅-Beem secure token`reference``string`✅-Order reference number`transactionId``string`✅-Unique transaction ID`mobile``string`❌`null`Customer mobile number`buttonText``string`❌`'Pay Now'`Button label`disabled``boolean`❌`false`Disable the button`redirectOnInit``boolean`❌`true`Auto-redirect to Beem checkout`labels``Labels`❌`{}`Custom labels object##### Events

[](#events-3)

EventPayloadDescription`checkout-initiated``{amount, transactionId, reference, checkoutUrl}`Checkout URL generated`checkout-error``{message}`Error occurred`checkout-complete`-Checkout flow complete##### Exposed Methods (via ref)

[](#exposed-methods-via-ref)

```

import { ref } from 'vue';

const checkoutRef = ref();

// Access exposed methods
checkoutRef.value?.initiateCheckout();
console.log(checkoutRef.value?.isLoading);
console.log(checkoutRef.value?.error);

```

---

#### BeemOtpVerification

[](#beemotpverification-1)

A two-step phone verification component.

```

import { BeemOtpVerification } from '@/vendor/beem-africa';

const handleVerified = (event: { phone: string }) => {
  console.log('Phone verified:', event.phone);
  // Update user profile, enable features, etc.
};

```

##### Props

[](#props-3)

PropTypeRequiredDefaultDescription`initialPhone``string`❌`''`Pre-filled phone number`otpLength``number`❌`6`Expected OTP code length`requestUrl``string`❌`'/beem/otp/request'`Backend endpoint for OTP request`verifyUrl``string`❌`'/beem/otp/verify'`Backend endpoint for verification`labels``Labels`❌`{}`Custom labels (21 keys available)##### Events

[](#events-4)

EventPayloadDescription`otp-sent``{phone}`OTP sent successfully`verified``{phone}`Phone number verified`error``{message}`Error occurred`reset`-Form was reset##### Exposed State

[](#exposed-state)

```
// Available via ref
{
  phone: Ref,
  otpCode: Ref,
  pinId: Ref,
  isRequesting: Ref,
  isVerifying: Ref,
  isVerified: Ref,
  otpSent: Ref,
  error: Ref,
  requestOtp: () => Promise,
  verifyOtp: () => Promise,
  resendOtp: () => void,
  resetVerification: () => void,
}
```

---

#### BeemSmsForm

[](#beemsmsform-1)

A full-featured SMS composition form.

```

import { BeemSmsForm } from '@/vendor/beem-africa';

const handleSmsSent = (event: { recipients: number; segments: number }) => {
  console.log(`Sent ${event.segments} segments to ${event.recipients} recipients`);
};

```

##### Props

[](#props-4)

PropTypeRequiredDefaultDescription`senderName``string`❌`''`Default sender ID (max 11 chars)`sendUrl``string`❌`'/beem/sms/send'`Backend endpoint for sending SMS`maxCharacters``number`❌`918`Maximum message length`labels``Labels`❌`{}`Custom labels (16 keys available)##### Events

[](#events-5)

EventPayloadDescription`sms-sent``{recipients, segments}`SMS sent successfully`error``{message}`Error occurred`reset`-Form was reset---

### Composables

[](#composables)

TypeScript composables for building custom UI implementations.

```
import { useBeemCheckout, useBeemOtp, useBeemSms } from '@/vendor/beem-africa';
```

#### useBeemCheckout

[](#usebeemcheckout)

```
const {
  isLoading,      // Ref - Loading state
  error,          // Ref - Error message
  checkoutUrl,    // Ref - Generated checkout URL
  initiateCheckout, // (options: CheckoutOptions) => Promise
  reset,          // () => void - Reset state
} = useBeemCheckout();

// Usage
const result = await initiateCheckout({
  amount: 1000,
  transactionId: 'TXN-123',
  reference: 'ORDER-001',
  mobile: '255712345678',
  redirectOnInit: false, // Don't auto-redirect
});

if (result.success) {
  console.log('Checkout URL:', result.url);
}
```

#### useBeemOtp

[](#usebeemotp)

```
const {
  isRequesting,   // Ref - Requesting OTP
  isVerifying,    // Ref - Verifying OTP
  isVerified,     // Ref - Verification successful
  pinId,          // Ref - Beem PIN ID
  error,          // Ref - Error message
  requestOtp,     // (phone: string) => Promise
  verifyOtp,      // (otpCode: string, customPinId?: string) => Promise
  reset,          // () => void - Reset state
} = useBeemOtp({
  requestUrl: '/api/beem/otp/request', // Optional
  verifyUrl: '/api/beem/otp/verify',   // Optional
});

// Usage
const requestResult = await requestOtp('255712345678');
if (requestResult.success) {
  // Show OTP input...
  const verifyResult = await verifyOtp('123456');
  if (verifyResult.valid) {
    console.log('Phone verified!');
  }
}
```

#### useBeemSms

[](#usebeemsms)

```
const {
  isSending,      // Ref - Sending state
  error,          // Ref - Error message
  lastResponse,   // Ref - Last API response
  sendSms,        // (smsData: SmsData) => Promise
  calculateSegments,     // (message: string) => number
  calculateCharacterCount, // (message: string) => number
  reset,          // () => void - Reset state
} = useBeemSms({
  sendUrl: '/api/beem/sms/send', // Optional
});

// Usage
const segments = calculateSegments('Hello World!'); // Returns 1

const result = await sendSms({
  senderName: 'MYAPP',
  message: 'Hello World!',
  recipients: ['255712345678', '255787654321'],
  scheduleTime: null,
});

if (result.success) {
  console.log(`Sent to ${result.recipients} recipients`);
}
```

---

### Backend Routes for Vue Components

[](#backend-routes-for-vue-components)

Vue components require backend endpoints to communicate with Beem APIs. Here's how to set them up:

#### OTP Routes

[](#otp-routes)

```
// routes/web.php or routes/api.php

use Gowelle\BeemAfrica\Facades\Beem;
use Illuminate\Http\Request;

Route::post('/beem/otp/request', function (Request $request) {
    $request->validate(['phone' => 'required|string|regex:/^[0-9]{10,15}$/']);

    try {
        $response = Beem::otp()->request($request->phone);

        return response()->json([
            'success' => $response->isSuccessful(),
            'pinId' => $response->getPinId(),
        ]);
    } catch (\Exception $e) {
        return response()->json([
            'success' => false,
            'message' => 'Failed to send OTP',
        ], 500);
    }
});

Route::post('/beem/otp/verify', function (Request $request) {
    $request->validate([
        'pinId' => 'required|string',
        'otpCode' => 'required|string|min:4|max:6',
    ]);

    try {
        $result = Beem::otp()->verify($request->pinId, $request->otpCode);

        if ($result->isValid()) {
            // Mark phone as verified in your database
            // auth()->user()->update(['phone_verified_at' => now()]);
        }

        return response()->json([
            'valid' => $result->isValid(),
        ]);
    } catch (\Exception $e) {
        return response()->json([
            'valid' => false,
            'message' => $e->getMessage(),
        ], 500);
    }
});
```

#### SMS Route

[](#sms-route)

```
use Gowelle\BeemAfrica\DTOs\SmsRecipient;
use Gowelle\BeemAfrica\DTOs\SmsRequest;
use Gowelle\BeemAfrica\Facades\Beem;

Route::post('/beem/sms/send', function (Request $request) {
    $request->validate([
        'senderName' => 'required|string|max:11',
        'message' => 'required|string|max:918',
        'recipients' => 'required|array|min:1',
        'recipients.*' => 'string|regex:/^[0-9]{10,15}$/',
        'scheduleTime' => 'nullable|date',
    ]);

    try {
        $recipients = array_map(
            fn ($phone) => new SmsRecipient(
                recipientId: uniqid('rcpt_'),
                destAddr: $phone
            ),
            $request->recipients
        );

        $smsRequest = new SmsRequest(
            sourceAddr: $request->senderName,
            message: $request->message,
            recipients: $recipients,
            scheduleTime: $request->scheduleTime,
        );

        $response = Beem::sms()->send($smsRequest);

        return response()->json([
            'success' => $response->isSuccessful(),
        ]);
    } catch (\Exception $e) {
        return response()->json([
            'success' => false,
            'message' => $e->getMessage(),
        ], 500);
    }
});
```

> **Tip:** For API routes, add them to `routes/api.php` and update the component's `requestUrl`, `verifyUrl`, or `sendUrl` props to include the `/api` prefix.

---

### Styling Customization

[](#styling-customization)

All components use scoped CSS with Beem's brand colors. You can customize styling in several ways:

#### 1. Override with CSS Variables (Recommended)

[](#1-override-with-css-variables-recommended)

```
/* In your app.css */
:root {
  --beem-primary: #33B1BA;
  --beem-primary-dark: #2a9aa3;
  --beem-secondary: #F3A929;
  --beem-text: #2D2D2C;
  --beem-text-muted: #555555;
  --beem-error: #dc3545;
  --beem-success: #16a34a;
}
```

#### 2. Deep Selectors (Vue)

[](#2-deep-selectors-vue)

```

/* Override component styles */
:deep(.beem-btn-primary) {
  background: linear-gradient(135deg, #your-color 0%, #darker-shade 100%);
}

:deep(.beem-input:focus) {
  border-color: #your-color;
  box-shadow: 0 0 0 3px rgba(your-color, 0.15);
}

```

#### 3. Publish and Edit Blade Views (Livewire)

[](#3-publish-and-edit-blade-views-livewire)

```
php artisan vendor:publish --tag="beem-africa-views"
```

This publishes views to `resources/views/vendor/beem-africa/livewire/` for full customization.

---

### Labels Reference

[](#labels-reference)

Both Vue and Livewire components support localization via labels.

#### Vue Components

[](#vue-components-1)

Pass a `labels` prop to customize text:

```

```

##### BeemCheckoutButton Labels

[](#beemcheckoutbutton-labels)

KeyDefaultDescription`amount``'Amount'`Amount label`payNow``'Pay Now'`Button text`processing``'Processing...'`Loading text`failedToInitiate``'Failed to initiate checkout'`Error message##### BeemOtpVerification Labels (21 keys)

[](#beemotpverification-labels-21-keys)

KeyDefault`verified``'Verified!'``verifyYourPhone``'Verify Your Phone'``enterPhoneToReceiveCode``'Enter your phone number to receive a verification code'``phoneNumber``'Phone Number'``sendOtp``'Send OTP'``sending``'Sending...'``enterVerificationCode``'Enter Verification Code'``weSentCodeTo``'We sent a code to :phone'``verificationCode``'Verification Code'``enterCode``'Enter code'``verify``'Verify'``verifying``'Verifying...'``resendIn``'Resend in :seconds s'``resendCode``'Resend Code'``changeNumber``'Change Number'``invalidOtp``'Invalid OTP code'``failedToSendOtp``'Failed to send OTP'``networkError``'Network error. Please try again.'``verifiedSuccess``'Phone number verified successfully!'``verificationFailed``'Verification failed. Please try again.'``invalidPhoneFormat``'Invalid phone number format (10-15 digits)'`##### BeemSmsForm Labels (16 keys)

[](#beemsmsform-labels-16-keys)

KeyDefault`senderName``'Sender Name'``senderPlaceholder``'MYAPP'``maxCharactersHint``'Max 11 characters'``recipients``'Recipients'``phonePlaceholder``'255XXXXXXXXX'``recipientsAdded``':count recipient(s) added'``message``'Message'``messagePlaceholder``'Type your message here...'``scheduleOptional``'Schedule (Optional)'``leaveEmptyHint``'Leave empty to send immediately'``sendSms``'Send SMS'``sending``'Sending...'``reset``'Reset'``invalidPhoneFormat``'Invalid phone number format (10-15 digits required)'``recipientAlreadyAdded``'This number is already added'``smsSentSuccess``'SMS sent to :count recipient(s)'`#### Livewire Components

[](#livewire-components-1)

Livewire components use Laravel's translation system. Publish and customize translations:

```
php artisan vendor:publish --tag="beem-africa-translations"
```

Edit files in `resources/lang/vendor/beem-africa/`.

---

### Running Component Tests

[](#running-component-tests)

#### Vue Component Tests

[](#vue-component-tests)

```
npm run test:run
```

#### Livewire Tests

[](#livewire-tests)

```
composer test
```

> **Note:** The package includes 104 component tests (75 Vue + 29 Livewire) to ensure reliability.

Testing
-------

[](#testing)

### Unit &amp; Feature Tests

[](#unit--feature-tests)

Run the test suite (excludes integration tests by default):

```
composer test
```

### Integration Tests

[](#integration-tests)

Integration tests require Beem sandbox credentials. Set the environment variables and run:

```
BEEM_API_KEY=your_api_key BEEM_SECRET_KEY=your_secret_key ./vendor/bin/pest --group=integration
```

### Static Analysis

[](#static-analysis)

```
composer analyse
```

### Code Style

[](#code-style)

```
composer format
```

Continuous Integration
----------------------

[](#continuous-integration)

The package includes GitHub Actions workflows:

### `tests.yml`

[](#testsyml)

- Runs on every push/PR to main
- Tests against PHP 8.3, 8.4, and 8.5
- Tests against Laravel 11, 12, and 13
- Runs PHPStan static analysis
- Checks code style with Pint

### `integration.yml`

[](#integrationyml)

- Runs weekly or on manual dispatch
- Runs integration tests with Beem sandbox
- Requires `BEEM_API_KEY`, `BEEM_SECRET_KEY`, and `BEEM_WEBHOOK_SECRET` secrets

To set up CI for your fork:

1. Go to your repository Settings → Secrets and variables → Actions
2. Add the following secrets:
    - `BEEM_API_KEY`: Your Beem sandbox API key
    - `BEEM_SECRET_KEY`: Your Beem sandbox secret key
    - `BEEM_WEBHOOK_SECRET`: Your webhook secret (optional)

Roadmap
-------

[](#roadmap)

See [ROADMAP.md](ROADMAP.md) for planned features and future development.

Contributing
------------

[](#contributing)

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.

Security
--------

[](#security)

If you discover any security-related issues, please email  instead of using the issue tracker.

Credits
-------

[](#credits)

- [Gowelle](https://github.com/gowelle)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

44

—

FairBetter than 92% of packages

Maintenance90

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity58

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~5 days

Recently: every ~23 days

Total

19

Last Release

52d ago

Major Versions

1.11.0 → v2.0.02026-03-21

PHP version history (2 changes)v1.0.0PHP ^8.2

v2.0.0PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/d77e8d8a848a61e17982aa6353f62833194bee3d956314b3eb80baa539f4cf29?d=identicon)[gowelle](/maintainers/gowelle)

---

Top Contributors

[![gowelle](https://avatars.githubusercontent.com/u/87917924?v=4)](https://github.com/gowelle "gowelle (42 commits)")

---

Tags

laravelotpsmscollectionsmessagingpaymentwhatsappcheckoutcontactsmobile-moneytanzaniaairtimeafricaaddressbookinternational-smsbeem-africabeemdisbursementsmojaglobal-sms

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/gowelle-laravel-beem-africa/health.svg)

```
[![Health](https://phpackages.com/badges/gowelle-laravel-beem-africa/health.svg)](https://phpackages.com/packages/gowelle-laravel-beem-africa)
```

###  Alternatives

[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

255.2k](/packages/aedart-athenaeum)[musahmusah/laravel-multipayment-gateways

A Laravel Package that makes implementation of multiple payment Gateways endpoints and webhooks seamless

852.2k1](/packages/musahmusah-laravel-multipayment-gateways)[asciisd/knet

Knet package is provides an expressive, fluent interface to KNet's payment services.

141.1k](/packages/asciisd-knet)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
