eSMS AfricaeSMS Africa
API ReferenceSMS API

Webhooks

Receive delivery reports (DLR) and inbound (MO) messages, with HMAC signature verification.

Overview

Instead of polling for status, configure webhooks and eSMS will POST events to your endpoints in real time:

  • dlr_url - delivery reports: every status change on a message you sent (submitted, delivered, failed, ...).
  • mo_url - inbound (MO) messages: replies from recipients.

Every request is signed with HMAC-SHA256 so you can verify it came from eSMS.

Configure endpoints

Set your URLs in Developers → Webhooks in the dashboard, or over the API:

GET  https://sms.esmsafrica.io/api/webhooks
PUT  https://sms.esmsafrica.io/api/webhooks
POST https://sms.esmsafrica.io/api/webhooks/test
POST https://sms.esmsafrica.io/api/webhooks/rotate-secret
curl -X PUT https://sms.esmsafrica.io/api/webhooks \
  -H "Authorization: Bearer esms_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "dlr_url": "https://example.com/hooks/dlr",
    "mo_url": "https://example.com/hooks/mo",
    "is_active": true
  }'

GET /api/webhooks returns your current config including the signing_secret. Endpoints must be publicly reachable - private, loopback and metadata addresses are refused by an SSRF guard.

POST /api/webhooks/test sends sample signed DLR and MO events to your configured URLs so you can verify your integration end to end.

Delivery report (DLR) payload

Delivered to your dlr_url. The event is message.<status>, e.g. message.delivered or message.failed.

{
  "event": "message.delivered",
  "message_id": "msg_abc123",
  "status": "delivered",
  "phone": "+254712345678",
  "sender_id": "MyApp",
  "segments": 1,
  "encoding": "GSM7",
  "environment": "live",
  "livemode": true,
  "cost": 0.0096,
  "error_code": null,
  "retry_count": 0,
  "delivered_at": "2026-08-14T12:30:04+00:00",
  "failed_at": null
}

On a delivery failure the event is message.failed, status is failed, error_code carries the failure reason and failed_at is set.

Inbound (MO) payload

Delivered to your mo_url when a recipient replies:

{
  "event": "message.inbound",
  "message_id": "mo_def456",
  "from": "+254712345678",
  "to": "MyApp",
  "text": "YES",
  "keyword": null,
  "opt_out": false,
  "connector": "AT_KE"
}

keyword and opt_out are set when the reply is a STOP/START keyword - see Opt-outs.

Headers

HeaderDescription
X-Webhook-IDUnique event ID (evt_...).
X-Webhook-Signaturesha256=<hex> HMAC of the raw request body, keyed by your signing_secret.
Content-Typeapplication/json.

Verifying the signature

Compute HMAC-SHA256 over the raw request body with your signing_secret and compare it (constant-time) to the hex in X-Webhook-Signature after the sha256= prefix.

Node.js (Express)
import crypto from "node:crypto";

// Use the raw body, e.g. express.raw({ type: "application/json" })
function verify(req, secret) {
  const header = req.get("X-Webhook-Signature") || "";
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(req.body).digest("hex");
  const a = Buffer.from(header);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/hooks/dlr", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req, process.env.ESMS_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body.toString());
  // ... handle event.status / event.event
  res.sendStatus(200);
});
Python (Flask)
import hashlib
import hmac
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = "your_signing_secret"

@app.post("/hooks/dlr")
def dlr():
    raw = request.get_data()  # raw bytes, before JSON parsing
    expected = "sha256=" + hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
    header = request.headers.get("X-Webhook-Signature", "")
    if not hmac.compare_digest(expected, header):
        abort(401)
    event = request.get_json()
    # ... handle event["status"] / event["event"]
    return "", 200

Delivery & retries

Respond with a 2xx status quickly. Failed DLR deliveries are retried with exponential backoff (10s, 30s, 90s, 270s, 810s). Rotate your secret any time with POST /api/webhooks/rotate-secret (this returns a new signing_secret).

On this page