WhatsApp Webhooks
Webhooks deliver inbound messages and outbound delivery/read receipts to your server in real time. Configure a single HTTPS endpoint in the dashboard; AfriRoute posts events to it as they occur.
🔧 Configuration
- Set your callback URL in Dashboard → WhatsApp → Webhooks.
- Provide a verify token for the subscription handshake.
- Store the app secret used to sign payloads with
x-hub-signature-256.
✅ Verification Handshake
On subscription, AfriRoute issues a GET with a challenge you must echo back:
app.get('/webhooks/whatsapp', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === process.env.VERIFY_TOKEN) {
return res.status(200).send(challenge);
}
res.sendStatus(403);
});
📥 Inbound Messages
{
"object": "whatsapp_business_account",
"entry": [{
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "251911234567",
"phone_number_id": "123456789"
},
"messages": [{
"from": "251922222222",
"id": "wamid.abc123",
"timestamp": "1748419800",
"type": "text",
"text": { "body": "Hello!" }
}]
}
}]
}]
}
📱 Status Updates
{
"object": "whatsapp_business_account",
"entry": [{
"changes": [{
"value": {
"statuses": [{
"id": "wamid.abc123",
"status": "delivered",
"timestamp": "1748419805",
"recipient_id": "251911234567"
}]
}
}]
}]
}
Status values: sent, delivered, read, failed
🧩 Handling Events
app.post('/webhooks/whatsapp', async (req, res) => {
// Acknowledge quickly, then process
res.sendStatus(200);
for (const entry of req.body.entry) {
for (const change of entry.changes) {
const value = change.value;
for (const message of value.messages ?? []) {
if (message.type === 'text') {
await handleText(message.from, message.text.body);
} else if (message.type === 'interactive') {
const id = message.interactive.button_reply?.id
|| message.interactive.list_reply?.id;
await handleSelection(message.from, id);
}
}
for (const status of value.statuses ?? []) {
await updateMessageStatus(status.id, status.status);
}
}
}
});
from flask import Flask, request
app = Flask(__name__)
@app.post('/webhooks/whatsapp')
def whatsapp_webhook():
body = request.get_json()
for entry in body.get('entry', []):
for change in entry.get('changes', []):
value = change['value']
for msg in value.get('messages', []):
if msg['type'] == 'text':
handle_text(msg['from'], msg['text']['body'])
for st in value.get('statuses', []):
update_status(st['id'], st['status'])
return '', 200
🔐 Signature Verification
Always verify the x-hub-signature-256 header before trusting a payload:
const crypto = require('crypto');
function verifyWebhook(req) {
const signature = req.headers['x-hub-signature-256'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.rawBody) // use the raw request body
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
💡 Best Practices
- Respond with 200 immediately, then process asynchronously — slow responses trigger retries.
- Verify signatures on every request to reject spoofed payloads.
- Make handlers idempotent — events may be delivered more than once.
- Implement retry tolerance; failed deliveries retry with exponential backoff.
- Log raw payloads during integration for easier debugging.
⚠️ Error Handling
| Situation | Behaviour |
|---|---|
| Non-2xx response | Event retried with exponential backoff for up to 24h |
| Invalid signature | You should reject and return 403 |
| Duplicate delivery | Deduplicate on messages[].id / statuses[].id |
📚 Related Resources
Need help? Contact Support → | Last Updated: May 2026