Webhooks API Reference
Webhooks let AfriRoute push real-time events to your server — delivery receipts, inbound messages, call status, and payment updates — instead of polling. This page consolidates the event format, security model, retry behavior, and management endpoints. For background see the Webhooks Overview.
📦 Event Envelope
Every webhook is delivered as an HTTP POST with a JSON body in a consistent envelope:
{
"id": "evt_8K2L9M0N",
"type": "sms.delivered",
"created_at": "2026-05-28T10:30:05Z",
"api_version": "v1",
"data": { "message_id": "msg_abc123", "status": "delivered" }
}
| Field | Type | Description |
|---|---|---|
id | string | Unique event ID (idempotency key) |
type | string | Event type (see table below) |
created_at | string | ISO 8601 timestamp the event was generated |
api_version | string | API version of the payload |
data | object | Event-specific payload |
Idempotency: Events may be delivered more than once — de-duplicate using
id.
📋 Event Types
| Event Type | Description |
|---|---|
sms.sent | Message accepted by carrier |
sms.delivered | Message delivered to handset |
sms.failed | Delivery failed (see data.error) |
sms.inbound | Inbound SMS received |
voice.completed | Call finished |
whatsapp.message | Inbound WhatsApp message |
payment.succeeded | Payment captured |
payment.failed | Payment declined or errored |
Full payloads for each type are documented in Webhook Events.
Security
AfriRoute signs every webhook so you can verify it genuinely originated from us and was not tampered with in transit. Each request includes these headers:
| Header | Description |
|---|---|
X-AfriRoute-Signature | Hex-encoded HMAC-SHA256 of the raw request body |
X-AfriRoute-Timestamp | Unix timestamp when the request was signed |
The signature is computed as HMAC-SHA256(secret, "{timestamp}.{raw_body}"), where secret is the signing secret returned when you register the endpoint. Always verify against the raw, unparsed body — re-serializing the JSON changes byte order and breaks verification. Reject requests whose timestamp is older than ~5 minutes to prevent replay attacks.
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, timestamp, secret) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // replay protection
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
See Webhook Security for secret rotation and IP allowlisting.
🔁 Retries
A delivery succeeds only when your endpoint returns 2xx within 10 seconds. Any other response or timeout triggers retries with exponential backoff:
| Attempt | Delay after previous |
|---|---|
| 1 | immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
After 6 failed attempts the event is dropped and the endpoint may be auto-disabled. Return 2xx quickly and process asynchronously to avoid timeouts.
📡 Management Endpoints
Register a Webhook
curl -X POST https://api.afriroute.ai/api/v1/webhooks \
-H "Authorization: Bearer $AFRIROUTE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/afriroute",
"events": ["sms.delivered", "sms.failed"]
}'
{
"id": "wh_4F5G6H",
"secret": "whsec_a1b2c3d4e5",
"status": "active"
}
The secret is shown only once — store it securely for signature verification.
List Webhooks
Retrieve all registered endpoints with GET /v1/webhooks.
Send a Test Event
curl -X POST https://api.afriroute.ai/api/v1/webhooks/wh_4F5G6H/test \
-H "Authorization: Bearer $AFRIROUTE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "sms.delivered"}'
📚 Related Resources
Last Updated: May 2026