Send SMS with PHP: cURL and Composer
Send SMS from PHP in a few lines: plain cURL or the topsms/topsms-php library, safe retries with an Idempotency-Key and signed delivery webhooks.
Send a POST request with cURL to the TopSMS API, one request per SMS, or install topsms/topsms-php with Composer and call $client->send(). Add an Idempotency-Key for safe retries and verify the X-TopSMS-Signature header on delivery webhooks.
What you need
- A free TopSMS account; you get €0.50 free credit after confirming your mobile number.
- An API key (client ID and secret) from the dashboard under API. Store it in environment variables, not in the code.
- PHP 7.4 or newer with
ext-curlandext-json.
Numbers go in international format (+49…, +43…). The sender can be your company name with up to 11 characters; German and Austrian networks deliver it without registration.
Option 1: plain cURL, no dependencies
<?php
$ch = curl_init('https://www.topsms.cz/api/sms/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('TOPSMS_CLIENT_ID') . ':' . getenv('TOPSMS_SECRET'),
'Content-Type: application/json',
'Idempotency-Key: order-1024-shipped',
],
CURLOPT_POSTFIELDS => json_encode([
'to' => '+4915112345678',
'text' => 'MyShop: Your order 1024 has shipped.',
'from' => 'MyShop',
]),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($body, true);
if ($code !== 200) {
error_log("SMS failed ($code): " . ($result['error'] ?? $body));
}The response contains id (for the status), smsCount (billed segments) and price. The Idempotency-Key header makes retries safe: if your script times out and sends the same request again with the same key, the SMS is not sent twice. Use one key per business event, for example the order number plus the event.
Option 2: the Composer library
composer require topsms/topsms-php<?php
require 'vendor/autoload.php';
use TopSms\TopSmsClient;
use TopSms\TopSmsException;
$client = new TopSmsClient(getenv('TOPSMS_CLIENT_ID'), getenv('TOPSMS_SECRET'));
try {
$result = $client->send('+4915112345678', 'MyShop: Your order 1024 has shipped.', 'MyShop');
echo "Sent, ID {$result['id']}\n";
$status = $client->status($result['id']); // sent | delivered | failed | expired
$credit = $client->credit();
} catch (TopSmsException $e) {
error_log("SMS failed ({$e->getHttpCode()}): {$e->getMessage()}");
}The library has no further dependencies and throws TopSmsException with the HTTP code: 400 for an invalid number or missing field, 401 for wrong credentials, 402 for insufficient credit, 403 for an inactive account or an IP outside the whitelist, 429 for the rate limit (60 requests per minute per key by default).
Delivery reports by webhook
Polling the status works, but a webhook is cleaner. Set a webhook URL on the API key in the dashboard; every status change arrives as a JSON POST with the header X-TopSMS-Signature, an HMAC-SHA256 of the raw body. Verify it against the raw body, not the parsed JSON:
<?php
require 'vendor/autoload.php';
use TopSms\TopSmsClient;
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TOPSMS_SIGNATURE'] ?? '';
if (!TopSmsClient::verifyWebhookSignature($raw, $signature, getenv('TOPSMS_WEBHOOK_SECRET'))) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// $event['status']: delivered | failed | expired
http_response_code(200);Answer with HTTP 2xx within 5 seconds. Failed deliveries are retried three times (immediately, after 30 seconds, after 2 minutes).
Testing without sending
Create a test key in the dashboard. It returns the same responses as a live key plus "test": true, sends and charges nothing, and simulates the status change and webhook after 2 to 4 seconds. Numbers ending in 0000 simulate a failure, 0001 an expiry. That way the whole integration, webhooks included, is finished before you top up.
Typical pitfalls
- Characters: an emoji or curly quotes switch the message to Unicode and the limit drops from 160 to 70 characters. Umlauts and ß are fine. See SMS length and characters.
- Number format: normalise local numbers like
0151 1234567to+49151…before sending. - Many recipients: do not loop over thousands of single requests;
/api/sms/send-bulktakes up to 500 recipients per request. See Bulk SMS for businesses. - WordPress: for WooCommerce there is a ready plugin, no code needed (Integrations).
What does sending SMS from PHP cost?
The API has no extra fee: an SMS to Germany or Austria costs €0.084 per segment, with no monthly fee, from prepaid credit that does not expire. Full table on the pricing page, the complete API reference in the documentation.