Plans from $9.99/moView Plans
S

Send SMS from PHP: No Twilio, Just Your Android Phone

Send real SMS from PHP using textbee and your Android phone as the gateway. Native cURL, Guzzle, and Laravel Http examples — zero per-message fees.

textbee team
php
tutorial
api
sms
android
laravel

TL;DR

  • textbee's REST API works from any PHP version that can make HTTP requests — no SDK needed.
  • Three options: native cURL (zero dependencies), Guzzle (composer require guzzlehttp/guzzle), or Laravel's Http facade (one line).
  • Store credentials in environment variables, not hardcoded strings.
  • The recipients field is an array — one request can reach multiple numbers.
  • No per-message API fee: messages go through the SIM in your Android phone.

You don't need a Twilio account or a virtual phone number to send SMS from PHP. If you have an Android phone with a working SIM, textbee turns it into an SMS gateway your PHP code can call over a simple REST API. The message goes out from your real phone number.

Before you start

  • textbee app installed on your Android device and linked to your account: download and quickstart
  • Device ID and API key from the dashboard at textbee.dev
  • PHP 7.4+ (the examples use modern syntax, but the cURL approach works on older versions too)
  • On Android 15 or 16? You may need to manually enable SMS permissions once

Option 1: Native cURL (no dependencies)

This works on any PHP installation. No Composer, no packages.

<?php

function sendSms(string $to, string $message): array
{
    $deviceId = getenv('TEXTBEE_DEVICE_ID');
    $apiKey   = getenv('TEXTBEE_API_KEY');

    $url  = "https://api.textbee.dev/api/v1/gateway/devices/{$deviceId}/send-sms";
    $body = json_encode(['recipients' => [$to], 'message' => $message]);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            "x-api-key: {$apiKey}",
        ],
    ]);

    $response = curl_exec($ch);
    $status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($status < 200 || $status >= 300) {
        throw new RuntimeException("SMS send failed with HTTP {$status}: {$response}");
    }

    return json_decode($response, true);
}

// Usage
$result = sendSms('+15551234567', 'Hello from PHP!');
print_r($result);

Export your credentials before running:

export TEXTBEE_DEVICE_ID="your-device-id"
export TEXTBEE_API_KEY="your-api-key"
php send.php

Option 2: Guzzle HTTP

If your project already uses Guzzle (or you prefer a more expressive HTTP client):

composer require guzzlehttp/guzzle
<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

function sendSms(string $to, string $message): array
{
    $deviceId = getenv('TEXTBEE_DEVICE_ID');
    $apiKey   = getenv('TEXTBEE_API_KEY');

    $client = new Client(['base_uri' => 'https://api.textbee.dev']);

    try {
        $response = $client->post("/api/v1/gateway/devices/{$deviceId}/send-sms", [
            'headers' => [
                'x-api-key'    => $apiKey,
                'Content-Type' => 'application/json',
            ],
            'json' => [
                'recipients' => [$to],
                'message'    => $message,
            ],
        ]);

        return json_decode($response->getBody()->getContents(), true);
    } catch (RequestException $e) {
        $body = $e->hasResponse() ? $e->getResponse()->getBody()->getContents() : '';
        throw new RuntimeException("SMS send failed: {$body}", 0, $e);
    }
}

$result = sendSms('+15551234567', 'Hello from Guzzle!');
print_r($result);

Option 3: Laravel Http facade

If you're in a Laravel project, this is the cleanest path:

use Illuminate\Support\Facades\Http;

function sendSms(string $to, string $message): array
{
    $response = Http::withHeaders([
        'x-api-key' => config('services.textbee.api_key'),
    ])->post(
        'https://api.textbee.dev/api/v1/gateway/devices/' . config('services.textbee.device_id') . '/send-sms',
        [
            'recipients' => [$to],
            'message'    => $message,
        ]
    );

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

    return $response->json();
}

Add your credentials to config/services.php:

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

And in .env:

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

What the API returns

On success, you get a JSON response wrapped in data:

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

success: true means the message was accepted and queued on your device — not yet necessarily delivered to the recipient's handset. The device sends it asynchronously over the cellular network. Track delivery status in the textbee dashboard, or configure webhooks to get notified when status changes.

On failure, you get a non-2xx status code and a body like:

{
  "success": false,
  "error": "Device not found or not active"
}

Common causes: wrong device ID, API key mismatch, device offline, or SMS permissions revoked on the Android device.

Sending to multiple recipients

The recipients field is an array. Send one request to reach multiple numbers:

$result = sendSms(['+15551234567', '+15559876543'], 'Appointment reminder: tomorrow at 10am.');

// Adjust the function signature:
function sendSms(array|string $to, string $message): array
{
    $recipients = is_array($to) ? $to : [$to];
    // ... rest of the function, use $recipients in the JSON body
}

Things worth knowing

Phone number format: Use E.164 format (+ followed by country code and number, no spaces or dashes). Example: +15551234567 for a US number. Some carriers accept local formats for same-country sends, but E.164 is unambiguous and always correct.

No per-message API fee: Because messages route through your own Android SIM, you pay your carrier's SMS rate — which is often included in an unlimited plan. The textbee pricing is a flat monthly subscription, not per-message.

Opt-in matters: Messages come from your real phone number. Unsolicited texts can violate regulations regardless of which SMS technology you use. Read the compliance checklist before sending to lists.

Rate limits: The practical constraint is your SIM and carrier throughput (~hundreds of messages per hour on a consumer SIM), not an API rate limit. For higher volume, connect multiple Android devices to the same account.

Frequently asked questions

Does this work with Laravel?

Yes — Option 3 above uses Laravel's Http facade directly. For a full Laravel integration with queued jobs, see the dedicated Laravel SMS guide.

Do I need to install any packages?

No. Option 1 uses PHP's built-in curl extension, which is enabled by default on virtually every PHP installation. Options 2 and 3 require Guzzle/Laravel, but only if you prefer their interface.

Can I send to multiple numbers in one call?

Yes. Pass an array to recipients. One API call, one batch — each number gets the same message body.

What PHP version does this require?

Option 1 works on PHP 5.6+ (drop the type hints for older versions). Options 2 and 3 require PHP 7.4+ and their respective dependencies.

Is there a textbee PHP SDK?

Not yet. The REST API is simple enough that a raw HTTP call is all you need — typically 10–15 lines. The API docs cover all endpoints if you need inbound webhooks, device status, or batch operations beyond what's shown here.

Keep going

Download textbee and send your first PHP SMS in under 5 minutes.