Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Africastalking Laravel Package

laravel-notification-channels/africastalking

Laravel notification channel for Africa's Talking SMS. Send SMS notifications through Africa's Talking via Laravel's built-in notification system, with a simple channel driver and minimal configuration for quick integration into your app.

View on GitHub
Deep Wiki
Context7

Getting Started

Goal of your first hour: one SMS arriving on one phone. Do not design a notification architecture first.

composer require laravel-notification-channels/africastalking

The Africa's Talking SDK comes in transitively — you install one package. If you later need voice, USSD, airtime, or mobile money you go to the SDK or REST API directly; this package is SMS only.

Read these three things, in this order:

  1. The README's configuration block. This is the highest-value five minutes you'll spend on the package. Depending on the major version, credentials live either in config/services.php under an africastalking key or in a published config/africastalking.php. Getting this wrong produces the classic "env vars are set but the channel says credentials are missing" hour of your life you don't get back.
  2. src/AfricasTalkingChannel.php — one small class. It defines the exact method name the channel looks for, how the recipient is resolved, and whether exceptions bubble. This is the entire surface you will ever depend on.
  3. src/AfricasTalkingMessage.php — the fluent builder you return from your notification.

Configuration (README-verified location for current majors):

// config/services.php
'africastalking' => [
    'username' => env('AFRICASTALKING_USERNAME'),
    'api_key'  => env('AFRICASTALKING_API_KEY'),
    'from'     => env('AFRICASTALKING_FROM'),
],
AFRICASTALKING_USERNAME=sandbox
AFRICASTALKING_API_KEY=atsk_xxxxxxxxxxxxxxxxxxxx
AFRICASTALKING_FROM=MYBRAND

sandbox as the username routes you to the AT sandbox, which only delivers to numbers you have whitelisted on the Africa's Talking dashboard. Add your own handset there before you write a single line of application code. Otherwise your first test "succeeds" with a 200 and no SMS, and you'll spend an afternoon debugging your notifiable.

First use case — a single OTP. Not a campaign, not a broadcast. An OTP forces you to solve, on day one, the four things that actually bite: phone normalization, recipient resolution, queue behaviour, and failure handling. A broadcast notification teaches you none of them and hides all four behind volume.

php artisan make:notification SendOtp
namespace App\Notifications;

use Illuminate\Notifications\Notification;
use NotificationChannels\AfricasTalking\AfricasTalkingChannel;
use NotificationChannels\AfricasTalking\AfricasTalkingMessage;

class SendOtp extends Notification
{
    public function __construct(public string $code) {}

    public function via(object $notifiable): array
    {
        return [AfricasTalkingChannel::class];
    }

    public function toAfricasTalking(object $notifiable): AfricasTalkingMessage
    {
        return AfricasTalkingMessage::create()
            ->content("Your verification code is {$this->code}. It expires in 10 minutes.");
    }
}

Route the destination on the notifiable model:

public function routeNotificationForAfricasTalking(): string
{
    return $this->phone; // stored as E.164: +254712345678
}
$user->notify(new SendOtp('482913'));

When that SMS lands in sandbox, integration is done. Everything after that is product, compliance, and operational hygiene.


Implementation Patterns

Queue the notification from the first commit. The channel makes a synchronous HTTP call to a third party inside your request cycle. Even for OTP — where perceived latency matters — the queue hop is sub-100ms on a warm worker and insulates your checkout/login endpoint from AT latency spikes and throttling.

class SendOtp extends Notification implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [10, 30, 60];
    public string $queue = 'sms';
}

Give SMS its own queue so a slow AT API never starves your mail or default queue.

Use Notification::route() for phone-only recipients. Pre-signup verification, guest checkout, phone-based password reset — no user model, no Notifiable trait required:

Notification::route('africastalking', '+254712345678')
    ->notify(new SendOtp('482913'));

The string key is the channel's route name, not the class. Confirm it in the channel source before you rely on it.

Decide the channel inside via(), never in business logic. This is the abstraction you should actually invest in — the AT adapter is disposable, the notification layer is not:

public function via(object $notifiable): array
{
    return $notifiable->wantsSmsAlerts()
        ? ['mail', AfricasTalkingChannel::class]
        : ['mail'];
}

Opt-out, quiet hours, country eligibility, and per-plan tiers all belong here. Sprinkling if ($user->plan === 'pro') around controllers is how notification systems rot.

Put a ->from() override on a base notification. Multi-brand or per-country sender IDs are repetitive if pasted into every toAfricasTalking(). A thin base class pays for itself the second time:

abstract class SmsNotification extends Notification implements ShouldQueue
{
    protected function message(string $body, ?string $from = null): AfricasTalkingMessage
    {
        return AfricasTalkingMessage::create()
            ->content($body)
            ->from($from ?? config('services.africastalking.from'));
    }
}

Test with fakes first, and know which fake you need. Notification::fake() asserts the notification contract and is what 95% of your tests should use:

Notification::fake();
$user->notify(new SendOtp('123456'));
Notification::assertSentTo($user, SendOtp::class);

If you need to assert the channel actually calls the SDK, note that Http::fake() only helps when the bundled SDK version routes through Laravel's HTTP client. Several AT SDK versions use Guzzle directly — in that case bind a GuzzleHttp\Handler\MockHandler into a stub handler stack in your test container. Check src/ before committing to an approach; this is a five-minute read that saves a flaky test suite.

Wrap the send behind a one-method interface early. Fifteen minutes now buys you a provider swap later, whether that's a country AT doesn't cover well or a 3am outage:

interface SmsNotifier
{
    public function send(string $to, string $body): void;
}

// implementation
public function send(string $to, string $body): void
{
    Notification::route('africastalking', $to)->notify(new GenericSms($body));
}

Provider migration becomes a one-line container binding plus a new implementation. Your domain code never learns which vendor sent the message.

Combine SMS with the database channel for OTP. SMS is the delivery mechanism; the database channel is your audit trail and the place your "resend code" UI reads from:

public function via(object $notifiable): array
{
    return ['database', AfricasTalkingChannel::class];
}

Log with correlation, and rethrow. AT's dashboard is the source of truth for delivery, but your logs need to tie sends to users and events:

try {
    $user->notify(new SendOtp($code));
    Log::info('sms.dispatched', ['user' => $user->id, 'type' => 'otp']);
} catch (\Throwable $e) {
    Log::error('sms.failed', ['user' => $user->id, 'error' => $e->getMessage()]);
    throw $e; // let the queue retry / let the job fail visibly
}

Bulk is not this package. The channel is one-recipient-per-notification by design. For campaigns, drip sequences, or a 10,000-recipient push, use the AT SDK's bulk endpoint from a dedicated job with batching, backpressure, and rate limiting — not 10,000 Notification::route() calls that each do their own HTTP round trip.


Gotchas and Tips

Config location drifts between majors. If env vars are set but the channel reports missing credentials, this is suspect number one, not your .env cache. Pin the version ("1.2.*", not ^1.2) for anything revenue-critical — a major bump can relocate config keys without an obvious failure. With a single maintainer and zero dependents, vendoring the two source files is a legitimate option for long-lived stability.

Phone numbers must be E.164. +254712345678, +2348012345678, +233.... AT does not normalize local formats such as 0712345678; you get a rejection or, worse, delivery to the wrong subscriber. Normalize at routeNotificationForAfricasTalking() — the last boundary before the channel sees the number — using propaganistas/laravel-phone or equivalent. Normalizing only at signup is insufficient; legacy rows drift.

Retries can duplicate SMS. The classic failure mode: AT receives and delivers the message but the HTTP response is lost to a timeout. Your queue retries and the user receives the OTP twice. Mitigations, in order of effectiveness:

  • Record (user, event, window) in a small idempotency table before dispatch; skip on duplicate.
  • Keep $tries at 2–3 with jittered $backoff, never 10.
  • Make OTP codes single-use with aggressive expiry so a duplicate is annoying, not a security hole.

Sender ID is a compliance lead time, not an engineering task. Alphanumeric sender IDs require per-country registration — Kenya, Nigeria, Ghana, and most others — usually with business documentation. Start day one; approval can take days. In sandbox from is ignored entirely. In production an unregistered sender ID may fail silently or fall back to a shared shortcode that looks like spam to recipients.

Exceptions are undifferentiated. The bundled SDK historically throws a single exception type for everything from bad credentials to insufficient balance to throttling. If you need to branch on failure class, you'll be parsing message strings. Practically: catch at the job level, classify coarsely by substring, and route two cases to alerting — auth failure and balance exhaustion. Balance exhaustion is the most common silent outage in SMS integrations; add a scheduled check against the AT balance endpoint.

Rate limits are per-account, not per-queue. Apply Laravel's RateLimited middleware (or Redis::throttle) sized to your tier's published throughput. Without it, a Monday-morning OTP burst throttles, and users see codes arriving minutes late — a worse experience than a clean failure they can retry.

Method name is toAfricasTalking(), camelCased and case-sensitive. toAfricastalking() or toAfricasTalking will throw a missing-method error at send time, not at boot. Copy the name out of the channel's source, not from memory.

Delivery receipts are out of scope. The channel sends and forgets. If you need per-message status for audit, customer-visible tracking, reconciliation, or regulatory reporting, implement an AT callback webhook in a small controller and store receipts in your own table. Treat it as a sidecar; do not try to extend the notification class into a tracking system.

Extension point: per-message from. AfricasTalkingMessage is fluent enough for ->from() per message, which covers multi-brand and per-country sender IDs without touching the channel:

return AfricasTalkingMessage::create()
    ->content($body)
    ->from($this->senderId ?? config('services.africastalking.from'));

Keep it in the base notification, not repeated across thirty classes.

Watch upstream and pin. A recent 2025 release is reassuring, but one maintainer and zero dependents means upgrade risk is entirely yours. Set a quarterly reminder to check the repo for Laravel compatibility notes — discover a Laravel 12 incompatibility in CI, not on a Friday deploy.

Debug order when an SMS doesn't arrive: (1) whitelisted in sandbox? (2) E.164 format? (3) config location matches the major? (4) is the notification queued and is the worker running? (5) does AT's dashboard show the send? That sequence resolves ~90% of "the package is broken" reports in under five minutes.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox