mr-rijal/laravel-sms
| Install | |
|---|---|
composer require mr-rijal/laravel-sms |
|
| Latest Version: | 1.0.2 |
| PHP: | ^8.2 |
| Laravel: | 11 · 12 · 13 |
| License: | MIT |
| Last Updated: | Nov 2, 2026 |
| Links: | GitHub · Packagist |
Laravel SMS
One fluent API for sending SMS from Laravel through 20 providers, with failover, queueing, notifications, delivery webhooks and first-class testing.
$result = Sms::to('+15551234567')->message('Your code is 4821')->sendNow();
$result?->firstId(); // the provider's message ID (null if an SmsSending listener cancelled the send)
Highlights
- 20 providers, from global platforms to regional gateways, plus
log,fakeandfailoverdrivers. - Fast: built on Laravel's HTTP client. Providers without a bulk endpoint get concurrent per-recipient requests (
Http::pool), and bulk providers get one request for everyone. - Reliable: every send returns an
SmsResultwith message IDs. Partial failures say exactly which recipients failed, andfailoverretries only those recipients on the next provider. - Secure: webhooks fail closed with signature checks, queued jobs are encrypted, and the package never puts credentials in exception messages, URLs or cache keys.
- Testable:
Sms::fake()withassertSent(),assertSentTo()andassertQueued(), and every driver works withHttp::fake(). - Laravel-native: Octane- and queue-safe (each send builds a fresh message), notification channel, events, macros,
Sms::extend(),php artisan sms:test, andphp artisan aboutintegration. - Typed: PHPStan at max level.
Requirements
| Laravel | PHP |
|---|---|
| 13.x | 8.3 – 8.5 |
| 12.x | 8.2 – 8.5 |
| 11.x | 8.2 – 8.5 |
Laravel 11 is past its security-support window. Use Laravel 12 or 13 in production.
Installation
composer require mr-rijal/laravel-sms
php artisan vendor:publish --tag=sms-config
Pick a provider and add its credentials to .env:
SMS_PROVIDER=twilio
TWILIO_SID=
TWILIO_TOKEN=
TWILIO_FROM=+15550000000
Check the setup with a real message:
php artisan sms:test +15551234567
Providers
| Provider | Key | Coverage | Environment variables |
|---|---|---|---|
| Twilio | twilio |
Global | TWILIO_SID, TWILIO_TOKEN, TWILIO_FROM or TWILIO_MESSAGING_SERVICE_SID |
| Vonage | vonage |
Global | VONAGE_KEY, VONAGE_SECRET, VONAGE_FROM |
| Sinch | sinch |
Global | SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, SINCH_FROM, SINCH_REGION |
| Bird (MessageBird) | bird |
Global | BIRD_API_KEY, BIRD_FROM, BIRD_REGION, BIRD_CATEGORY |
| Plivo | plivo |
Global | PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, PLIVO_FROM |
| Telnyx | telnyx |
Global | TELNYX_API_KEY, TELNYX_FROM or TELNYX_MESSAGING_PROFILE_ID |
| Infobip | infobip |
Global | INFOBIP_API_KEY, INFOBIP_SENDER, INFOBIP_BASE_URL |
| ClickSend | clicksend |
Global | CLICKSEND_USERNAME, CLICKSEND_API_KEY, CLICKSEND_FROM |
| Clickatell | clickatell |
Global | CLICKATELL_API_KEY, CLICKATELL_FROM |
| Brevo | brevo |
Global | BREVO_API_KEY, BREVO_SMS_SENDER, BREVO_SMS_TYPE |
| Bandwidth | bandwidth |
US / Canada | BANDWIDTH_ACCOUNT_ID, BANDWIDTH_APPLICATION_ID, BANDWIDTH_CLIENT_ID, BANDWIDTH_CLIENT_SECRET, BANDWIDTH_FROM |
| SendPulse | sendpulse |
Global | SENDPULSE_CLIENT_ID, SENDPULSE_CLIENT_SECRET, SENDPULSE_SENDER |
| Amazon SNS | aws_sns |
Global | AWS_DEFAULT_REGION (+ keys, or an IAM role) — needs composer require aws/aws-sdk-php |
| WhatsApp Cloud API | whatsapp |
Global | WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_ACCESS_TOKEN |
| MSG91 | msg91 |
India | MSG91_AUTHKEY, MSG91_SENDER |
| Sparrow SMS | sparrow |
Nepal | SPARROW_TOKEN, SPARROW_FROM |
| Termii | termii |
Africa | TERMII_API_KEY, TERMII_SENDER_ID, TERMII_CHANNEL |
| Sendchamp | sendchamp |
Africa | SENDCHAMP_API_KEY, SENDCHAMP_SENDER_NAME, SENDCHAMP_ROUTE |
| Africa's Talking | africastalking |
Africa | AFRICASTALKING_API_KEY, AFRICASTALKING_USERNAME |
| BulkSMS | bulksms |
Global | BULKSMS_TOKEN_ID, BULKSMS_TOKEN_SECRET |
| Failover | failover |
— | SMS_FAILOVER_DRIVERS=twilio,vonage |
| Log (development) | log |
— | SMS_LOG_CHANNEL (optional) |
| Fake (tests) | fake |
— | — |
Templates (->template($id, $vars)) are supported by MSG91 (DLT flows), WhatsApp (approved templates) and Brevo (SMS templates). The other providers need message text.
Sending
use MrRijal\LaravelSms\Facades\Sms;
// Default provider
Sms::to('+15551234567')->message('Hello')->send();
// Several recipients, a specific provider and sender
Sms::provider('sinch')
->to(['+15551234567', '+15557654321'])
->from('ACME')
->message('Maintenance tonight at 10pm')
->send();
// Templates
Sms::to('+919812345678')->template('6512a...', ['otp' => 4821])->send();
// Build a message once and send it anywhere
$message = SmsMessage::create('Hello')->to('+15551234567');
Sms::sendNow($message);
Numbers may contain spaces, dashes, dots and parentheses; they are normalized before sending.
Results and errors
sendNow() returns an SmsResult:
$result = Sms::to($numbers)->message('Hi')->sendNow();
$result->provider; // "twilio"
$result->recipients(); // accepted numbers
$result->ids(); // provider message IDs
foreach ($result->messages as $sent) {
$sent->to; $sent->id; $sent->status;
}
When a provider rejects a recipient, a DeliveryException is thrown. It tells you exactly what happened:
use MrRijal\LaravelSms\Exceptions\DeliveryException;
try {
Sms::to($numbers)->message('Hi')->sendNow();
} catch (DeliveryException $e) {
$e->failedRecipients; // numbers that were not accepted
$e->partial?->recipients(); // numbers that were accepted before the failure
}
Every package exception implements MrRijal\LaravelSms\Exceptions\SmsException. Invalid input throws InvalidMessageException, and bad configuration throws InvalidConfigurationException.
Queueing
Sms::to($numbers)->message('Hi')->sendLater();
Sms::to($numbers)->message('Hi')->sendLaterAt(now()->addMinutes(10));
Set SMS_QUEUE=true to make send() queue by default (then send() returns null). Queued messages are split into one job per recipient, so a provider error for one recipient never makes the retry re-send to the others. Jobs are encrypted with your APP_KEY.
SMS delivery is still at-least-once: if a provider accepts a message but the response is lost (a timeout, a dropped connection), the retry, or failover, can send it to that recipient again.
SMS_QUEUE_CONNECTION=redis
SMS_QUEUE_NAME=sms
SMS_QUEUE_TRIES=3
SMS_QUEUE_TIMEOUT=120 # keep your queue's retry_after above this
Failover and load balancing
SMS_PROVIDER=failover
SMS_FAILOVER_DRIVERS=twilio,vonage,sinch
Failover tries each provider in order. If Twilio accepts 98 of 100 recipients, only the 2 failed numbers go to Vonage.
For simple load balancing, set the provider to random and list the pool in sms.random_drivers.
Speed
Providers with a bulk endpoint (Sinch, Plivo, Infobip, ClickSend, Clickatell, Sparrow, Sendchamp, Africa's Talking, BulkSMS, SendPulse, MSG91 templates) receive one request for every recipient. The rest send per-recipient requests concurrently:
SMS_HTTP_CONCURRENCY=10
SMS_HTTP_TIMEOUT=30
SMS_HTTP_CONNECT_TIMEOUT=10
OAuth providers (Bandwidth, SendPulse) cache their access tokens until shortly before they expire.
Notifications
Return an SmsMessage (or a plain string) from toSms():
use MrRijal\LaravelSms\SmsMessage;
class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
public function via(object $notifiable): array
{
return ['sms'];
}
public function toSms(object $notifiable): SmsMessage
{
return SmsMessage::create("Order #{$this->order->id} has shipped.");
}
}
Route notifications with routeNotificationForSms() on the notifiable, or on demand:
public function routeNotificationForSms(): string
{
return $this->phone;
}
Notification::route('sms', '+15551234567')->notify(new OrderShipped($order));
For quick one-offs, use the bundled SmsNotification:
$user->notify(new SmsNotification(message: 'Welcome!'));
$user->notify(new SmsNotification(templateId: 'OTP', variables: ['otp' => 4821], provider: 'msg91'));
The HasSms trait adds $user->sms('...') and $user->smsTemplate($id, $vars).
Events
| Event | When | Properties |
|---|---|---|
SmsSending |
Before sending. Return false from a listener to cancel. |
message, provider |
SmsSent |
After the provider accepted the message | message, provider, result |
SmsFailed |
After a send failed (the exception is re-thrown) | message, provider, exception |
SmsWebhookReceived |
After a verified provider webhook | provider, payload, messageId, status, recipient |
Webhooks
Delivery reports and inbound messages are received at POST /laravel-sms/webhook/{provider} (route name sms.webhook) for WhatsApp, Termii, Sendchamp, Africa's Talking and BulkSMS.
SMS_WEBHOOKS_ENABLED=true
WHATSAPP_VERIFY_TOKEN= # Meta's GET subscription challenge
WHATSAPP_WEBHOOK_SECRET= # app secret that signs POST callbacks (X-Hub-Signature-256)
TERMII_WEBHOOK_SECRET= # HMAC-SHA512, X-Termii-Signature
SENDCHAMP_WEBHOOK_SECRET= # HMAC-SHA256, X-Sendchamp-Signature
BULKSMS_WEBHOOK_SECRET= # HMAC-SHA256, X-BulkSMS-Signature
AFRICASTALKING_WEBHOOK_SECRET=
Every webhook fails closed: requests are rejected with 401 unless the secret is configured and verified. Africa's Talking does not sign callbacks, so add the secret to the callback URL: …/webhook/africastalking?token=<secret>. The token is stripped before your listeners see the payload.
Webhook routes use the stateless api middleware group with throttle:120,1. Change sms.webhooks.middleware and SMS_WEBHOOKS_PREFIX as needed. Custom drivers can receive webhooks by implementing Contracts\HandlesWebhooks.
Custom drivers
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use MrRijal\LaravelSms\Drivers\HttpDriver;
use MrRijal\LaravelSms\SentMessage;
use MrRijal\LaravelSms\SmsMessage;
use MrRijal\LaravelSms\SmsResult;
class AcmeDriver extends HttpDriver
{
protected string $label = 'Acme';
protected function validateConfig(): void
{
$this->require('api_key', 'from');
}
public function send(SmsMessage $message): SmsResult
{
$text = $this->text($message);
return $this->sendEach(
$message,
fn (PendingRequest $request, string $to) => $request
->withToken($this->value('api_key'))
->post('https://api.acme.test/sms', ['to' => $to, 'from' => $this->sender($message), 'text' => $text]),
function (Response $response, string $to): SentMessage {
if (! $response->successful()) {
$this->fail($response, 'error.message');
}
return new SentMessage($to, $this->jsonString($response, 'id'));
},
);
}
}
Register it in config/sms.php under drivers (and its credentials under providers), or at runtime:
Sms::extend('acme', fn ($app, array $config) => new AcmeDriver($config));
Any class implementing Contracts\SmsProvider works as a driver.
Testing
use MrRijal\LaravelSms\Facades\Sms;
use MrRijal\LaravelSms\SmsMessage;
public function test_it_sends_the_login_code(): void
{
Sms::fake();
// ... run your code ...
Sms::assertSentTo('+15551234567');
Sms::assertSent(fn (SmsMessage $sms) => str_contains($sms->getText(), 'code'));
Sms::assertSentCount(1);
Sms::assertQueued(2);
}
public function test_it_sends_nothing_for_unverified_users(): void
{
Sms::fake();
// ... run your code ...
Sms::assertNothingSent();
Sms::assertNothingQueued();
}
Sms::fake() fires the same events and resolves providers like a real send, so a typo in a provider name still fails your test. It keeps drivers registered with Sms::extend().
To test an HTTP driver end to end without network calls, use Http::fake(). The AWS SNS driver uses the AWS SDK instead, so pass a mocked SnsClient to new AwsSnsDriver($config, $client).
Run the package's own checks with composer ci (Pint, PHPStan at max level, and PHPUnit). CI covers PHP 8.2–8.5 × Laravel 11–13.
Upgrading
See UPGRADE.md for moving from 1.x to 2.0, and CHANGELOG.md for all changes.
Contributing
Contributions are welcome. Please see CONTRIBUTING.md.
Security
If you discover a security issue, please email prashantrijal.721@gmail.com instead of opening an issue. See SECURITY.md.
Credits
Support
If this package is useful to you, you can support its maintenance via buymemomo.com/rijal.
License
The MIT License (MIT). Please see LICENSE for more information.
Related Packages
A modern, flexible Laravel package for integrating any SMS gateway with REST API...
Version History
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| 1.0.2 | ^8.2 | ^11.0| | MIT | |
| 1.0.1 | ^8.2 | ^11.0| | MIT | |
| 1.0 | ^8.4 | ^12.0 | MIT |