laravel-notification-channels/sms77
Laravel notification channel for sms77 that sends SMS messages through the sms77 API using Laravel’s native notification system. Configure your API key and sender, then deliver transactional, alert, or verification texts via a simple channel class.
Install the channel and publish its config:
composer require laravel-notification-channels/sms77
php artisan vendor:publish --provider="NotificationChannels\Sms77\Sms77ServiceProvider" --tag=config
As of v1.0.7 the package's dependency constraints were widened for Laravel 13 (plus a PHPUnit bump in CI). That release is purely compatibility/maintenance — no channel API changes, no new message builder methods, no deprecations. So the install and usage documented below is identical whether you are on v1.0.6 or v1.0.7; v1.0.7 is simply the version you need if you're running Laravel 13.
Set your credentials in .env (the key name is specific to this package):
SMS77_API_KEY=your-api-key
The first use case is almost always a transactional alert: an order confirmation, OTP, or ops alert to a single number. Wire it into an existing or new Notification class:
use NotificationChannels\Sms77\Sms77Channel;
use NotificationChannels\Sms77\Sms77Message;
class OrderShipped extends Notification
{
public function via($notifiable): array
{
return [Sms77Channel::class];
}
public function toSms77($notifiable): Sms77Message
{
return (new Sms77Message())
->to($notifiable->phone)
->text("Your order #{$this->order->id} has shipped.");
}
}
The two things that trip people up on first run:
Notifiable model must implement routeNotificationForSms77() (or you must call ->to() on the message explicitly). There's no default fallback.+491701234567), not a national format with a leading 0. This is the most common reason for a 400 from the API on your first attempt.First debugging step: php artisan tinker, instantiate Sms77Message, and check that ->to() and ->text() serialize the way you expect before debugging the notification pipeline.
Channel-per-notification. Keep SMS out of mail()-style "one notification does everything" classes for anything user-facing. Use via() to conditionally include Sms77Channel based on user preference, verified phone, or opt-in flags:
public function via($notifiable): array
{
return array_filter([
Sms77Channel::class => $notifiable->sms_opted_in,
'mail' => true,
]);
}
On-demand notifications for one-off sends. The idiomatic path for alerts to a phone number not attached to a model:
Notification::route('sms77', '+491701234567')
->notify(new ServerDownAlert($server));
This is the pattern to prefer in scheduled commands and health checks, since it keeps the model layer clean.
Builder-style messages. Sms77Message mirrors the sms77 API surface — from(), flash(), label(), delay(), foreignId(), ttl(), unicode(). Use them deliberately:
return (new Sms77Message())
->to($notifiable->phone)
->from('MyShop')
->label('orders')
->foreignId($this->order->id)
->unicode()
->text($renderedMessage);
label() and foreignId() are what make the sms77 dashboard usable later — without them, all traffic shows up anonymous. Set them from day one.
Queue deliberately. SMS is an external HTTP call inside notify(). Implement ShouldQueue on notifications that also send mail, or dispatch with a delay. If you don't, a slow sms77 response will slow the request that triggered it.
Fallback ordering. A common, low-effort pattern is via() returning [Sms77Channel::class, 'mail'] for OTP flows — but only when you've confirmed the API path is under a reasonable timeout; otherwise email becomes the bottleneck instead of the fallback.
Testability. Notification::fake() intercepts notification dispatch but not the channel's HTTP call, so it's fine for "was the right notification sent" assertions. For channel-level tests, prefer mocking the underlying SMS client (or Http::fake()) and assert the payload shape rather than asserting on Sms77Message directly — the message is a thin DTO and over-testing it gives you brittle tests.
Phone formatting is the #1 failure mode. sms77 rejects national formats. Normalize at the model boundary (a phone_e164 accessor or a Laravel cast/observer), not inside the notification, so every channel benefits.
routeNotificationForSms77() is mandatory unless you call ->to(). If you see NotificationChannels\Sms77\Exceptions\CouldNotSendNotification with a routing message, that's the cause. The channel does not look at $notifiable->phone automatically.
Config caching. php artisan config:cache in deploy pipelines is standard, but every environment variable must be present at cache build time. If SMS77_API_KEY is injected at runtime (e.g., a secrets manager sidecar), the cached config will hold null. Verify with php artisan config:show sms77 after deploy.
Unicode and segment counting. GSM-7 messages are 160 chars per segment; Unicode (emoji, non-Latin) drops to 70. The unicode() flag forces the API to treat the message as UCS-2, which is correct but triples cost per segment. If you're rendering user-supplied content into SMS, sanitize or transliterate — otherwise a single emoji in a template blows up your bill.
Sender ID constraints. from() must reference an approved sender in your sms77 account. Unapproved values are silently downgraded or rejected depending on country rules. This varies by destination country, which makes it hard to spot in staging.
Delay and TTL are API-side features, not queue features. ->delay() schedules delivery on sms77's servers; ->ttl() expires the message. Neither interacts with Laravel's queue. If you want retries with backoff, do it on the Laravel side and accept that a delay double-schedules if you also pass ->delay().
Queue + retries means duplicate SMS. SMS has no idempotency key exposed by this channel. If a notification job retries after a timeout, you will send the message twice. Either use ->foreignId() as your own dedupe key on the receiving side, or set $tries = 1 on SMS-bearing jobs where duplicates are unacceptable.
Package maturity. This is a very small package (single-digit stars, a handful of dependents) but it is maintained: v1.0.7 lands Laravel 13 support, so you are not adopting an abandoned adapter. Still, treat it as a thin wrapper rather than infrastructure, and pin the constraint consciously (^1.0 is fine now; if you're on Laravel 13 you need ^1.0.7). The pragmatic move on a production project is to wrap it in a single internal class or trait in your app:
trait SendsSms
{
protected function sms(string $to, string $body): Sms77Message
{
return (new Sms77Message())->to($to)->from(config('sms.sender'))->text($body);
}
}
If the package stalls or the bundled SDK lags the sms77 API, you swap it out in one place instead of every notification class. The channel's public API is small enough that a first-party replacement is a day's work.
Upgrading to v1.0.7. Nothing to do beyond composer update laravel-notification-channels/sms77. There are no code changes, no config file changes, no deprecations — the diff is CI/dependency metadata and StyleCI formatting. That also means v1.0.7 gives you no new behaviour to exploit; if you were waiting on a feature (e.g. new builder methods, idempotency, better error payloads), it is still not here.
Error handling. CouldNotSendNotification is thrown on non-2xx responses. Catch it at the notification boundary if you have a fallback channel, or let it fail the queued job if you'd rather surface the problem. Do not swallow the exception broadly — a wrong API key silently producing "sent" log lines is a common false confidence.
Debugging in staging. Use a single test number and the sms77 dashboard's delivery log (searchable by label/foreignId) rather than guessing. If the dashboard shows delivery but the user says otherwise, it's a carrier-side issue and not something you can fix in the channel — log the sms77 message ID and open a support ticket with it.
Look first to: config/sms77.php (for env key name quirks), Sms77Message (to see which builder methods actually exist — the list is shorter than the raw API), composer.json (to confirm the release line you're on supports your Laravel version — relevant again for Laravel 13), and the channel class itself (to confirm it reads routeNotificationForSms77 and not routeNotificationForSms).
How can I help you explore Laravel packages today?