Webhooky v SMS API: praktický průvodce
Jak fungují delivery webhooky, co posílají, jak je v aplikaci přijmout a zabezpečit. Příklady v PHP, Node.js a Pythonu + nejčastější integrace.
Webhook je HTTP POST z naší strany na vaši URL, kdykoliv se něco stane s SMS, kterou jste odeslali — typicky když operátor potvrdí doručení nebo chybu. Bez webhooku byste museli pollovat stav každé SMS (zbytečně), s webhookem se to dozvíte v reálném čase.
V tomto článku ukážu, jak webhook přijmout a zpracovat s příklady v 3 jazycích, a jak zajistit, že je bezpečný.
Co webhook obsahuje
Když pošlete SMS přes API, dostanete externalId. Když se SMS doručí (nebo selže), na vaši webhook URL přijde POST:
{
"externalId": "msg-12345",
"to": "+420608030884",
"status": "delivered",
"deliveredAt": "2026-05-08T14:23:45Z",
"operatorCost": 0.028,
"carrier": "230.2"
}
Možné status hodnoty:
| Status | Význam |
|---|---|
sent | Předáno operátorovi |
delivered | Doručeno na telefon |
failed | Selhalo (špatné číslo, vypnuto, atd.) |
expired | Vypršelo (telefon byl nedostupný 24h+) |
rejected | Odmítnuto operátorem |
carrier je MCC.MNC code operátora (230.1 = T-Mobile CZ, 230.2 = O2 CZ, 230.3 = Vodafone CZ).
Jak nastavit webhook URL
V dashboardu API klíčů u svého API klíče vyplňte Webhook URL:
https://muj-eshop.cz/api/topsms-webhook
Pak vám každá delivery report přistane na tuto URL.
Přijem webhooku — PHP
<?php
// /api/topsms-webhook.php
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);
if (!$data) {
http_response_code(400);
exit('Invalid JSON');
}
// Aktualizace v DB
$pdo = new PDO('mysql:host=localhost;dbname=eshop', 'user', 'pass');
$stmt = $pdo->prepare(
'UPDATE messages SET status = ?, delivered_at = ?, operator_cost = ? WHERE external_id = ?'
);
$stmt->execute([
$data['status'],
$data['deliveredAt'] ?? null,
$data['operatorCost'] ?? null,
$data['externalId'],
]);
http_response_code(200);
echo 'OK';
Přijem webhooku — Node.js (Express)
// /api/topsms-webhook.js
const express = require('express')
const app = express()
app.use(express.json())
app.post('/api/topsms-webhook', async (req, res) => {
const { externalId, status, deliveredAt, operatorCost } = req.body
await db.message.update({
where: { externalId },
data: { status, deliveredAt, operatorCost }
})
res.status(200).send('OK')
})
app.listen(3000)
Přijem webhooku — Python (Flask)
from flask import Flask, request, jsonify
import sqlite3
app = Flask(__name__)
@app.route('/api/topsms-webhook', methods=['POST'])
def webhook():
data = request.get_json()
if not data:
return 'Invalid JSON', 400
conn = sqlite3.connect('eshop.db')
conn.execute(
'UPDATE messages SET status = ?, delivered_at = ?, operator_cost = ? WHERE external_id = ?',
(data['status'], data.get('deliveredAt'), data.get('operatorCost'), data['externalId'])
)
conn.commit()
conn.close()
return 'OK', 200
if __name__ == '__main__':
app.run(port=3000)
Bezpečnost — proč webhook URL musí být tajná
Pokud někdo zná vaši webhook URL, může na ni posílat falešné delivery reporty. Vy si pak myslíte, že SMS byla doručena, ale nebyla.
Tři vrstvy ochrany:
1. URL s tajemstvím
Místo /api/topsms-webhook použijte /api/topsms-webhook?secret=N0km7BGj5eWj8HLxyRBuxeiC4_CjYuim. V aplikaci ověřte secret:
if ($_GET['secret'] !== 'N0km7BGj5eWj8HLxyRBuxeiC4_CjYuim') {
http_response_code(403);
exit;
}
2. IP whitelist
Naše webhook služby posílají z konkrétních IP. V dashboardu najdete aktuální seznam. Ve své aplikaci povolte jen tyto IP:
$allowedIPs = ['142.93.207.11', '142.93.207.12'];
if (!in_array($_SERVER['REMOTE_ADDR'], $allowedIPs)) {
http_response_code(403);
exit;
}
3. HMAC signature (pokročilé)
Pokud potřebujete nejvyšší úroveň zabezpečení, vyžádejte si HMAC podpis. Každý webhook bude mít hlavičku X-TopSMS-Signature se SHA-256 podpisem payloadu. Vy si ji v aplikaci ověříte:
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_TOPSMS_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, 'your-shared-secret');
if (!hash_equals($expected, $sig)) {
http_response_code(403);
exit;
}
HMAC mode si můžete aktivovat per API klíč (kontaktujte podporu).
Retry logika — co dělat, když webhook selže
Pokud vaše endpoint vrátí HTTP 5xx, opakujeme webhook 3× s exponential backoff:
| Pokus | Latence |
|---|---|
| 1 | T+0 |
| 2 | T+30s |
| 3 | T+5min |
| 4 | T+30min |
Po 4 neúspěšných pokusech webhook vyhodíme a delivery report najdete pouze přes GET /api/sms/status/.
Doporučení:
- Vraťte HTTP 200 OK rychle — webhook handler nesmí trvat víc než 5 sekund.
- Async zpracování — uložte payload do queue (Redis, RabbitMQ) a hned vraťte 200. Skutečné zpracování dělejte na pozadí.
Idempotence — jak nezpůsobit duplikáty
Můžeme webhook poslat 2× (pokud první přišel, ale vy jste odpověděli pomalu a my retryovali). Vaše aplikace by měla:
# Check, jestli jsme webhook už zpracovali
existing = db.message.find_one({'externalId': data['externalId'], 'status': data['status']})
if existing:
return 'OK', 200 # Idempotent: nic neudělat, jen vrátit 200
externalId + status kombinace je unikátní per událost.
Testování webhooku
V dev prostředí těžko zatestujete webhook na svém localhost. Použijte tunneling službu:
ngrok (free)
ngrok http 3000
# Vrátí: https://abc123.ngrok-free.app
Nastavte v dashboardu webhook URL na https://abc123.ngrok-free.app/api/topsms-webhook. Při testovacím SMS dostanete webhook na localhost.
localtunnel (alternativa)
npx localtunnel --port 3000
Webhook.site (testovací bin)
webhook.site — dostanete unikátní URL, kam můžeme posílat webhooky, a vidíte přijaté payloads ve webovém UI. Skvělé pro debug.
Časté chyby
1. Pomalá odpověd
Pokud webhook handler dělá pomalou operaci (volá další API, generuje PDF…), retry hází duplikáty. Vraťte 200 rychle, zpracujte později.
2. Bez ověření autenticity
Pokud webhook nemá ani secret, IP whitelist ani HMAC, kdokoliv může poslat falešný delivery report a zničit vaše statistiky.
3. Chybějící idempotence
Pokud na duplikátní webhook reagujete duplikátně (např. odešlete e-mail klientovi 2×), je to UX katastrofa. Vždy check existing record.
4. Webhook pro každou událost
Nepotřebujete webhook pro každý status (sent, delivered, failed, expired). Zpravidla stačí jen delivered a failed. Jinde nevíte, co dělat. V API klíčích si můžete v budoucnu vybrat, jaké stavy chcete.