SMS Gateway Integration in Laravel (Multi-Provider Guide)

SMS Gateway Integration in Laravel (Multi-Provider Guide)
Laravel SMS Team
Laravel SMS Team
June 22, 2026

Relying on a single SMS provider introduces a single point of failure. This guide demonstrates building a multi-provider SMS gateway system in Laravel that supports Twilio, Vonage (formerly Nexmo), AWS SNS, and custom providers — with automatic fallback between gateways.

Design Pattern for Gateway Abstraction

The strategy pattern is ideal for this use case. Define a common interface, implement it for each provider, and use Laravel’s service container to resolve the appropriate gateway at runtime.

Creating an SMS Gateway Interface

Start with a clean interface:

<?php

namespace App\Contracts;

use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;

interface SmsGateway
{
    public function send(SmsMessage $message): SmsResult;

    public function getName(): string;
}
---

Define DTOs for the message and result:

```php
<?php

namespace App\DataTransferObjects;

class SmsMessage
{
    public function __construct(
        public readonly string $to,
        public readonly string $content,
        public readonly ?string $from = null,
        public readonly ?array $options = [],
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            to: $data['to'],
            content: $data['content'],
            from: $data['from'] ?? null,
            options: $data['options'] ?? [],
        );
    }
}
---

```php
<?php

namespace App\DataTransferObjects;

class SmsResult
{
    public function __construct(
        public readonly bool $success,
        public readonly string $provider,
        public readonly ?string $messageId = null,
        public readonly ?string $error = null,
        public readonly array $raw = [],
    ) {}
}
---

## Implementing TwilioGateway

```php
<?php

namespace App\Services\SmsGateways;

use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
use Twilio\Rest\Client;

class TwilioGateway implements SmsGateway
{
    protected Client $client;
    protected string $fromNumber;

    public function __construct()
    {
        $this->client = new Client(
            config('gateways.twilio.account_sid'),
            config('gateways.twilio.auth_token')
        );
        $this->fromNumber = config('gateways.twilio.from_number');
    }

    public function send(SmsMessage $message): SmsResult
    {
        try {
            $response = $this->client->messages->create(
                $message->to,
                [
                    'from' => $message->from ?? $this->fromNumber,
                    'body' => $message->content,
                ] + ($message->options ?? [])
            );

            return new SmsResult(
                success: true,
                provider: $this->getName(),
                messageId: $response->sid,
                raw: $response->toArray(),
            );
        } catch (\Exception $e) {
            return new SmsResult(
                success: false,
                provider: $this->getName(),
                error: $e->getMessage(),
            );
        }
    }

    public function getName(): string
    {
        return 'twilio';
    }
}
---

## Implementing VonageGateway

```php
<?php

namespace App\Services\SmsGateways;

use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
use Vonage\Client as VonageClient;
use Vonage\Client\Credentials\Basic;
use Vonage\SMS\Message\SMS;

class VonageGateway implements SmsGateway
{
    protected VonageClient $client;
    protected string $fromNumber;

    public function __construct()
    {
        $basic = new Basic(
            config('gateways.vonage.api_key'),
            config('gateways.vonage.api_secret')
        );
        $this->client = new VonageClient($basic);
        $this->fromNumber = config('gateways.vonage.from_number');
    }

    public function send(SmsMessage $message): SmsResult
    {
        try {
            $response = $this->client->sms()->send(
                new SMS(
                    $message->to,
                    $message->from ?? $this->fromNumber,
                    $message->content
                )
            );

            $result = $response->current();

            return new SmsResult(
                success: $result->getStatus() === 0,
                provider: $this->getName(),
                messageId: $result->getMessageId(),
                raw: $result->toArray(),
            );
        } catch (\Exception $e) {
            return new SmsResult(
                success: false,
                provider: $this->getName(),
                error: $e->getMessage(),
            );
        }
    }

    public function getName(): string
    {
        return 'vonage';
    }
}
---

## Using Laravel's Service Container for Binding

Register gateways in a service provider:

```php
<?php

namespace App\Providers;

use App\Contracts\SmsGateway;
use App\Services\SmsGateways\TwilioGateway;
use App\Services\SmsGateways\VonageGateway;
use App\Services\SmsGateways\AwsSnsGateway;
use App\Services\SmsManager;
use Illuminate\Support\ServiceProvider;

class SmsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind('gateway.twilio', TwilioGateway::class);
        $this->app->bind('gateway.vonage', VonageGateway::class);
        $this->app->bind('gateway.aws-sns', AwsSnsGateway::class);

        $this->app->singleton(SmsManager::class, function ($app) {
            $manager = new SmsManager();

            $default = config('gateways.default', 'twilio');
            $fallback = config('gateways.fallback');

            $manager->setDefault($app->make("gateway.{$default}"));

            if ($fallback) {
                $manager->setFallback($app->make("gateway.{$fallback}"));
            }

            return $manager;
        });
    }
}
---

Configuration file at `config/gateways.php`:

```php
<?php

return [
    'default' => env('SMS_GATEWAY', 'twilio'),

    'fallback' => env('SMS_FALLBACK_GATEWAY', 'vonage'),

    'twilio' => [
        'account_sid' => env('TWILIO_ACCOUNT_SID'),
        'auth_token' => env('TWILIO_AUTH_TOKEN'),
        'from_number' => env('TWILIO_FROM_NUMBER'),
    ],

    'vonage' => [
        'api_key' => env('VONAGE_API_KEY'),
        'api_secret' => env('VONAGE_API_SECRET'),
        'from_number' => env('VONAGE_FROM_NUMBER'),
    ],

    'aws-sns' => [
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_SNS_REGION', 'us-east-1'),
        'sender_id' => env('AWS_SNS_SENDER_ID'),
    ],
];
---

## Automatic Fallback Between Gateways

The `SmsManager` orchestrates primary and fallback gateways:

```php
<?php

namespace App\Services;

use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;
use Illuminate\Support\Facades\Log;

class SmsManager implements SmsGateway
{
    protected SmsGateway $default;

    protected ?SmsGateway $fallback = null;

    public function setDefault(SmsGateway $gateway): void
    {
        $this->default = $gateway;
    }

    public function setFallback(?SmsGateway $gateway): void
    {
        $this->fallback = $gateway;
    }

    public function send(SmsMessage $message): SmsResult
    {
        $result = $this->default->send($message);

        if ($result->success) {
            return $result;
        }

        Log::warning('Primary SMS gateway failed', [
            'gateway' => $this->default->getName(),
            'error' => $result->error,
        ]);

        if ($this->fallback) {
            $fallbackResult = $this->fallback->send($message);

            Log::info('Fallback SMS gateway used', [
                'gateway' => $this->fallback->getName(),
                'success' => $fallbackResult->success,
            ]);

            return $fallbackResult;
        }

        return $result;
    }

    public function getName(): string
    {
        return $this->default->getName();
    }
}
---

### Usage in Application Code

```php
use App\DataTransferObjects\SmsMessage;
use App\Services\SmsManager;

$manager = app(SmsManager::class);

$message = new SmsMessage(
    to: '+15559876543',
    content: 'Your appointment is confirmed for tomorrow at 2 PM.',
    from: '+15551234567',
);

$result = $manager->send($message);

if ($result->success) {
    echo "Sent via {$result->provider}, ID: {$result->messageId}";
} else {
    echo "Failed: {$result->error}";
}
---

### Selecting a Specific Gateway

When you need to force a specific provider (e.g., for cost routing):

```php
// Direct usage without fallback
$twilio = app('gateway.twilio');
$result = $twilio->send($message);

// Or via a helper
public function sendVia(string $gateway, SmsMessage $message): SmsResult
{
    return app("gateway.{$gateway}")->send($message);
}
---

## Testing with Mock Gateways

Create a fake gateway for testing:

```php
<?php

namespace App\Services\SmsGateways;

use App\Contracts\SmsGateway;
use App\DataTransferObjects\SmsMessage;
use App\DataTransferObjects\SmsResult;

class FakeGateway implements SmsGateway
{
    public bool $shouldFail = false;

    public function send(SmsMessage $message): SmsResult
    {
        if ($this->shouldFail) {
            return new SmsResult(
                success: false,
                provider: $this->getName(),
                error: 'Simulated failure',
            );
        }

        return new SmsResult(
            success: true,
            provider: $this->getName(),
            messageId: 'fake_' . uniqid(),
        );
    }

    public function getName(): string
    {
        return 'fake';
    }
}
---

Swap gateways in tests:

```php
<?php

namespace Tests\Feature;

use App\Contracts\SmsGateway;
use App\Services\SmsGateways\FakeGateway;
use App\Services\SmsManager;
use Tests\TestCase;

class SmsSendingTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        $fake = new FakeGateway();
        $manager = app(SmsManager::class);
        $manager->setDefault($fake);
    }

    public function test_sends_sms_successfully(): void
    {
        $this->post('/send-sms', [
            'to' => '+15559876543',
            'message' => 'Test message',
        ])->assertOk();
    }
}
---

### Testing Fallback Logic

```php
public function test_fallback_gateway_is_used_when_primary_fails(): void
{
    $primary = new FakeGateway();
    $primary->shouldFail = true;

    $fallback = new FakeGateway();

    $manager = app(SmsManager::class);
    $manager->setDefault($primary);
    $manager->setFallback($fallback);

    $result = $manager->send(
        new SmsMessage('+15559876543', 'Hello')
    );

    $this->assertTrue($result->success);
    $this->assertEquals('fake', $result->provider);
}
---

## Related Articles

- [Twilio SMS Integration with Laravel (Complete Guide 2026)](/blog/laravel-twilio-sms-integration)
- [Building a Bulk SMS Platform with Laravel](/blog/building-bulk-sms-platform-laravel)
- [Laravel Custom SMS Channel for Notifications](/blog/laravel-custom-sms-channel)