Webhooks (DLR)
Receive real-time delivery report callbacks when message status changes.
Webhooks deliver real-time delivery report (DLR) notifications to your server when a message reaches a terminal state - delivered or failed.
Configure your webhook
Go to Settings → Webhooks in the portal, or use the API:
PUT /api/webhooks{
"dlr_url": "https://yourapp.com/webhooks/sms-dlr"
}The platform will send a POST request to your dlr_url for every delivery status update.
Webhook payload
{
"event": "message.delivered",
"message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "delivered",
"phone": "+256712345678",
"sender_id": "eSMSAfrica",
"segments": 1,
"cost": 35.00,
"error_code": null,
"retry_count": 0,
"delivered_at": "2026-05-02T12:00:05Z",
"failed_at": null
}| Field | Description |
|---|---|
event | message.delivered or message.failed |
message_id | The UUID returned when you sent the message |
status | delivered or failed |
phone | Recipient phone in E.164 format |
sender_id | Sender ID used for this message |
segments | Number of SMS segments sent |
cost | Cost charged for this message |
error_code | Carrier error code, if failed |
retry_count | Number of retry attempts made |
delivered_at | UTC timestamp of delivery (null if failed) |
failed_at | UTC timestamp of final failure (null if delivered) |
Signature verification
Every webhook request is signed with HMAC-SHA256. Verify signatures to ensure requests are from eSMS Africa and not a third party.
Signature header
X-Webhook-Signature: sha256=abc123def456...The header contains sha256= followed by the HMAC-SHA256 hex digest of the raw request body, signed with your webhook signing secret.
Verification
import hashlib
import hmac
def verify_signature(payload: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
# In your webhook handler:
# verify_signature(request.body, request.headers["X-Webhook-Signature"], SIGNING_SECRET)const crypto = require("crypto");
function verifySignature(payload, header, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}function verifySignature(string $payload, string $header, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $header);
}Get your signing secret
GET /api/webhooks{
"dlr_url": "https://yourapp.com/webhooks/sms-dlr",
"mo_url": null,
"signing_secret": "whsec_abc123...",
"is_active": true
}To rotate the secret (e.g. after a suspected leak):
POST /api/webhooks/rotate-secretAfter rotating, update your signing secret in all webhook handlers before the old secret expires. Deliveries will fail verification until you update.
Responding to webhooks
Your endpoint must return HTTP 200 within 15 seconds. If it times out or returns a non-2xx status, the platform will retry with exponential backoff.
| Attempt | Delay |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 30 seconds |
| 3rd retry | 90 seconds |
| 4th retry | 4.5 minutes |
| 5th retry | ~13 minutes |
After all retries are exhausted, the webhook delivery is abandoned for that event.
Example webhook handler
from fastapi import FastAPI, Request, HTTPException
import hashlib, hmac
app = FastAPI()
SIGNING_SECRET = "whsec_your_secret"
@app.post("/webhooks/sms-dlr")
async def dlr_handler(request: Request):
body = await request.body()
header = request.headers.get("X-Webhook-Signature", "")
expected = "sha256=" + hmac.new(SIGNING_SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, header):
raise HTTPException(status_code=401, detail="Invalid signature")
event = await request.json()
message_id = event["message_id"]
status = event["status"]
# Update your database
await db.update_message_status(message_id, status)
return {"received": True}const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.raw({ type: "application/json" }));
const SIGNING_SECRET = "whsec_your_secret";
app.post("/webhooks/sms-dlr", (req, res) => {
const header = req.headers["x-webhook-signature"];
const expected = "sha256=" + crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header))) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body);
// Update your database
updateMessageStatus(event.message_id, event.status);
res.json({ received: true });
});