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 Laravel Package

laravel-notification-channels/clickatell

Send SMS notifications through Clickatell with this Laravel notification channel. Install via Composer, configure your API token, and deliver messages using a simple toClickatell method.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install, but read the resolver output.

composer require laravel-notification-channels/clickatell

The last tag is 0.1.5 (Sept 2021, Laravel 8-era) and it has zero dependents. Before you write a single line of application code, confirm it actually installs against your lockfile:

composer why-not laravel-notification-channels/clickatell

If you're on Laravel 10+, don't fight it with --ignore-platform-reqs in production. The driver is a thin wrapper — vendoring it or reimplementing the ~50 lines is the sane path.

2. Publish the config and treat it as authoritative.

php artisan vendor:publish --provider="NotificationChannels\Clickatell\ClickatellServiceProvider" --tag="config"

Open config/clickatell.php and read the key names it actually consumes. Don't copy env variable names from blog posts; wire .env to whatever the published file reads:

CLICKATELL_API_TOKEN=your-token
CLICKATELL_FROM=MyBrand

3. Where to look first. The package is small enough to read end-to-end in ten minutes, and you should — there is no meaningful external documentation:

  • src/ClickatellChannel.php — the send() method. This tells you how it resolves the recipient, what HTTP client it uses, and what it inspects in the response. Everything in Gotchas depends on this file.
  • src/ClickatellMessage.php — the fluent builder (create(), from(), to(), content()). This is your entire authoring surface.
  • config/clickatell.php — the only injection point the package exposes.

4. Teach your notifiable how to be reached.

public function routeNotificationForClickatell(): ?string
{
    return $this->phone; // store E.164: +27820000000
}

5. Write the notification. Standard Laravel; only toClickatell() is package-specific.

use Illuminate\Notifications\Notification;
use NotificationChannels\Clickatell\ClickatellChannel;
use NotificationChannels\Clickatell\ClickatellMessage;

class OrderShipped extends Notification
{
    public function __construct(public Order $order) {}

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

    public function toClickatell(object $notifiable): ClickatellMessage
    {
        return ClickatellMessage::create(
            "Order #{$this->order->id} has shipped."
        );
    }
}

6. Smoke test without touching a model. Prove credentials and sender ID before building anything else:

Notification::route('clickatell', '+27820000000')
    ->notify(new OrderShipped($order));

Pick exactly one production flow to start: OTP/2FA or password reset. High value per message, tiny blast radius, and failure is loud rather than a silent marketing miss.

Implementation Patterns

Gate the channel in via(). via() runs before the channel does, so it's your cheap guard rail — skip SMS for users with no number or no consent instead of catching an exception downstream:

public function via(object $notifiable): array
{
    return $notifiable->phone && $notifiable->sms_opt_in
        ? [ClickatellChannel::class]
        : ['mail'];
}

Use shouldSend() for consent and quiet hours. It's evaluated before dispatch and composes cleanly with queueing:

public function shouldSend(object $notifiable, string $channel): bool
{
    return ! $notifiable->inQuietHours();
}

Queue every SMS, always. The channel rides Laravel's notification pipeline, so retries are free — but only if you opt in. Add a tries count, a backoff, and a dedicated queue so a stuck SMS worker can't starve mail:

class OrderShipped extends Notification implements ShouldQueue
{
    public int $tries = 3;
    public function backoff(): array { return [10, 60, 300]; }
    public function viaQueues(): array { return [ClickatellChannel::class => 'sms']; }
}

For OTPs specifically, pair this with ->middleware(new RateLimited('sms')) so a retry storm doesn't become a Clickatell bill.

Abstract the body into a base class. Every project ends up with seven near-identical notifications. One abstract parent holds the scaffolding; each concrete class implements only smsBody():

abstract class SmsNotification extends Notification implements ShouldQueue
{
    abstract protected function smsBody(object $notifiable): string;

    public function via(object $notifiable): array
    {
        return $notifiable->phone ? [ClickatellChannel::class] : [];
    }

    public function toClickatell(object $notifiable): ClickatellMessage
    {
        return ClickatellMessage::create(
            Str::limit($this->smsBody($notifiable), 160, '')
        );
    }
}

Keep the vendor swappable behind your own seam. This is a thin driver that formats one HTTP request. Treat it as an implementation detail:

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

class ClickatellSmsSender implements SmsSender { /* delegates to notify() */ }

The highest-leverage hour you'll spend on this package. It turns the eventual replacement — or a second vendor for a second region — into a one-class change rather than a refactor.

Override the recipient sparingly. When a message must go somewhere other than the notifiable's own number (on-call rota, ops escalation), set it on the message:

return ClickatellMessage::create($body)->to('+27820000000');

Prefer Notification::route() for genuinely ad-hoc recipients; it keeps the notification class honest.

Test at both layers. Notification::fake() proves your call sites selected the right channel — it does not exercise the driver:

Notification::fake();
$user->notify(new OrderShipped($order));
Notification::assertSentTo($user, OrderShipped::class, function ($n, $channels) {
    return in_array(ClickatellChannel::class, $channels);
});

For the driver layer, Http::fake() and assert the outgoing request body — that's the only way to catch phone-format and sender-ID regressions:

Http::fake(['*' => Http::response(['messages' => [['accepted' => true]]], 202)]);

Log outbound sends. Listen for NotificationSent / NotificationFailed and persist recipient, channel, body hash, and vendor response ID. When someone says "I never got the code," this table is the difference between a five-minute answer and a two-hour investigation.

Gotchas and Tips

A 2xx is not proof of delivery. Clickatell's messaging API returns 202 Accepted with a per-message accepted flag — a batch can come back 202 while every message inside was rejected (unapproved sender ID, blocked route, invalid destination). Check what send() actually inspects; if it only asserts the HTTP status, rejected sends will look like successes in your logs. Validate the response body yourself.

There are no delivery receipts here. The package is outbound-only — no status webhooks, no callback parsing. "Did the OTP arrive?" is unanswered by this dependency. If you need fallback logic (SMS → voice → email), budget for building it against Clickatell's status API separately. Decide this before SMS lands on the auth path.

Compatibility is yours to prove, not the release notes'. Pre-1.0, single maintainer, released for Laravel 8, zero dependents — there's no community-tested matrix to lean on. Verify against your actual lockfile and pin the exact version. Never --ignore-platform-reqs in production to force the install.

Config caching can silently blank the credential. If the service provider reads env() directly instead of through config(), php artisan config:cache in production will hand the client a null token. Test on staging with the config cached:

php artisan config:cache && php artisan config:clear

Normalise phone numbers before they reach the channel. The channel passes your string straight through. Enforce E.164 (+, country code, digits — no spaces or dashes) in a model cast and reject at write time, not at send time where the failure lands in a queue and disappears.

Sender ID registration is per-country and outside this package. Alphanumeric sender IDs are fine in some markets and rejected or overwritten in others (notably the US). A failing send that looks like a code bug is often a provisioning issue in your Clickatell account. Confirm from is approved for every destination country in your user base before launch.

Encoding doubles your bill silently. GSM-7 gives 160 characters per segment; a single emoji or curly quote forces UCS-2 at 70. Clickatell bills per segment. Keep bodies ASCII, strip smart punctuation, prefer a short link over a long URL, and sanitise user-supplied names before interpolating them into templates.

A missing route throws — it doesn't no-op. If routeNotificationForClickatell() returns null and the message has no explicit to(), expect an exception inside a queued job. Gate in via()/shouldSend(), and make the phone column non-nullable for users who opted into SMS.

Consent, STOP handling, and suppression lists are yours. The package sends to whoever you route to it. Inbound STOP keywords need a Clickatell webhook plus a suppression table your via() reads. That's a launch prerequisite, not a follow-up ticket — it's the one item here with regulatory consequences.

Throughput is a worker-and-plan question, not a library question. Concurrency is set by your queue workers and your Clickatell plan's rate limit. Simulate a realistic burst (broadcast to 5k users) in staging to surface 429s before production does.

Give the dependency an owner and a review date. Ten stars, one maintainer, last release 2021. Pin it, log it, keep the SmsSender seam — so the eventual swap is a scheduled afternoon instead of an incident.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource