← All articles

How to integrate an SMS API: step-by-step guide in 5 minutes

9 min readDeutsch

Connect an SMS API to your app: account, API key, first SMS via REST and delivery reports by webhook. Examples in curl, PHP, Node.js and Python, no SDK needed.

TL;DR: registration, an API key and the first SMS sent through the REST API — five minutes in total. This quickstart has code in four languages (curl, PHP, Node.js, Python) and explains authentication, formats and typical errors. No SDK required — any HTTP client will do.

This guide is for developers who want to send their first SMS through the TopSMS API as quickly as possible. It assumes basic knowledge of HTTP and JSON.

1. Registration + API key (2 minutes)

  1. Create an account — you get 10 free SMS for testing
  2. Confirm your phone number with the code we text you
  3. In the dashboard go to API access → “Create key”
  4. Fill in:
    • Name: dev-test (just for you)
    • Scopes: tick send (sending) and read (reading status)
    • IP whitelist: fill in for production, skip for a test
    • Test key: tick it if you want to debug the integration without sending anything. Such a key sends and charges nothing, returns the same responses as a live key (plus "test": true) and simulates delivery reports and webhooks; numbers ending in 0000 simulate a failure, 0001 an expiry. The number +420600000000 in the samples below is only a placeholder — with a live key, use your own number
  5. Click “Create” — you see the clientId and secret (the secret is shown only once, store it)

Authentication is a Bearer token in the form clientId:secret.

2. First SMS with curl (30 seconds)

curl -X POST https://www.topsms.cz/api/sms/send \
  -H "Authorization: Bearer YOUR_CLIENT_ID:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+420600000000",
    "from": "TopSMS",
    "text": "Ahoj z API!"
  }'

Response:

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

Store the id; you use it to look up the delivery status. price is the amount just deducted from your credit and smsCount the number of charged parts. The API reports amounts in CZK, the currency in which credit is held: the 0.98 in the example is the price of one part to a Czech number (€0.040); a part to Germany, Austria or the rest of the EU costs €0.084. The message texts in the samples are placeholders — put your own wording there.

End-to-end latency (your terminal → the recipient's phone) is usually a matter of seconds, not minutes; the exact time depends on the load on the operators' networks.

3. PHP (3 lines)

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

if ($response['ok'] ?? false) {
    echo "SMS odeslána, ID: {$response['id']}";
}

For production we recommend the Guzzle HTTP client for its better error handling. There is also an optional library: composer require topsms/topsms-php.

4. Node.js (native fetch)

const response = await fetch('https://www.topsms.cz/api/sms/send', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.TOPSMS_CLIENT_ID}:${process.env.TOPSMS_SECRET}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: '+420600000000',
    from: 'TopSMS',
    text: 'Ahoj z Node.js!',
  }),
})

const data = await response.json()
if (data.ok) {
  console.log('SMS odeslána, ID:', data.id)
} else {
  console.error('Chyba:', data.error)
}

Node.js 18+ has fetch built in. For older versions use node-fetch or axios.

5. Python (requests)

import os
import requests

response = requests.post(
    'https://www.topsms.cz/api/sms/send',
    headers={
        'Authorization': f'Bearer {os.environ["TOPSMS_CLIENT_ID"]}:{os.environ["TOPSMS_SECRET"]}',
        'Content-Type': 'application/json',
    },
    json={
        'to': '+420600000000',
        'from': 'TopSMS',
        'text': 'Ahoj z Pythonu!',
    },
)

data = response.json()
if data.get('ok'):
    print(f"SMS odeslána, ID: {data['id']}")
else:
    print(f"Chyba: {data.get('error')}")

6. Phone number format

We recommend the E.164 format with a + prefix:

  • +49301234567 (DE)
  • +431234567 (AT)
  • +420600000000 (CZ)
  • +421900123456 (SK)

The API is tolerant and converts these notations to E.164 itself:

  • 0049301234567 (00 instead of +)
  • 49301234567 (country code without +)
  • 600000000 or 600 000 000 — a number without a country code is treated as a Czech number (spaces are removed)

For German and Austrian recipients, therefore, always include the country code: a national format such as 0170 … or 0664 … is not accepted (the same goes for a Slovak number written as 0905 …). The response returns the normalised number in the to field.

7. Sender ID (the from parameter)

  • Without your own sender name: send TopSMS (our default) — fine for a test
  • With your own sender name: send the approved name (e.g. Eshop123). You request the name in the dashboard; German, Austrian and most other European networks deliver alphanumeric sender names without operator registration. Czech and Slovak operators require a one-off registration (€20.58), which we file for you
  • Max 11 characters, letters/digits/hyphen/underscore only
  • Approved but not yet active at the operators: the message goes out as TopSMS and the response contains "senderFallback": true and "requestedFrom": "Eshop123". After activation it is sent under your name with no change to your code.

8. Reading the message status

After sending you have an id. Look up the status:

curl -H "Authorization: Bearer YOUR_CLIENT_ID:YOUR_SECRET" \
  https://www.topsms.cz/api/sms/status/MESSAGE_ID

Response:

{
  "id": "cmp7a1b2c3...",
  "externalId": "a1b2c3d4-...",
  "to": "+420600000000",
  "from": "TopSMS",
  "text": "Ahoj z API!",
  "status": "delivered",
  "price": 0.98,
  "carrier": "230.2",
  "createdAt": "2026-05-19T10:23:42.000Z",
  "deliveredAt": "2026-05-19T10:23:45.000Z",
  "failedAt": null,
  "errorMessage": null,
  "errorCode": null
}

Possible states: pending (being sent right now), queued (in the queue or scheduled), sent, delivered, failed, expired. For failed and expired you find the reason in errorCode (undelivered, rejected, expired, repeated_failure and others) and in errorMessage. carrier is the recipient's network code in MCC.MNC form — in the example 230.2 is O2 Czech Republic (230.1 T-Mobile CZ, 230.3 Vodafone CZ).

Alternatively, set a webhook URL on the API key — the delivery report then arrives as a POST in real time. See Webhooks in the documentation.

9. Typical errors (HTTP status)

StatusReasonFix
401Invalid tokenCheck the Authorization header
400Missing to/text, invalid number or blacklisted numberCheck the number, ideally E.164 with +
403Unapproved sender ID in fromUse an approved name, or omit from (default TopSMS)
402Insufficient creditTop up in the dashboard
403Scope send missingEdit the API key, tick send
429Rate limit of the key (default 60 requests/min)Wait according to the Retry-After header, or raise the limit in the dashboard (up to 1000)
500 record_failedThe SMS was sent but not recorded, credit refundedDo not retry, write to support
500 otherUnexpected errorRetry with the same Idempotency-Key
502The operator did not accept the message, credit refundedTry again later

10. Recommended production architecture

[Your application]
      ↓ POST /api/sms/send
[TopSMS API]
      ↓ async
[Mobile networks]
      ↓
[Recipient's phone]
      ↓ delivery report
[TopSMS API]
      ↓ POST webhook
[Your application]
      ↓ update DB

Key principles:

  1. Idempotency: send an Idempotency-Key header (an order ID, for instance). A repeated request then does not send the SMS a second time — see the API documentation
  2. Webhook signature: validate the HMAC signature (configurable per API key)
  3. Rate-limit budget: plan the daily volume around peaks with a 30 % reserve
  4. Fallback: if the credit falls below a threshold, use a backup route (another channel)

11. Security — what NOT to do

  • Never put the API key in the frontend (JavaScript in the browser) — anyone can steal it. Call the API from the server only.
  • Never commit it to git — use a .env file plus .gitignore.
  • Do not share the secret — not in Discord, Slack or screenshots. If it leaks, issue a new one immediately and revoke the old one.
  • Do not test on random numbers — +420600000000 in the samples is only a placeholder; for experiments use a test key, which sends and charges nothing.

12. What next

Once you get the first successful id in a response, your integration works. Next steps:

  • Your own sender name — request it in the dashboard; for Czech and Slovak networks the operators approve new names in batches roughly once a month (allow about 3 weeks); one-off fee €20.58
  • Webhooks — push delivery reports with HMAC signature
  • Bulk sending — /api/sms/send-bulk takes up to 500 recipients in one request, see bulk sending
  • Connecting a shop — for WooCommerce use the official plugin, other platforms through the REST API or webhooks: integrations

For higher volumes (from 20,000 SMS a month) we set up an SMPP connection individually on request.

Related articles