laravel-notification-channels/turbosms
A Laravel notification channel for sending SMS messages through TurboSMS. Integrate TurboSMS with Laravel notifications and deliver SMS using a simple, familiar channel API.
Install and wire up
composer require laravel-notification-channels/turbosms
php artisan vendor:publish --provider="NotificationChannels\TurboSms\TurboSmsServiceProvider"
Pin at least 1.1.1 if you're on a modern Laravel — that release (PRs #10 and #12) is what added Laravel 11 and Laravel 12 compatibility. Older tags declare tighter framework constraints and Composer will refuse or downgrade you on Laravel 11/12 skeletons. On 1.1.1 the install constraint can be:
"laravel-notification-channels/turbosms": "^1.1.1"
The 1.1.0 → 1.1.1 diff is framework-compatibility only: no channel API, config key, or message-method changes, so nothing in the usage below shifts. The service provider is auto-discovered on Laravel 5.5+; on older skeletons register NotificationChannels\TurboSms\TurboSmsServiceProvider in config/app.php.
Publishing creates config/turbosms.php. Populate it from .env — typically a login/password pair for the TurboSMS account plus a sender name that has already been approved in the TurboSMS dashboard:
TURBOSMS_LOGIN=your-login
TURBOSMS_PASSWORD=your-password
TURBOSMS_SENDER=MyApp
Where to look first
'turbosms' vs TurboSmsChannel::class) and message factory name.config/turbosms.php — confirms the env keys and any base URL override.src/TurboSmsMessage.php — the fluent surface you'll actually call (content(), sender(), to() in most channels of this family).src/TurboSmsChannel.php — the send() method tells you how responses and errors are handled, which is what you'll debug against.First use case: transactional OTP or order status
use NotificationChannels\TurboSms\TurboSmsChannel;
use NotificationChannels\TurboSms\TurboSmsMessage;
class OrderShipped extends Notification
{
public function __construct(public Order $order) {}
public function via($notifiable): array
{
return [TurboSmsChannel::class];
}
public function toTurboSms($notifiable): TurboSmsMessage
{
return TurboSmsMessage::create("Order #{$this->order->id} shipped");
}
}
Give the notifiable a routing method so the channel can find a number:
public function routeNotificationForTurboSms($notification): ?string
{
return $this->phone; // normalize to 380XXXXXXXXX before returning
}
That's the whole loop: one via() entry, one toTurboSms() method, one routing method.
On-demand / anonymous recipients. For users without a phone column, or for pre-signup OTP, bypass the notifiable entirely:
Notification::route('turbosms', '380501234567')
->notify(new PhoneVerification($code));
If the key isn't 'turbosms', substitute TurboSmsChannel::class — the README is authoritative here, and it differs across channels in this org.
Fan-out across channels. SMS is usually the fallback, not the primary. Return ['mail', TurboSmsChannel::class] and keep the message text short and channel-agnostic; SMS silently truncates anything over the segment limit.
Queue it. SMS is a network call inside the request cycle. Implement ShouldQueue on the notification and let a worker absorb latency and retries. If you need strict once-only delivery, add ShouldBeUnique keyed on the OTP/order ID rather than relying on the channel.
Throttle deliberately. OTP endpoints are abuse targets. Combine a RateLimiter in the controller with queued dispatch so a burst doesn't burn your TurboSMS balance:
RateLimiter::attempt("otp:{$user->id}", 3, fn () => $user->notify(new SendOtp($code)), 300);
Test with the notification fakes. The channel doesn't need to hit the network in CI:
Notification::fake();
$user->notify(new OrderShipped($order));
Notification::assertSentTo($user, OrderShipped::class, function ($n, $channels) {
return in_array(TurboSmsChannel::class, $channels, true);
});
For content assertions, instantiate toTurboSms($user) directly and assert the message body — cheaper than faking the transport.
Observe delivery. Listen to Laravel's Illuminate\Notifications\Events\NotificationSent and NotificationFailed. Log failures with the notifiable ID and the message text (redact OTPs), so you can reconcile against the TurboSMS dashboard when a user says "I never got the code."
Wrap the transport for non-notification sends. If you need to send SMS outside the notification system (admin alerts, batch jobs), put a thin SmsService around the channel or message class rather than scattering Notification::route() calls. It gives you one place to swap providers later.
Phone format is the #1 failure. TurboSMS expects digits in international form without + or separators (380XXXXXXXXX). Normalize inside routeNotificationForTurboSms() — not at the call site — so every send path inherits the fix. Strip parentheses, dashes, and leading 0 from local numbers.
Sender names must be pre-approved. TURBOSMS_SENDER has to match a signature registered in the TurboSMS account. An unapproved sender generally surfaces as an API-level rejection, not a transport exception, so it looks like a silent failure.
Check the response body, not just the HTTP status. TurboSMS's API commonly returns 200 OK with an error payload. Read TurboSmsChannel::send() to see whether the package throws on that; if it doesn't, your try/catch around notify() will never fire and failed sends will vanish. Wrap the channel or add your own assertion on the response in a subclass if this matters.
Encoding changes your bill. Cyrillic text is UCS-2: ~70 characters per segment instead of 160. A 140-character Ukrainian message is two billable SMS. Keep messages under 70 characters where possible and test with real Cyrillic text, not lorem ipsum.
Config caching bites. After changing any TURBOSMS_* env var, run php artisan config:clear. In containerized deploys the config is baked at build time — make sure credentials are injected at runtime, not compiled into the image.
Credentials are per-environment. Use a separate TurboSMS sender or account for staging, and point staging at a test number. There's no sandbox mode in the channel itself.
Version compatibility is the one thing that just changed. 1.1.1 is a compatibility release: Laravel 11 (#10) and Laravel 12 (#12). Practical consequences:
^1.1.1. An unpinned composer require will land here anyway, but a stale lockfile copied from a Laravel 10 project won't.TurboSmsChannel / TurboSmsMessage surface and config keys are untouched, so existing code needs no edits. Treat this as a "bump and run the suite" upgrade, not a migration.Low adoption is a real risk. Very few dependents and low star count means few people have exercised edge cases. Mitigations:
"^1.1.1" or an exact tag) rather than floating.TurboSmsChannel::send() end-to-end before going live; it's short.SmsSender) between your app and the channel so a provider swap is a single binding change.Timeouts and retries. If the message class or config exposes a timeout, set it low (3–5s) and let the queue handle retries. Without a timeout, a stalled connection hangs a worker. Cap $tries and $backoff on the notification to avoid double-sending OTPs on transient errors.
toTurboSms() returning null. Several channels in this family treat a null message as "skip this channel"; others throw. Verify in the source before you rely on conditional sending — if it throws, guard with an early return in via() instead.
Message length truncation is silent. The API cuts overlong bodies at a segment boundary. Assert length in a unit test on toTurboSms($user)->content if the text is dynamic.
How can I help you explore Laravel packages today?