Skip to main content

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" }
}
FieldTypeDescription
idstringUnique event ID (idempotency key)
typestringEvent type (see table below)
created_atstringISO 8601 timestamp the event was generated
api_versionstringAPI version of the payload
dataobjectEvent-specific payload

Idempotency: Events may be delivered more than once — de-duplicate using id.

📋 Event Types​

Event TypeDescription
sms.sentMessage accepted by carrier
sms.deliveredMessage delivered to handset
sms.failedDelivery failed (see data.error)
sms.inboundInbound SMS received
voice.completedCall finished
whatsapp.messageInbound WhatsApp message
payment.succeededPayment captured
payment.failedPayment 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:

HeaderDescription
X-AfriRoute-SignatureHex-encoded HMAC-SHA256 of the raw request body
X-AfriRoute-TimestampUnix 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:

AttemptDelay after previous
1immediate
21 minute
35 minutes
430 minutes
52 hours
66 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"}'

Last Updated: May 2026