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.
- Free test key: sends and charges nothing, simulates delivery reports and webhooks. A live key comes with 10 test SMS after registration.
- €0.084 per SMS to Germany, Austria and the rest of the EU, from €0.036 to Czech and Slovak networks; no monthly fee and no API fees. Other countries follow the price zones on the pricing page.
- Verification codes (OTP, 2FA): the ready-made Verify API both sends and checks the code, including limits against abuse.
- Delivery reports by webhook, bulk and scheduled sending, Idempotency-Key, OpenAPI, MCP server for AI assistants.
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):
| Field | Required | Description |
|---|---|---|
to | yes | Phone number in international format (e.g. +420600000000). We support more than 200 countries and territories — the price depends on the destination, see the pricing. |
text | yes | Message 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. |
from | no | Sender 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..." }- Format: ISO 8601 with a time zone (
+02:00orZ), or a Unix timestamp. At most 30 days ahead; a time less than 1 minute from now is sent immediately. - The credit is deducted when the message is scheduled. Track the status with
GET /api/sms/jobs/{jobId}. - Cancellation:
DELETE /api/sms/jobs/{jobId}(scopesend) cancels a scheduled send or a queued batch that has not started sending yet. The price is refunded to the credit. If sending is already in progress, it returns409withcode: "not_cancellable".
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."}'- Applies to
/api/sms/send,/api/sms/send-bulkand/api/verify/send. The key is 1–255 printable ASCII characters without spaces (we recommend a UUID or an order ID), otherwise we return400(invalid_idempotency_key); it is valid for 24 hours and bound to your API key. - The same key with a different body or endpoint returns
422(idempotency_mismatch); if the original request is still running,409(idempotency_in_progress). - Errors
401,402,403,408,409,429and5xxare not stored, so after removing the cause you can retry with the same key. The exception is500with coderecord_failed(the SMS was sent, only not recorded), which we remember so that a retry does not send the SMS a second time. - Other responses, including
400and404, are stored. After fixing an invalid request, therefore, use a new key.
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}.
- Numbers ending in
0000simulate a failure (invalid number),0001expiry, all others delivery - Everything else is checked: number format, blacklist, sender approval, key permissions
- The
pricefield shows what the message would cost; the credit does not change
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:
pending— accepted, being sent right now (a fraction of a second)queued— in the queue or waiting for the scheduled send timesent— accepted and handed over to the operatordelivered— delivered to the handset (seedeliveredAt)failed— not delivered (reason inerrorMessageanderrorCode, time infailedAt)expired— validity expired, the operator did not deliver the message
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):
expired | Validity expired; the handset was switched off or out of coverage the whole time. You can try sending later. |
undelivered | The 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. |
rejected | The operator rejected the message (the recipient blocks SMS from companies, the sender or the content). It does not mean a wrong number. |
deleted | The operator cancelled the message before delivery. |
unknown | The operator did not return a delivery result. |
repeated_failure | Delivery 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_sent | The 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:
- WooCommerce: the official plugin sends SMS about order status and new orders; installation from a ZIP in two minutes.
- Fakturoid and iDoklad: SMS reminders for overdue invoices are set up in the dashboard, no code.
- Shoptet: an add-on is in preparation; until its release, connect via Make or this API.
- Make: the official TopSMS app with modules for sending, bulk sending, message status, credit and a delivery-report trigger, no code.
- Zapier, n8n: an HTTP module calls
POST /api/sms/sendwith the headerAuthorization: Bearer clientId:secret, see the samples below. - CSV import: send one-off and marketing messages from a campaign with variables from your system's export.
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.
| HTTP | Meaning |
|---|---|
202 | Not 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}. |
400 | Missing to or text, invalid number, blacklisted number (opted-out contact), invalid send_at (invalid_send_at) or invalid Idempotency-Key format (invalid_idempotency_key). |
401 | Missing or invalid authentication (check the Bearer clientId:secret format). |
402 | Insufficient credit on the account. |
403 | Account not active/approved, IP outside the whitelist, key lacks the required scope, or an unapproved Sender ID in the from field. |
404 | Message not found (status query). |
409 | Conflict: 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). |
422 | The Idempotency-Key was already used with a different request (idempotency_mismatch). |
429 | Rate 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. |
500 | record_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. |
502 | The 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:
- Without diacritics (GSM-7): 160 characters per 1 SMS; for longer messages 153 characters per part.
- With diacritics or emoji (Unicode): 70 characters per 1 SMS; for longer messages 67 characters per part.
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());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
- Rate limit — set per key (default 60 requests/min, adjustable in the dashboard up to 1000). A bulk request counts as one request even when it carries up to 500 messages.
- IP whitelist — optionally allow only the specific IP addresses of your server for a key. A request from another IP gets 403.
- Sender ID — your own sender name instead of a number (the
fromfield). Operators approve new senders in batches roughly once a month (allow about 3 weeks); one-off fee €20.58. The API accepts only approved Sender IDs of your account (or the default “TopSMS”) — an unapproved name returns 403. An approved name that the operators have not yet activated is replaced by the default “TopSMS” and the response marks this with thesenderFallbackfield. Details on the Custom Sender ID page (Czech). - Blacklist — numbers marked as opted out (manually in the contact detail or in bulk by importing a CSV with an optOut column) are automatically rejected by the API with code 400. You manage the opt-out on your side; we respect it.
- Number format — we recommend E.164 (
+420…). The API also accepts00420…, a country code without the plus sign and a Czech number without a country code; the response returns the normalised number in thetofield. - Retries to Germany — some German networks return “undelivered” immediately when the handset is switched off. In the account settings you can enable automatic retries (up to 6 attempts within 8 hours); each attempt is charged as a new SMS and you see the number of attempts and their price in the history. The
externalIdfield then belongs to the last attempt. - Keys and scope — managed in the dashboard (API access section): creation,
send/readpermissions, IP whitelist, webhook URL, call log and revocation. A test key (ticked on creation) sends and charges nothing. - Verify API — a code is valid for 60 to 1800 s (default 5 min), 5 attempts per code, at most 5 codes per number per hour and 30 s between two codes. A new code to the same number invalidates the previous one.
- Several keys and cost allocation — an account can have up to 10 active keys with their own names (e.g. per project or cost centre). Every sent SMS is assigned to a specific key.
- Source tracking — in the History every message has a Source column (“API · key name”, “Campaign”, “Manual”); you can filter by it and it is part of the CSV export — ideal for internal cost allocation.
10 test SMS to start, no credit card and no monthly fee. Questions about the API: info@topsms.cz.