eSMS AfricaeSMS Africa
Delivery

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:

Endpoint
PUT /api/webhooks
Request body
{
  "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

POST to your dlr_url
{
  "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
}
FieldDescription
eventmessage.delivered or message.failed
message_idThe UUID returned when you sent the message
statusdelivered or failed
phoneRecipient phone in E.164 format
sender_idSender ID used for this message
segmentsNumber of SMS segments sent
costCost charged for this message
error_codeCarrier error code, if failed
retry_countNumber of retry attempts made
delivered_atUTC timestamp of delivery (null if failed)
failed_atUTC 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

Request 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

verify_webhook.py
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)
verifyWebhook.js
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));
}
verify_webhook.php
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

Endpoint
GET /api/webhooks
Response 200
{
  "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):

Endpoint
POST /api/webhooks/rotate-secret

After 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.

AttemptDelay
1st retry10 seconds
2nd retry30 seconds
3rd retry90 seconds
4th retry4.5 minutes
5th retry~13 minutes

After all retries are exhausted, the webhook delivery is abandoned for that event.

Example webhook handler

webhook_handler.py
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}
webhookHandler.js
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 });
});

On this page