Skip to main content

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​

  1. Set your callback URL in Dashboard → WhatsApp → Webhooks.
  2. Provide a verify token for the subscription handshake.
  3. 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​

SituationBehaviour
Non-2xx responseEvent retried with exponential backoff for up to 24h
Invalid signatureYou should reject and return 403
Duplicate deliveryDeduplicate on messages[].id / statuses[].id

Need help? Contact Support → | Last Updated: May 2026