SMS API documentation

The TopSMS SMS API: add SMS sending to your application in a few minutes. A simple HTTP/JSON API: one POST sends a message, one GET returns the delivery status. Authentication with an API key, no SDK and no dependencies — the HTTP client your language already has is enough.

€0.084 per SMS to Germany, Austria and the rest of the EU, from €0.036 to Czech and Slovak networks · free test key · 10 free SMS to start. Prices exclude VAT.

Get started in two minutes

One HTTP request and the SMS is on its way. No SDK, no dependencies. If this is your first SMS integration, see the step-by-step guide How to integrate an SMS API in 5 minutes.

curl -X POST https://www.topsms.cz/api/sms/send \
  -H "Authorization: Bearer CLIENT_ID:SECRET" \
  -H "Content-Type: application/json" \
  -d '{"to":"+420600000000","text":"Vas overovaci kod je 482910","from":"MojeFirma"}'

Replace CLIENT_ID:SECRET with your API key from the dashboard. Further down this page: code samples in six languages, delivery status, webhooks and error codes.

Get an API key →

Basics: endpoint and authentication

The API is plain HTTP/JSON over HTTPS. The production base URL is https://www.topsms.cz. You need no library or SDK — the HTTP client you already have (curl, Guzzle, requests, fetch) is enough.

Authentication uses an API key in the Authorization header. The key is a pair of clientId and secret, generated in the dashboard under API access (the secret is stored only as a hash, so save it when you create the key):

Authorization: Bearer <clientId>:<secret>
Content-Type: application/json

Each key has permissions (scope) — send for sending and read for reading status — and optionally an IP whitelist that restricts the key to the specific addresses of your server.

Sending an SMS

POST https://www.topsms.cz/api/sms/send — requires scope send.

Request body (JSON):

FieldRequiredDescription
toyesPhone number in international format (e.g. +420600000000). We support more than 200 countries and territories — the price depends on the destination, see the pricing.
textyesMessage text in UTF-8. Diacritics switch the encoding to Unicode (70/67 characters per part) — the SMS length calculator counts parts and price. The text length is not limited; a longer message is split and charged per part.
fromnoSender name (Sender ID), max 11 characters. It must be an approved Sender ID of your account; when omitted, the account's default sender is used (Dashboard → Sender ID → “Set as default”), otherwise “TopSMS”. An approved name that the operators have not yet activated is replaced by the default (see senderFallback below).

The text is sent exactly as you submit it — the API does not substitute variables such as {name}. Personalisation (variables, salutations, values from CSV) is done by the campaign module in the dashboard; when sending through the API, your application fills in the values before the call.

Request:

POST /api/sms/send
{
  "to": "+420600000000",
  "text": "Vas overovaci kod je 482910",
  "from": "MojeFirma"
}

Successful response (HTTP 200):

{
  "ok": true,
  "id": "cmp7a1b2c3...",
  "externalId": "a1b2c3d4",
  "to": "+420600000000",
  "from": "MojeFirma",
  "smsCount": 1,
  "price": 0.9,
  "status": "sent"
}

id is the internal message identifier (use it to query the status), externalId is the ID at the operator, smsCount is the number of charged parts and price is the amount deducted from your credit at that moment. The API reports all amounts in CZK, the currency in which credit is held (see Credit balance).

Inactive Sender ID. When the sender in the from field is approved but the operators have not activated it yet, the message is sent under the default name “TopSMS” and the response contains "from": "TopSMS", "senderFallback": true and "requestedFrom": "MojeFirma". The same applies to bulk sending. After activation, messages go out under your name with no change on your side.

Error 500 with code record_failed means the SMS was sent but could not be recorded in our system. The credit has been refunded. Do not retry the message; write to us. With an Idempotency-Key header a retry is safe: you receive the same response and the SMS is not sent again.

Bulk sending (several recipients at once)

POST https://www.topsms.cz/api/sms/send-bulk — requires scope send. Up to 500 recipients per request.

The same text to several recipients:

POST /api/sms/send-bulk
{
  "to": ["+420600000000", "+420600000001"],
  "text": "Zitra mame zavreno.",
  "from": "MojeFirma"
}

Or a different text for each recipient — your application prepares the personalisation:

POST /api/sms/send-bulk
{
  "messages": [
    { "to": "+420600000000", "text": "Pavle, faktura 2026001 je po splatnosti." },
    { "to": "+420600000001", "text": "Karlo, faktura 2026002 je po splatnosti." }
  ],
  "from": "MojeFirma"
}

The response contains a result for each recipient separately, in the same order in which you sent them:

{
  "ok": true,
  "total": 2,
  "accepted": 1,
  "rejected": 1,
  "price": 0.98,
  "results": [
    { "to": "+420600000000", "status": "sent", "id": "cmp7a1b2...", "externalId": "a1b2c3d4", "price": 0.98, "error": null },
    { "to": "+420600000001", "status": "failed", "id": null, "externalId": null, "price": 0, "error": "Invalid phone number. Use international format (+420…)." }
  ]
}

Only messages with status sent are charged. Invalid numbers, blacklisted numbers and messages rejected by the operator are not deducted from the credit. The credit is checked for the whole batch in advance — if it is insufficient, nothing is sent and we return code 402.

You get the delivery status of each message by its id, as for a single send, or by webhook.

Large batches run asynchronously. Above 50 recipients (or when you send "async": true) the API responds immediately with 202 Accepted and a jobId, the credit is deducted in advance and our queue handles the sending within seconds to minutes. Track the progress with GET /api/sms/jobs/{jobId} (scope read): status queued / scheduled / running / done / failed / cancelled, counters sent / failed / queued and per-message results (?results=0 returns only the counters). Messages rejected by the gateway are refunded to the credit automatically; delivery reports arrive by webhook just as with synchronous sending.

HTTP/1.1 202 Accepted
{
  "ok": true,
  "queued": true,
  "jobId": "cmuk6llct0003yqlx8ke6rdt6",
  "statusUrl": "https://www.topsms.cz/api/sms/jobs/cmuk6llct0003yqlx8ke6rdt6",
  "total": 120, "accepted": 118, "rejected": 2, "price": 115.64,
  "results": [ { "to": "+420600000000", "status": "queued", "id": "cmuk...", "price": 0.98, "error": null }, ... ]
}

Scheduled sending (send_at)

Add the send_at field to /api/sms/send or /api/sms/send-bulk and the message is sent at the given time. Useful for appointment reminders, morning mailings or messages outside quiet hours.

{
  "to": "+420601234567",
  "text": "Pripominame zitrejsi termin v 10:00.",
  "send_at": "2026-10-01T09:00:00+02:00"
}

The response is 202 with status scheduled and a jobId:

{ "ok": true, "scheduled": true, "id": "cmv...", "jobId": "cmw...", "status": "scheduled",
  "sendAt": "2026-10-01T07:00:00.000Z", "price": 0.98,
  "statusUrl": "https://www.topsms.cz/api/sms/jobs/cmw..." }

Idempotency (Idempotency-Key)

During a network outage you do not know whether the request went through. Send it again with the same Idempotency-Key header and the SMS is not sent twice: you receive the original response with the header Idempotent-Replayed: true.

curl -X POST https://www.topsms.cz/api/sms/send \
  -H "Authorization: Bearer CLIENT_ID:SECRET" \
  -H "Idempotency-Key: 5f0c7a1e-9d2b-4c55-8f7e-2b1d3c4e5a6f" \
  -H "Content-Type: application/json" \
  -d '{"to": "+420601234567", "text": "Objednavka 1234 odeslana."}'

Test mode

When creating an API key in the dashboard, tick Test key. Such a key sends and charges nothing but returns the same responses as a live key (plus "test": true) and, after 2 to 4 seconds, changes the message status and sends a delivery report with "test": true to the webhook. The only exception is scheduled sending: a test key with send_at returns 200 with "scheduled": true and no jobId. You can also read the status via /api/sms/status/{id}.

Request and webhook log

In the dashboard, under API access, open Log and webhook for each key: you see the last 100 calls (time, endpoint, status code, error, request body) and webhook delivery attempts including your server's response. In the same place you set or change the webhook URL and send a test webhook. The log is kept for 30 days; the secret and the Authorization header are not stored.

Verify API: verification codes

For login, registration or confirming actions: POST /api/verify/send sends a code, POST /api/verify/check verifies it. We generate and guard the code (validity, 5 attempts, per-number limits). Details and examples on the Verify API page; a step-by-step guide is in OTP via SMS: implementation guide.

POST /api/verify/send
{ "to": "+420601234567", "brand": "MojeFirma" }
→ { "ok": true, "id": "cmv...", "status": "pending", "expiresAt": "...", "price": 0.98 }

POST /api/verify/check
{ "id": "cmv...", "code": "482913" }
→ { "valid": true, "status": "approved" }

Delivery status (delivery report)

GET https://www.topsms.cz/api/sms/status/{id} — requires scope read.

For {id} use either the internal id or the externalId from the send response. We recommend id, because externalId changes when a message to Germany is retried automatically. The delivery report from the operator arrives asynchronously (typically within a few seconds to minutes) and the status is then updated. The recommended approach is short polling — after sending, query the status from time to time.

{
  "id": "cmp7a1b2c3...",
  "externalId": "a1b2c3d4",
  "to": "+420600000000",
  "from": "MojeFirma",
  "status": "delivered",
  "price": 0.9,
  "carrier": "230.1",
  "createdAt": "2026-08-10T09:12:00.000Z",
  "deliveredAt": "2026-08-10T09:12:03.000Z",
  "failedAt": null,
  "errorMessage": null,
  "errorCode": null
}

carrier is the recipient's network code in the form MCC.MNC, for example 230.1 T-Mobile CZ, 230.2 O2 CZ, 230.3 Vodafone CZ, 231.6 O2 Slovakia.

Values of the status field:

For a long message made up of several parts the status is delivered only after all parts have been delivered. If any part does not arrive, the message ends as failed, because the handset cannot reassemble it.

Reason for non-delivery in the errorCode field (the same field is present in the webhook and in batch results):

expiredValidity expired; the handset was switched off or out of coverage the whole time. You can try sending later.
undeliveredThe operator returned the message as undeliverable: the number does not exist, is not active or the handset does not accept SMS. Some foreign networks return a temporarily unavailable handset (switched off, no signal) this way instead of waiting; for important messages we recommend retrying after a few minutes with the same Idempotency-Key extended by the attempt number.
rejectedThe operator rejected the message (the recipient blocks SMS from companies, the sender or the content). It does not mean a wrong number.
deletedThe operator cancelled the message before delivery.
unknownThe operator did not return a delivery result.
repeated_failureDelivery to the same number failed for at least the third time in 90 days. The number probably does not exist; we recommend removing it from your list.
not_sentThe message could not be handed over to the operator; the price was refunded to the credit.

Webhooks (push delivery reports)

Don't want to poll? Set a webhook URL for your API key in the dashboard — when a message is delivered, fails or expires, we send an HTTP POST:

{
  "event": "sms.status",
  "id": "cmp7a1b2c3...",
  "externalId": "a1b2c3d4",
  "to": "+420600000000",
  "from": "MojeFirma",
  "status": "delivered",
  "price": 0.9,
  "deliveredAt": "2026-09-07T09:12:03.000Z",
  "failedAt": null,
  "errorMessage": null,
  "errorCode": null,
  "sentAt": "2026-09-07T09:12:04.100Z"
}

Signature: header X-TopSMS-Signature: sha256=HMAC-SHA256(body, webhookSecret) — we show the secret when you create a key with a webhook URL. Verify the signature over the raw request body, not over the parsed JSON. Delivery: 3 attempts (immediately, +30 s, +2 min), 5 s timeout, we expect HTTP 2xx. Details: Webhooks.

No-code integrations and plugins

Don't want to call the API yourself? Most common connections are ready-made or can be set up without programming:

An overview of all integrations and their status is on the Plugins and integrations page.

Libraries, OpenAPI and AI assistants

PHP: composer require topsms/topsms-php — github.com/topsms-ghb/topsms-php (sending, status, credit and webhook signature verification). A machine-readable description of the API for import into Postman/Insomnia: OpenAPI specification.

AI assistants: the TopSMS MCP server lets Claude, ChatGPT and other agents send SMS and read delivery status, and llms.txt is a plain-text summary of the API for coding assistants.

Credit balance

GET https://www.topsms.cz/api/credit — requires scope read.

Returns the current balance and your price per SMS. Useful for automatic credit monitoring — for example an alert in your system when the balance drops below a week's consumption.

{
  "ok": true,
  "credit": 1482.96,
  "currency": "CZK",
  "smsPrice": 0.98
}

Amounts in the API are in CZK, the currency in which credit is held. In this example the balance corresponds to €61.03 and the price per SMS to €0.040.

Error codes

An error returns JSON { "error": "description" } with the corresponding HTTP code and, for most errors, a machine-readable "code" field as well. The exception is a bulk send in which all numbers are invalid: it returns 400 with a results field (reason for each recipient) instead of error. The Verify API endpoints also add a machine-readable "code" (for example rate_limited, cooldown, insufficient_credit, invalid_number, not_found) that makes the error easy to handle in code.

HTTPMeaning
202Not an error: a bulk batch above 50 recipients (or with "async": true) was accepted into the queue; track progress with GET /api/sms/jobs/{jobId}.
400Missing to or text, invalid number, blacklisted number (opted-out contact), invalid send_at (invalid_send_at) or invalid Idempotency-Key format (invalid_idempotency_key).
401Missing or invalid authentication (check the Bearer clientId:secret format).
402Insufficient credit on the account.
403Account not active/approved, IP outside the whitelist, key lacks the required scope, or an unapproved Sender ID in the from field.
404Message not found (status query).
409Conflict: a request with the same Idempotency-Key is still running, or the job can no longer be cancelled (not_cancellable). Jobs created by a campaign are cancelled in the dashboard (campaign_job).
422The Idempotency-Key was already used with a different request (idempotency_mismatch).
429Rate limit of the key exceeded, or for the Verify API more than 5 codes to one number per hour (rate_limited) or another code sooner than 30 s (cooldown, field retryAfter in seconds). Every 429 response has a Retry-After header with the number of seconds until the next attempt.
500record_failed: the SMS was sent but could not be recorded; the credit has been refunded. Do not retry, write to us. Retry other 500 errors with the same Idempotency-Key.
502The operator did not accept the message; the credit was refunded and you can try again. For the Verify API an unapproved sender or a blacklisted number also ends this way (send_failed).

Message length and price

The price is calculated from the number of message parts (segments). The limit for one SMS depends on whether the text contains diacritics or other non-GSM characters:

The send response returns smsCount (number of charged parts) and price (according to your tariff), so you know the cost immediately. Per part: €0.084 to Germany, Austria and the rest of the EU; €0.036–0.040 to Czech and Slovak networks depending on monthly volume; other countries as per the pricing. Details of the calculation are in the article How many characters fit in an SMS.

Line breaks: send a line break in the text field as a newline character, written in JSON as \n (for example "Dobrý den,\nobjednávka je připravena."). We send the message exactly as you submit it; each line break counts as one character.

Sending an SMS: curl, PHP, Python, Node.js, C#, Java

In all samples, replace CLIENT_ID:SECRET with your key.

curl

curl -X POST https://www.topsms.cz/api/sms/send \
  -H "Authorization: Bearer CLIENT_ID:SECRET" \
  -H "Content-Type: application/json" \
  -d '{"to":"+420600000000","text":"Ahoj","from":"MojeFirma"}'

PHP (cURL)

$ch = curl_init('https://www.topsms.cz/api/sms/send');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer CLIENT_ID:SECRET',
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'to'   => '+420600000000',
    'text' => 'Ahoj z PHP',
    'from' => 'MojeFirma',
  ]),
]);
$response = curl_exec($ch);
echo $response;

Python (requests)

import requests

r = requests.post(
    "https://www.topsms.cz/api/sms/send",
    headers={"Authorization": "Bearer CLIENT_ID:SECRET"},
    json={"to": "+420600000000", "text": "Ahoj z Pythonu", "from": "MojeFirma"},
)
print(r.json())

Node.js (fetch)

const res = await fetch("https://www.topsms.cz/api/sms/send", {
  method: "POST",
  headers: {
    "Authorization": "Bearer CLIENT_ID:SECRET",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ to: "+420600000000", text: "Ahoj z Node", from: "MojeFirma" }),
})
console.log(await res.json())

C# (.NET 6+, HttpClient)

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "CLIENT_ID:SECRET");
var res = await http.PostAsJsonAsync("https://www.topsms.cz/api/sms/send",
    new { to = "+420600000000", text = "Ahoj z .NET", from = "MojeFirma" });
Console.WriteLine(await res.Content.ReadAsStringAsync());

Java (11+, java.net.http)

var req = HttpRequest.newBuilder(URI.create("https://www.topsms.cz/api/sms/send"))
    .header("Authorization", "Bearer CLIENT_ID:SECRET")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"to\":\"+420600000000\",\"text\":\"Ahoj z Javy\",\"from\":\"MojeFirma\"}"))
    .build();
var res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
More worked examples. Six real-world scenarios in all six languages — order confirmation with Idempotency-Key, login code with the Verify API, appointment reminder with send_at, personalised invoice reminders in bulk, polling the delivery status and receiving a webhook with signature verification — are on the Czech documentation page: Examples from practice. The code is language-neutral; only the comments are in Czech.

Limits, IP whitelist and Sender ID

Create a free account

10 test SMS to start, no credit card and no monthly fee. Questions about the API: info@topsms.cz.

Česky · Back to the home page