All articles

Send SMS With PHP Using an Android SMS Gateway

A complete PHP tutorial for sending SMS through a paired Android phone with the SharkSMS API - the endpoint, a working cURL script, the real responses, and how to handle them.

PHP source code on a screen, representing sending SMS with PHP

If you want to send SMS with PHP using an Android SMS gateway, you do not need a carrier SMPP account or a paid message-per-send API. With SharkSMS you pair an ordinary Android phone as a gateway and your PHP code posts a small HTTP request to the API; the phone sends the text over its own SIM. This guide walks through the exact request, a complete working script, the real responses the API returns, and what you need in place before a message actually goes out.

What you need before you start

Three things have to be in place for a send to complete. Miss any one and the API will tell you which, as you will see below.

  • An active plan with the SMS service. Sending SMS is gated on your subscription. Without it the API answers with a permission error rather than sending.
  • A paired Android gateway device. You pair a phone once by scanning a QR code in the dashboard; the phone then receives messages to send. Its device id is what you pass as device.
  • An API key (secret) created under Tools in your dashboard. Treat it like a password.

If you have not set up the gateway yet, read how to use an Android phone as an SMS gateway first, then come back here for the code.

How sending works

The API supports two send modes. Device mode (mode=devices) routes the message to a phone you have paired, which sends it over its SIM - this is the your-own-number, your-own-SIM approach. Credits mode (mode=credits) routes through a configured third-party gateway instead. This tutorial uses device mode, which is the reason for pairing an Android phone.

Developer typing on a laptop keyboard
Photo by Pixabay. Pexels.

The endpoint and authentication

Send requests are HTTP POST to /api/send/sms on your SharkSMS site. Authentication is a single POST field, secret, holding your API key. A missing or wrong key is rejected before anything else happens, so you can test your wiring safely without sending a thing.

The core fields for device mode are secret, mode, device, phone (in international E.164 form such as +12025550123), and message. On a dual-SIM phone you can add sim set to 1 or 2.

A complete PHP example

Here is a full script using PHP's built-in cURL. It reads the secret from an environment variable rather than hard-coding it, builds the POST body, and prints the API's message.

<?php
// send-sms.php - send one SMS through a paired Android gateway
$endpoint = "https://your-sharksms-site.com/api/send/sms";
$secret   = getenv("SHARKSMS_SECRET"); // never hard-code the key

$fields = [
    "secret"  => $secret,
    "mode"    => "devices",   // send through a paired Android phone
    "device"  => "1",         // the device id from your dashboard
    "sim"     => 1,           // 1 or 2 on a dual-SIM phone
    "phone"   => "+12025550123",
    "message" => "Hello from PHP via the SharkSMS API",
];

$ch = curl_init($endpoint);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query($fields),
    CURLOPT_TIMEOUT        => 20,
]);

$response = curl_exec($ch);

if ($response === false) {
    exit("Network error: " . curl_error($ch));
}

$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$result = json_decode($response, true);
echo $result["message"] . PHP_EOL;

Running the script: the real response

Run it from the command line with php send-sms.php. What you get back depends on your account state, and it is worth being honest about that. On an instance where the SMS service is not yet enabled on the plan, the API returns HTTP 200 with a JSON body like this:

{"status":403,"message":"Subscription has no permission to use SMS services!","data":false}

That is a real response, not an error in your code - the request was well formed and authenticated, and the API is telling you the account is not cleared to send SMS yet. Enable the SMS service on your plan and the next likely message, if no phone is paired, is Device doesn't exist! with status 404. Pair a device, and the same request returns status 200 with the message queued for the phone to pick up. In other words, you build the request once and the API guides you through the remaining prerequisites.

The responses you will meet, and what they mean

  • 401 Invalid API secret supplied - the secret is wrong or missing.
  • 403 Subscription has no permission to use SMS services - the plan does not include SMS.
  • 404 Device doesn't exist - device does not match a paired phone on your account.
  • 400 Invalid phone number - phone is not a valid number in E.164 form.
  • 200 - accepted and queued; the paired phone sends it and reports the delivery status back.
A person holding an Android smartphone
Photo by Markus Spiske. Pexels.

Parsing the response and handling errors

The API always answers with JSON carrying a numeric status and a human message. Branch on the status rather than the text, which can change:

<?php
$result = json_decode($response, true);

if (!is_array($result) || !isset($result["status"])) {
    // The API always returns JSON; anything else is a transport problem.
    throw new RuntimeException("Unexpected API response: " . $response);
}

if ($result["status"] === 200) {
    // Accepted and queued for the gateway to pick up.
    $messageId = $result["data"]["messageId"] ?? null;
    echo "Queued. Message id: {$messageId}" . PHP_EOL;
} else {
    // 401 bad secret, 403 no permission/plan, 404 device missing, 400 bad input.
    echo "API error {$result['status']}: {$result['message']}" . PHP_EOL;
}

A reusable send function

In a real application you want one place that sends and returns the decoded result, so callers do not repeat the cURL setup:

<?php
function sharksms_send(string $phone, string $message, string $device = "1"): array
{
    $ch = curl_init("https://your-sharksms-site.com/api/send/sms");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_TIMEOUT        => 20,
        CURLOPT_POSTFIELDS     => http_build_query([
            "secret"  => getenv("SHARKSMS_SECRET"),
            "mode"    => "devices",
            "device"  => $device,
            "phone"   => $phone,
            "message" => $message,
        ]),
    ]);

    $body = curl_exec($ch);
    if ($body === false) {
        throw new RuntimeException("cURL error: " . curl_error($ch));
    }
    curl_close($ch);

    $data = json_decode($body, true);
    return is_array($data) ? $data : ["status" => 0, "message" => "Invalid response"];
}

Call it with a number and a message, then check the returned status. Keep the function thin; put retries and logging in the caller so the send path stays easy to read.

A computer terminal showing command-line output
Photo by cottonbro studio. Pexels.

Checking delivery status

Because an Android phone sends the message and then reports back, delivery is not instantaneous within the HTTP call - a 200 means queued, not delivered. The gateway updates each message through queued, pending, sent, and failed states. Store the message id the API returns and poll or receive a webhook to learn the final state, rather than assuming a 200 is the end of the story.

Keeping your API secret safe

The secret authenticates every send, so it must never reach the browser or a public repository. Keep it server-side in an environment variable or a secrets manager, as the examples do with getenv(). If a key leaks, revoke it in the dashboard and issue a new one. Send only over HTTPS so the key is never on the wire in clear text.

Servers in a data center rack
Photo by Sora Shimazaki. Pexels.

Testing your wiring without sending

One quiet benefit of the API's design is that you can validate everything short of the actual send before your plan or gateway is ready. Point the script at your site with a real API key and watch the responses: a 401 means the key is wrong, a clean 403 subscription message means the key is right but the plan needs SMS, and a 400 invalid-number message means your phone formatting is off. Each of those is a step you can fix in code and configuration without a single text leaving a phone. By the time you have an active plan and a paired device, your request builder, JSON parsing, and error handling are already proven. This is why the examples above are worth running now rather than waiting: the request is the hard part to get right, and the API tells you precisely where you stand at each stage.

Sending to more than one recipient

The endpoint sends one message per request, so to reach a list you loop and call it per recipient. Do this considerately. A single SIM has a natural throughput ceiling and carriers watch for bursts that look like spam, so pace your loop rather than firing hundreds of requests back to back. A short pause between sends, and sending during normal daytime hours, keeps delivery healthy. For genuinely large lists, a paced queue on your server that feeds the API steadily is far safer than a tight loop, and it lets you record each message id and retry only the ones that failed. If your volumes grow beyond what one phone can carry, pair a second device and split the list across them, or move that traffic to a credits gateway.

Whatever the volume, respect consent: only text people who opted in, honour opt-out requests, and keep messages relevant. The same rules that keep you compliant also keep your delivery rates high.

Timeouts, retries, and duplicates

Networks fail, so set a sensible CURLOPT_TIMEOUT - twenty seconds is plenty - and decide what a timeout means for your app. The safe assumption is that a request which timed out may still have been received, so blind retries can send the same text twice. If duplicates matter, record that you attempted a given message before you send, and reconcile against the delivery status rather than resending on the first hiccup. When you do retry, back off between attempts instead of hammering the endpoint, and treat a 4xx status as a reason to fix input rather than retry - only transport-level failures are worth retrying at all.

Frequently asked questions

Do I need Composer or an SDK?

No. The examples use only PHP's built-in cURL extension, which ships with standard PHP. A single HTTP POST is all the API needs.

Why did my well-formed request not send?

Almost always a missing prerequisite: the SMS service is not on the plan, or no phone is paired. The message in the JSON response names the exact reason.

Can I send through WhatsApp instead?

Yes - SharkSMS also exposes a WhatsApp send endpoint for accounts that have linked WhatsApp. The request shape differs; see the API overview.

Start sending

Pair a phone, create an API key, and the PHP above sends from your own number. For the full request reference and other endpoints, see what a web service SMS API is and the SharkSMS API page, or read the Python version of this walkthrough. Ready to try it? Create your account and pair a device.

All articles

Comments

No comments yet. Be the first.

Sign in to comment

Comments come from SharkSMS accounts, so you always know who you are reading. Creating one is free and takes a minute.