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

Clickatell Notifier Laravel Package

symfony/clickatell-notifier

Symfony Notifier integration for Clickatell SMS. Send text messages through Clickatell's API using a simple DSN: clickatell://ACCESS_TOKEN@default?from=FROM, where ACCESS_TOKEN is your Clickatell auth token and FROM is the sender.

View on GitHub
Deep Wiki
Context7

Getting Started

This is not a Laravel package — it is a Symfony Notifier bridge. There is no auto-discovered service provider, no config/notifier.php, and no facade. You get exactly one class that implements Symfony\Component\Notifier\Transport\TransportInterface plus a factory. Everything else is on you.

Install

composer require symfony/notifier symfony/clickatell-notifier symfony/http-client

Look at first: the README DSN line and then Symfony\Component\Notifier\Transport::fromDsn(), Symfony\Component\Notifier\Message\SmsMessage, and Symfony\Component\Notifier\Recipient\SmsRecipient. Those four are the entire surface you'll touch day-to-day.

Wire it once in AppServiceProvider:

// config/services.php
'clickatell' => ['dsn' => env('CLICKATELL_DSN')],

// AppServiceProvider::register()
$this->app->singleton(NotifierInterface::class, function () {
    return new Notifier([
        Transport::fromDsn(config('services.clickatell.dsn')),
    ]);
});
CLICKATELL_DSN=clickatell://ACCESS_TOKEN@default?from=MyBrand

First use case — a transactional OTP, sent synchronously from tinker to prove credentials and sender ID work before you build anything around it:

$sent = app(NotifierInterface::class)->send(
    new SmsMessage('Your code is 123456', new SmsRecipient('+27821234567'))
);
dump($sent->getMessageId()); // Clickatell message id — persist it

Once that returns a message ID, wrap it in a queued Laravel notification (below) rather than calling it inline.

Implementation Patterns

1. Custom notification channel (the canonical Laravel pattern). Keep Symfony types out of your notification classes by adapting at the channel boundary:

class ClickatellChannel
{
    public function __construct(private NotifierInterface $notifier) {}

    public function send(object $notifiable, Notification $notification): void
    {
        $sms = $notification->toClickatell($notifiable);

        $message = new SmsMessage(
            $sms->content,
            new SmsRecipient($notifiable->routeNotificationFor('clickatell', $notification))
        );

        $sent = $this->notifier->send($message);

        // Persist for support tickets, delivery webhooks, audits
        $notifiable->smsLogs()->create([
            'message_id' => $sent->getMessageId(),
            'body'       => $sms->content,
        ]);
    }
}

Then via() returns ['clickatell'] and Laravel resolves the channel from the container.

2. Always queue. Put ShouldQueue on the notification and give it $tries/backoff. SMS is a network call to a third party; you do not want Clickatell latency in your request/response cycle. Laravel's queue retry replaces Symfony Messenger's retry — you are not running Messenger, so the retry semantics are whatever you configure in the notification.

3. Domain service wrapper. Bind a thin SmsSender/ClickatellClient service that owns the NotifierInterface and exposes send(string $to, string $body, array $context = []). Every call site depends on your interface, not Symfony's. When Clickatell's pricing or reliability becomes a problem, you swap the implementation and nothing else changes.

4. One transport per sender. The from value lives in the DSN, not in SmsMessage. If you need per-brand/per-tenant sender IDs, build a small factory keyed by tenant that constructs Transport::fromDsn() per DSN, and cache the resulting Notifier instances:

class SmsNotifierFactory
{
    private array $notifiers = [];

    public function for(string $senderId): NotifierInterface
    {
        return $this->notifiers[$senderId] ??= new Notifier([
            Transport::fromDsn("clickatell://{$this->token}@default?from={$senderId}"),
        ]);
    }
}

5. Idempotency at the domain layer. Laravel queue retries can fire the same notification twice. For OTPs and payment alerts, store a dedupe key (user + template + window) before dispatching and short-circuit inside the channel. Clickatell will happily send duplicates and bill you for both.

6. Testing. Swap the HttpClientInterface for MockHttpClient inside a transport you build in the test, or bind a fake SmsSender in the container and assert on your own interface. The latter is far less brittle than asserting on Clickatell's JSON payload:

$this->mock(SmsSender::class)
    ->shouldReceive('send')
    ->once()
    ->with('+27821234567', Mockery::on(fn ($b) => str_contains($b, '123456')));

7. Local / staging. Use Symfony's null:// DSN via CLICKATELL_DSN=null:// in .env.testing and .env.local. That gives you a real SentMessage with a fake ID and zero credits burned.

Gotchas and Tips

  • Tiny surface, tiny community. 0 dependents and single-digit stars means nobody else is exercising the edge cases for you. Treat this as ~200 lines of glue over Clickatell's REST API, not a maintained integration. Read Clickatell's own API docs — the bridge documents nothing about status codes, delivery receipts, or webhooks.
  • The DSN host must be literally default. clickatell://[email protected]?from=X will fail; the factory expects the scheme + default host and reads from from the query string.
  • Recipient must be a real E.164 number. The transport does almost no normalization — a missing + or a local format will get rejected by Clickatell, not by PHP. Validate at the edge with propaganistas/laravel-phone or similar before it reaches the bridge.
  • to is sent as an array. There was a real bug — "send the recipient phone number as an array" — fixed in 7.1.2 / 7.0.9 / 6.4.9. If you pin an older patch and see Clickatell 400s on a perfectly valid number, that's why. Minimum-safe versions: >=6.4.9, >=7.1.2, >=8.x.
  • PHP version matrix is a trap. v6.4 → PHP 8.1+, v7.x → PHP 8.2+, v8.x → PHP 8.4+. Laravel 11/12 on PHP 8.3 means you want ^7.1. Don't blanket require symfony/clickatell-notifier:^8.0.
  • Exceptions are Symfony's, not Laravel's. You'll catch Symfony\Component\Notifier\Exception\TransportExceptionInterface (and UnsupportedMessageTypeException). Your App\Exceptions\Handler and retry() logic need to know about them explicitly if you care about retrying network errors but not validation errors.
  • Symfony's event dispatcher doesn't reach Laravel's. MessageEvent / FailedMessageEvent are dispatched on a PSR-14 dispatcher you'd have to build and bridge yourself. Simpler: decorate NotifierInterface with a logging decorator that fires your own Laravel events.
  • No meaningful per-message MessageOptions. from and auth come from the DSN only. Don't design a per-message override feature on top of this without the factory pattern above.
  • Async is your job. Symfony Notifier's Messenger-based async transport is not available here. Queued Laravel notifications are the answer; just remember that Laravel's retry_after + a slow Clickatell outage can pile up jobs — set a reasonable $tries (3–5) and backoff([10, 60, 300]).
  • Extension points if you outgrow it: implement TransportInterface yourself against Clickatell's REST API directly, or keep the bridge and decorate NotifierInterface. There is nothing on the Laravel side to extend — no events, no macros, no config schema.
  • Watch the symfony/notifier pull. It suggests/adds a lot of unrelated transports. Run composer why symfony/notifier after install and consider --no-dev trimming; it can bloat containers meaningfully compared to a 40-line Guzzle call.
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib