Twilio is one of the most popular SMS gateways available, offering reliable global SMS delivery, a robust API, and excellent developer tooling. This guide walks through integrating Twilio SMS with your Laravel application from start to finish.
Start by installing the official Twilio PHP SDK via Composer:
composer require twilio/sdk
---
This package provides the `Twilio\Rest\Client` class used to interact with the Twilio API.
## Configuring Twilio Credentials
Add your Twilio credentials to `.env`:
```env
TWILIO_ACCOUNT_SID=your_account_sid
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_FROM_NUMBER=+15551234567
---
Create a `config/twilio.php` configuration file:
```php
<?php
return [
'account_sid' => env('TWILIO_ACCOUNT_SID'),
'auth_token' => env('TWILIO_AUTH_TOKEN'),
'from_number' => env('TWILIO_FROM_NUMBER'),
];
---
## Creating a Twilio Service Class
Build a dedicated service class to keep your Twilio logic organized:
```php
<?php
namespace App\Services;
use Twilio\Rest\Client;
class TwilioService
{
protected Client $client;
protected string $fromNumber;
public function __construct()
{
$this->client = new Client(
config('twilio.account_sid'),
config('twilio.auth_token')
);
$this->fromNumber = config('twilio.from_number');
}
public function sendSms(string $to, string $message): array
{
$message = $this->client->messages->create($to, [
'from' => $this->fromNumber,
'body' => $message,
]);
return [
'sid' => $message->sid,
'status' => $message->status,
'to' => $message->to,
'from' => $message->from,
];
}
}
---
## Sending SMS via Twilio
Use the service anywhere in your application:
```php
use App\Services\TwilioService;
$twilio = app(TwilioService::class);
$result = $twilio->sendSms('+15559876543', 'Your verification code is: 8472');
Log::info('SMS sent', ['sid' => $result['sid']]);
---
## Twilio Notification Channel
Laravel's notification system makes SMS delivery clean and testable. Create a notification channel:
```php
<?php
namespace App\Notifications\Channels;
use App\Services\TwilioService;
use Illuminate\Notifications\Notification;
class TwilioSmsChannel
{
public function __construct(
protected TwilioService $twilio
) {}
public function send(object $notifiable, Notification $notification): void
{
$message = $notification->toTwilioSms($notifiable);
$this->twilio->sendSms(
$message->to,
$message->content
);
}
}
---
Define a notification using the channel:
```php
<?php
namespace App\Notifications;
use App\Notifications\Channels\TwilioSmsChannel;
use Illuminate\Notifications\Notification;
class WelcomeSms extends Notification
{
public function via(object $notifiable): array
{
return [TwilioSmsChannel::class];
}
public function toTwilioSms(object $notifiable): object
{
return (object) [
'to' => $notifiable->phone,
'content' => "Welcome to our platform, {$notifiable->name}!",
];
}
}
---
Register the channel in `config/app.php` or a service provider:
```php
use App\Notifications\Channels\TwilioSmsChannel;
use App\Services\TwilioService;
public function register(): void
{
$this->app->bind(TwilioSmsChannel::class, function ($app) {
return new TwilioSmsChannel($app->make(TwilioService::class));
});
}
---
## Receiving SMS Webhooks
Twilio can forward inbound SMS to your Laravel application via webhooks. Create a route and controller:
```php
use App\Http\Controllers\IncomingSmsController;
Route::post('/webhooks/twilio/incoming', [IncomingSmsController::class, 'handle'])
->name('webhooks.twilio.incoming');
---
```php
<?php
namespace App\Http\Controllers;
use App\Models\SmsMessage;
use Illuminate\Http\Request;
class IncomingSmsController extends Controller
{
public function handle(Request $request): \Illuminate\Http\Response
{
$validated = $request->validate([
'From' => 'required|string',
'Body' => 'required|string',
'MessageSid' => 'required|string',
'To' => 'required|string',
]);
SmsMessage::create([
'from' => $validated['From'],
'to' => $validated['To'],
'body' => $validated['Body'],
'message_sid' => $validated['MessageSid'],
'direction' => 'inbound',
]);
return response('<Response></Response>', 200)
->header('Content-Type', 'text/xml');
}
}
---
### Setting the Webhook URL in Twilio Console
1. Log into the [Twilio Console](https://console.twilio.com)
2. Navigate to **Phone Numbers > Manage > Active Numbers**
3. Select your number
4. Under **Messaging**, set **A message comes in** to your webhook URL: `https://your-app.com/webhooks/twilio/incoming`
5. Set the HTTP method to `POST`
## Verifying Twilio Webhook Signatures
Always verify incoming webhooks to ensure they're genuinely from Twilio:
```php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Twilio\Security\RequestValidator;
class IncomingSmsController extends Controller
{
public function handle(Request $request): \Illuminate\Http\Response
{
$validator = new RequestValidator(config('twilio.auth_token'));
$signature = $request->header('X-Twilio-Signature');
$url = $request->fullUrl();
$params = $request->all();
if (!$validator->validate($signature, $url, $params)) {
abort(401, 'Invalid webhook signature');
}
// Process the message...
}
}
---
You can also register middleware to handle validation globally:
```php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Twilio\Security\RequestValidator;
class VerifyTwilioWebhook
{
public function handle(Request $request, Closure $next): mixed
{
$validator = new RequestValidator(config('twilio.auth_token'));
if (!$validator->validate(
$request->header('X-Twilio-Signature'),
$request->fullUrl(),
$request->all()
)) {
abort(401, 'Invalid webhook signature');
}
return $next($request);
}
}
---
## Delivery Status Callbacks
Twilio can send delivery receipts via a separate webhook:
```php
Route::post('/webhooks/twilio/status', [DeliveryStatusController::class, 'handle'])
->name('webhooks.twilio.status');
---
```php
<?php
namespace App\Http\Controllers;
use App\Models\SmsMessage;
use Illuminate\Http\Request;
class DeliveryStatusController extends Controller
{
public function handle(Request $request): \Illuminate\Http\Response
{
$validated = $request->validate([
'MessageSid' => 'required|string',
'MessageStatus' => 'required|string',
'ErrorCode' => 'nullable|string',
]);
SmsMessage::where('message_sid', $validated['MessageSid'])
->update(['status' => $validated['MessageStatus']]);
return response()->json(['status' => 'ok']);
}
}
---
## Testing the Twilio Integration
### Unit Tests
```php
<?php
namespace Tests\Unit;
use App\Services\TwilioService;
use Mockery\MockInterface;
use Tests\TestCase;
class TwilioServiceTest extends TestCase
{
public function test_sends_sms_successfully(): void
{
$this->mock(TwilioService::class, function (MockInterface $mock) {
$mock->shouldReceive('sendSms')
->once()
->with('+15559876543', 'Test message')
->andReturn([
'sid' => 'SM123',
'status' => 'queued',
'to' => '+15559876543',
'from' => '+15551234567',
]);
});
$service = app(TwilioService::class);
$result = $service->sendSms('+15559876543', 'Test message');
$this->assertEquals('queued', $result['status']);
}
}
---
### Feature Tests for Webhooks
```php
<?php
namespace Tests\Feature;
use App\Models\SmsMessage;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class IncomingSmsWebhookTest extends TestCase
{
use RefreshDatabase;
public function test_handles_incoming_sms(): void
{
$response = $this->postJson('/webhooks/twilio/incoming', [
'From' => '+15559876543',
'Body' => 'Hello, this is a test reply.',
'MessageSid' => 'SM' . str_repeat('0', 32),
'To' => '+15551234567',
]);
$response->assertStatus(200);
$this->assertDatabaseHas('sms_messages', [
'from' => '+15559876543',
'body' => 'Hello, this is a test reply.',
'direction' => 'inbound',
]);
}
}
---
## Troubleshooting Common Issues
| Issue | Likely Cause | Solution |
|-------|-------------|----------|
| "Account not authorized" | Wrong SID/token | Verify `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` in `.env` |
| Invalid `From` number | Unverified sender | Verify the number in Twilio Console for trial accounts |
| Webhook returns 401 | Signature validation failed | Double-check `TWILIO_AUTH_TOKEN` matches the one in your account |
| SMS not delivered | Carrier filtering | Check delivery status via webhook; consider message content |
| Trial account limits | Trial restrictions | Upgrade to a paid Twilio account |
## Related Articles
- [Building a Bulk SMS Platform with Laravel](/blog/building-bulk-sms-platform-laravel)
- [SMS Gateway Integration in Laravel (Multi-Provider Guide)](/blog/laravel-sms-gateway-integration)
- [Laravel Custom SMS Channel for Notifications](/blog/laravel-custom-sms-channel)