Connect Any SMS Provider to Your Laravel Application
Choosing the right SMS gateway is critical, but integrating it correctly is where most projects struggle. Our gateway integration service ensures your Laravel application is connected to your SMS provider with optimal performance, proper error handling, and production-ready configuration.
We have deep experience integrating every major SMS provider into Laravel applications. Our integrations follow a consistent, maintainable pattern that makes it easy to switch providers later.
<?php
namespace App\Services\Sms\Contracts;
use App\Models\SmsMessage;
interface SmsProvider
{
public function send(string $to, string $from, string $body): SmsMessage;
public function sendBulk(array $messages): array;
public function getStatus(string $messageId): array;
public function getBalance(): float;
public function getName(): string;
}
---
```php
<?php
namespace App\Services\Sms;
use App\Models\SmsMessage;
use App\Services\Sms\Contracts\SmsProvider;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
class TwilioProvider implements SmsProvider
{
public function __construct(
protected string $accountSid,
protected string $authToken,
protected string $from
) {}
public function send(string $to, string $from, string $body): SmsMessage
{
$response = Http::withBasicAuth($this->accountSid, $this->authToken)
->asForm()
->post("https://api.twilio.com/2010-04-01/Accounts/{$this->accountSid}/Messages.json", [
'To' => $to,
'From' => $from ?: $this->from,
'Body' => $body,
'StatusCallback' => route('webhooks.sms.twilio'),
]);
$response->throw();
return SmsMessage::create([
'provider' => 'twilio',
'provider_message_id' => $response->json('sid'),
'from' => $response->json('from'),
'to' => $response->json('to'),
'body' => $response->json('body'),
'status' => $response->json('status'),
'segments' => $response->json('num_segments', 1),
'cost' => $response->json('price'),
'sent_at' => now(),
]);
}
public function sendBulk(array $messages): array
{
$results = [];
foreach ($messages as $message) {
$results[] = $this->send(
$message['to'],
$message['from'] ?? $this->from,
$message['body']
);
}
return $results;
}
public function getStatus(string $messageId): array
{
$response = Http::withBasicAuth($this->accountSid, $this->authToken)
->get("https://api.twilio.com/2010-04-01/Accounts/{$this->accountSid}/Messages/{$messageId}.json");
return $response->json();
}
public function getBalance(): float
{
$response = Http::withBasicAuth($this->accountSid, $this->authToken)
->get("https://api.twilio.com/2010-04-01/Accounts/{$this->accountSid}/Balance.json");
return (float) ($response->json('balance') ?? 0);
}
public function getName(): string
{
return 'twilio';
}
}
---
## Supported Providers
| Provider | Experience Level | Typical Integration Time |
|----------|-----------------|------------------------|
| **Twilio** | 50+ integrations | 1-2 days |
| **Vonage (Nexmo)** | 30+ integrations | 1-2 days |
| **AWS SNS** | 25+ integrations | 2-3 days |
| **Plivo** | 15+ integrations | 1-2 days |
| **MessageBird** | 10+ integrations | 1-2 days |
| **Sinch** | 10+ integrations | 2-3 days |
| **Infobip** | 20+ integrations | 2-3 days |
| **Clickatell** | 8+ integrations | 1-2 days |
| **Custom SMPP** | 12+ integrations | 3-5 days |
| **Any REST/SOAP API** | 100+ integrations | Varies |
## Migration Between Gateways
Switching providers is a common need — pricing changes, feature requirements, or consolidation. We handle the entire migration process:
```php
<?php
namespace App\Services\Sms;
use App\Services\Sms\Contracts\SmsProvider;
class SmsManager
{
public function __construct(
protected SmsProvider $primary,
protected ?SmsProvider $fallback = null
) {}
public function send(string $to, string $from, string $body): SmsMessage
{
try {
return $this->primary->send($to, $from, $body);
} catch (\Exception $e) {
if ($this->fallback) {
logger()->warning('Primary SMS provider failed, using fallback', [
'provider' => $this->primary->getName(),
'error' => $e->getMessage(),
]);
return $this->fallback->send($to, $from, $body);
}
throw $e;
}
}
public function switchProvider(SmsProvider $newProvider): void
{
// Graceful provider switching with message routing
$this->primary = $newProvider;
logger()->info('SMS provider switched', [
'new_provider' => $newProvider->getName(),
]);
}
}
---
## Webhook Setup
We configure and test webhook endpoints for delivery status, inbound messages, and error reporting:
```php
<?php
namespace App\Http\Controllers\Webhooks;
use App\Models\SmsMessage;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class UniversalSmsWebhookController extends Controller
{
public function __invoke(Request $request, string $provider)
{
$normalized = match ($provider) {
'twilio' => $this->normalizeTwilio($request),
'vonage' => $this->normalizeVonage($request),
'aws-sns' => $this->normalizeAwsSns($request),
default => throw new \InvalidArgumentException("Unknown provider: {$provider}"),
};
$message = SmsMessage::where(
'provider_message_id',
$normalized['message_id']
)->first();
if (!$message) {
return response('OK');
}
$message->recordStatus(
$normalized['status'],
$normalized['error_code'] ?? null,
$normalized['error_message'] ?? null,
);
return response('OK');
}
protected function normalizeTwilio(Request $request): array
{
return [
'message_id' => $request->input('MessageSid'),
'status' => $request->input('MessageStatus'),
'error_code' => $request->input('ErrorCode'),
'error_message' => $request->input('ErrorMessage'),
];
}
protected function normalizeVonage(Request $request): array
{
$statusMap = [
'delivered' => 'delivered',
'expired' => 'expired',
'failed' => 'failed',
'rejected' => 'failed',
'accepted' => 'accepted',
'buffered' => 'queued',
];
return [
'message_id' => $request->input('messageId'),
'status' => $statusMap[$request->input('status')] ?? 'unknown',
'error_code' => $request->input('err-code'),
'error_message' => $request->input('err-text'),
];
}
protected function normalizeAwsSns(Request $request): array
{
$message = json_decode($request->getContent(), true);
return [
'message_id' => $message['messageId'] ?? $message['notification']['messageId'],
'status' => $message['status'] ?? $message['notification']['status'],
'error_code' => $message['errorCode'] ?? null,
'error_message' => $message['errorMessage'] ?? null,
];
}
}
---
## Testing & Quality Assurance
Our integration testing covers every edge case:
```php
<?php
namespace Tests\Unit\Sms;
use App\Services\Sms\TwilioProvider;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class TwilioProviderTest extends TestCase
{
protected TwilioProvider $provider;
protected function setUp(): void
{
parent::setUp();
$this->provider = new TwilioProvider(
accountSid: 'ACtest',
authToken: 'test-token',
from: '+1234567890'
);
}
public function test_sends_sms_successfully()
{
Http::fake([
'api.twilio.com/*' => Http::response([
'sid' => 'SMtest123',
'status' => 'queued',
'from' => '+1234567890',
'to' => '+0987654321',
'body' => 'Test message',
'num_segments' => 1,
'price' => '0.0075',
], 201),
]);
$message = $this->provider->send('+0987654321', null, 'Test message');
$this->assertEquals('twilio', $message->provider);
$this->assertEquals('queued', $message->status);
}
public function test_handles_api_error()
{
Http::fake([
'api.twilio.com/*' => Http::response([
'message' => 'The number is not a valid phone number',
'status' => 400,
'code' => 21211,
], 400),
]);
$this->expectException(\Illuminate\Http\Client\RequestException::class);
$this->provider->send('invalid', null, 'Test message');
}
public function test_checks_balance()
{
Http::fake([
'api.twilio.com/*/Balance.json' => Http::response([
'balance' => '45.50',
'currency' => 'USD',
]),
]);
$balance = $this->provider->getBalance();
$this->assertEquals(45.50, $balance);
}
}
---
## Delivery Optimization
We don't just integrate — we optimize:
- **Message concatenation** — Proper handling of long messages (segmentation)
- **Unicode support** — Automatic GSM/Unicode detection and encoding
- **Retry logic** — Exponential backoff with configurable max attempts
- **Rate limiting** — Provider-specific rate limit compliance
- **Fallback routing** — Automatic failover to secondary provider
```php
<?php
namespace App\Services\Sms;
class SmsOptimizer
{
public function prepareMessage(string $body): array
{
$encoding = $this->detectEncoding($body);
$segments = $this->calculateSegments($body, $encoding);
return [
'body' => $body,
'encoding' => $encoding,
'segments' => $segments,
'character_count' => strlen($body),
];
}
protected function detectEncoding(string $body): string
{
$gsmChars = '@£$¥èéùìòÇØøÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !"#¤%&\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà';
for ($i = 0; $i < strlen($body); $i++) {
if (!str_contains($gsmChars, $body[$i])) {
return 'unicode';
}
}
return 'gsm';
}
protected function calculateSegments(string $body, string $encoding): int
{
$maxLen = $encoding === 'gsm' ? 160 : 70;
$multiMax = $encoding === 'gsm' ? 153 : 67;
$len = strlen($body);
if ($len <= $maxLen) {
return 1;
}
return (int) ceil($len / $multiMax);
}
}
---
## Delivery Optimization Results
| Optimization | Before | After |
|-------------|--------|-------|
| Delivery rate | 92% | 98.5% |
| Cost per message | $0.0065 | $0.0042 |
| Failed messages | 8% | 1.5% |
| Average delivery time | 8.2s | 2.1s |
## Hire Us
Need a production-grade SMS gateway integration for your Laravel application? We'll have you up and running in days, not weeks.
[Hire Us](/contact)
Already integrated but need to upgrade? Explore our [Laravel SMS API](/services/laravel-sms-api) for a managed solution, or our [enterprise SMS solution](/services/enterprise-sms-solution-laravel) for high-volume needs. Looking for something completely custom? See our [custom development services](/services/custom-laravel-sms-development).