textbee Logotextbee.dev
Plans from $9.99/mo.View Plans
Send SMS from Laravel: Gateway, Notifications, and Queued Jobs
laravel
php
tutorial
sms
api
notifications

Send SMS from Laravel: Gateway, Notifications, and Queued Jobs

Send SMS in Laravel using textbee and your Android phone. Covers the Http facade, a service class, a custom notification channel, and queued jobs.

TT

textbee team

6 min read
Share

TL;DR

  • textbee's REST API works perfectly with Laravel's Http:: facade: no SDK or package required.
  • The cleanest pattern: an SmsService class injected via Laravel's service container.
  • For Laravel Notifications: build a custom channel so you can use $notifiable->notify(new OrderShipped()) like any built-in channel.
  • For high-traffic apps: dispatch a queued job so SMS sends never block the request lifecycle.
  • Store credentials in .env and access via config(): never hardcode API keys.

If you're building a Laravel application and want to send SMS notifications, textbee gives you a practical alternative to Twilio: your own Android phone becomes the gateway, your messages send from your real number, and you pay a flat subscription instead of per-message fees.

Before you start

  • textbee app installed on your Android device: download and quickstart
  • Device ID and API key from the dashboard at textbee.dev
  • Laravel 9+ (examples use Laravel 10/11 syntax; Laravel 9 is identical)
  • PHP 8.1+ (for readonly properties in the service class)

Step 1: Add credentials to .env

TEXTBEE_DEVICE_ID=your-device-id-here
TEXTBEE_API_KEY=your-api-key-here

Then publish them through a config file. Create config/textbee.php:

PHP
<?php

return [
    'device_id' => env('TEXTBEE_DEVICE_ID'),
    'api_key'   => env('TEXTBEE_API_KEY'),
];

Step 2: The service class

Create app/Services/SmsService.php:

PHP
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;

class SmsService
{
    private string $baseUrl = 'https://api.textbee.dev/api/v1/gateway/devices';

    public function send(string|array $recipients, string $message): array
    {
        $deviceId = config('textbee.device_id');
        $apiKey   = config('textbee.api_key');

        $response = Http::withHeader('x-api-key', $apiKey)
            ->timeout(10)
            ->post("{$this->baseUrl}/{$deviceId}/send-sms", [
                'recipients' => (array) $recipients,
                'message'    => $message,
            ]);

        $response->throw(); // throws RequestException on 4xx/5xx

        return $response->json('data');
    }
}

Register it in AppServiceProvider (or let Laravel auto-discover it since it has no interface dependency):

PHP
// In App\Providers\AppServiceProvider::register()
$this->app->singleton(SmsService::class);

Use it anywhere in the application:

PHP
use App\Services\SmsService;

class AppointmentController extends Controller
{
    public function confirm(Appointment $appointment, SmsService $sms)
    {
        $sms->send(
            $appointment->client_phone,
            "Hi {$appointment->client_name}, your appointment is confirmed for {$appointment->formatted_time}."
        );

        return response()->json(['status' => 'confirmed']);
    }
}

Step 3: Sending to multiple recipients

The (array) $recipients cast in the service class means you can pass a string for a single recipient or an array for bulk:

PHP
// Single
$sms->send('+15551234567', 'Your order has shipped!');

// Multiple (same message to all)
$sms->send(['+15551234567', '+15559876543'], 'Store closes at 6pm today.');

Step 4: Custom Laravel Notification channel

Laravel's Notification system lets you send notifications through multiple channels (mail, database, broadcast) using a unified API. Adding textbee as a channel means you can write:

PHP
$user->notify(new AppointmentReminder($appointment));

Create the channel (app/Notifications/Channels/TextbeeChannel.php):

PHP
<?php

namespace App\Notifications\Channels;

use App\Services\SmsService;
use Illuminate\Notifications\Notification;

class TextbeeChannel
{
    public function __construct(private SmsService $sms) {}

    public function send(mixed $notifiable, Notification $notification): void
    {
        $phone = $notifiable->routeNotificationFor('textbee')
            ?? $notifiable->phone;

        if (! $phone) {
            return;
        }

        $message = $notification->toTextbee($notifiable);
        $this->sms->send($phone, $message);
    }
}

Create a notification (app/Notifications/AppointmentReminder.php):

PHP
<?php

namespace App\Notifications;

use App\Notifications\Channels\TextbeeChannel;
use Illuminate\Notifications\Notification;

class AppointmentReminder extends Notification
{
    public function __construct(private mixed $appointment) {}

    public function via(mixed $notifiable): array
    {
        return [TextbeeChannel::class];
    }

    public function toTextbee(mixed $notifiable): string
    {
        return "Hi {$notifiable->name}, reminder: your appointment is tomorrow at {$this->appointment->time}. Reply YES to confirm.";
    }
}

Add the routing method to your User model (or any notifiable):

PHP
public function routeNotificationForTextbee(): string
{
    return $this->phone_number; // E.164 format
}

Now you can notify users exactly like you would via email:

PHP
$user->notify(new AppointmentReminder($appointment));
// or for scheduled/queued delivery:
Notification::send($users, new AppointmentReminder($appointment));

Step 5: Queued sending (don't block the request)

SMS sends are network I/O: they can take 1-3 seconds. Never run them synchronously inside a request handler if you can avoid it. Use a queued job:

Create the job (php artisan make:job SendSms):

PHP
<?php

namespace App\Jobs;

use App\Services\SmsService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendSms implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 30; // seconds between retries

    public function __construct(
        private string $phone,
        private string $message,
    ) {}

    public function handle(SmsService $sms): void
    {
        $sms->send($this->phone, $this->message);
    }
}

Dispatch from a controller or service:

PHP
SendSms::dispatch($user->phone, "Your order {$order->number} has shipped!")->onQueue('notifications');

Or dispatch with a delay:

PHP
// Send 24 hours before appointment
SendSms::dispatch($client->phone, $reminderText)
    ->delay(now()->diffInSeconds($appointment->starts_at->subDay()));

Make sure you have a queue worker running:

Shell
php artisan queue:work --queue=notifications

For production, configure Supervisor to keep the worker alive across restarts.

Error handling

The $response->throw() call in SmsService raises a RequestException on HTTP errors. Catch it where it matters:

PHP
use Illuminate\Http\Client\RequestException;

try {
    $sms->send($phone, $message);
} catch (RequestException $e) {
    Log::warning('SMS send failed', [
        'phone'  => $phone,
        'status' => $e->response->status(),
        'body'   => $e->response->body(),
    ]);
    // fail silently, alert, or retry depending on use case
}

For queued jobs, Laravel's built-in $tries and $backoff handle retries automatically. Failed jobs land in the failed_jobs table for inspection.

What the API returns

On success:

{
  "data": {
    "success": true,
    "message": "SMS added to queue for processing",
    "smsBatchId": "batch-abc123",
    "recipientCount": 1
  }
}

The smsBatchId is your reference for tracking delivery status in the textbee dashboard. success: true means the message was accepted and queued on your Android device: delivery to the recipient's handset happens asynchronously over the cellular network.

Testing SMS sends in Laravel

In tests, use Http::fake() to mock the textbee API:

PHP
use Illuminate\Support\Facades\Http;

public function test_sends_sms_on_confirmation(): void
{
    Http::fake([
        'api.textbee.dev/*' => Http::response([
            'data' => ['success' => true, 'smsBatchId' => 'test-batch', 'recipientCount' => 1],
        ], 200),
    ]);

    $response = $this->postJson('/api/appointments/1/confirm');

    $response->assertOk();
    Http::assertSent(fn ($request) =>
        str_contains($request->url(), 'send-sms') &&
        $request['message'] === 'Your appointment is confirmed.'
    );
}

Frequently asked questions

Does textbee have a native Laravel package?

Not yet. The Http:: facade and the service class pattern above cover all use cases cleanly. If you'd like an official textbee/laravel package, open a GitHub issue to signal demand.

Can I use Laravel Notifications with textbee?

Yes. Step 4 above shows exactly how to build a custom Notification channel. Once the channel is registered, textbee works identically to built-in channels like mail and database. Your Notification class just needs a toTextbee() method that returns the message string.

How do I send SMS in a Celery task? (Laravel equivalent: queued job)

Use the SendSms job from Step 5. It implements ShouldQueue, so dispatching it puts it in Laravel's queue, not the request thread. Pair with Laravel Horizon or a Supervisor-managed queue worker for production reliability.

Can I send SMS from a Laravel console command?

Yes. Inject SmsService in the command constructor or use the facade-style:

PHP
class SendWeeklyDigest extends Command
{
    public function handle(SmsService $sms): void
    {
        User::whereNotNull('phone')->each(function ($user) use ($sms) {
            $sms->send($user->phone, "Weekly summary: ...");
            sleep(2); // rate limiting between sends
        });
    }
}

Run it as a scheduled command in routes/console.php (Laravel 11+) or app/Console/Kernel.php (Laravel 10).

What's the right way to store the API key in production?

In .env as TEXTBEE_API_KEY, and access via config('textbee.api_key'): never via env() directly in application code (breaks config caching). Use Laravel Forge secrets, Vapor environment variables, or a secrets manager (AWS SSM, Doppler) to inject the value into the deployment environment.

Keep going

Download textbee and send your first Laravel SMS in under 10 minutes.