Delivery webhook
Copy page
When a carrier returns a delivery report, we POST a JSON payload to your
delivery_url:
POST https://your-server.com/webhook/deliveryContent-Type: application/jsonUser-Agent: 23Telecom-Webhooks/1.0{ "message_id": "api_42_1743667200123456789_a3f8b2c1d9e45f67", "recipient": "+14155551234", "sender_id": "MyApp", "status": "DELIVRD", "status_code": "000", "num_parts": 1, "cost": 0.0085, "timestamp": "2026-02-13T10:30:04Z"}Response & retries
Section titled “Response & retries”Your server must respond with HTTP 2xx within 5 seconds. Anything else triggers automatic retries: up to 3 attempts total, with backoff delays of 10s and 30s. Every attempt is logged — see webhook logs.
Payload fields
Section titled “Payload fields”Choose fields when configuring the webhook.
Defaults: message_id, recipient, sender_id, status, status_code,
num_parts, cost, timestamp.
| Field | Description |
|---|---|
message_id | Your message ID (from the send response) |
telecom_message_id | Carrier-assigned ID (deprecated — use message_id) |
recipient | Destination phone number |
sender_id | Sender ID used |
status | DELIVRD, UNDELIV, REJECTD, EXPIRED, UNKNOWN |
status_code | Carrier-specific status code |
num_parts | SMS segment count |
cost | Actual cost from the carrier (may be 0 on free routes) |
timestamp | Delivery time, ISO 8601 UTC |
workspace_id | Opt-in — see workspaces |
Receiver examples
Section titled “Receiver examples”Signature verification shown below is optional but recommended — set a signing secret first (see securing webhooks).
import hmac, hashlibfrom flask import Flask, request
app = Flask(__name__)SECRET = "your_webhook_signing_secret"
@app.route("/webhook/delivery", methods=["POST"])def delivery(): ts = request.headers.get("X-Webhook-Timestamp", "") sig = request.headers.get("X-Webhook-Signature", "") body = request.get_data(as_text=True)
if SECRET and sig: expected = "sha256=" + hmac.new( SECRET.encode(), f"{ts}.{body}".encode(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(sig, expected): return "Bad signature", 401
data = request.get_json() print(f"DLR: {data['message_id']} -> {data['status']}") return "OK", 200const crypto = require('crypto');const express = require('express');const app = express();const SECRET = 'your_webhook_signing_secret';
app.post('/webhook/delivery', express.raw({ type: '*/*' }), (req, res) => { const ts = req.headers['x-webhook-timestamp']; const sig = req.headers['x-webhook-signature']; const body = req.body.toString();
if (SECRET && sig) { const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(`${ts}.${body}`).digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).send('Bad signature'); } }
const data = JSON.parse(body); console.log(`DLR: ${data.message_id} -> ${data.status}`); res.send('OK');});
app.listen(3000);<?php$secret = 'your_webhook_signing_secret';$payload = file_get_contents('php://input');$ts = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
if ($secret && $sig) { $expected = 'sha256=' . hash_hmac('sha256', "$ts.$payload", $secret); if (!hash_equals($expected, $sig)) { http_response_code(401); exit('Bad signature'); }}
$data = json_decode($payload, true);error_log("DLR: {$data['message_id']} -> {$data['status']}");echo 'OK';Checklist before production
Section titled “Checklist before production”- Endpoint is HTTPS and responds 2xx in well under 5 seconds.
- Handler is idempotent — retries can deliver the same event twice.
- Signature verification enabled (securing webhooks).
- Alerting on webhook failures — check delivery logs periodically.