Webhooks
Receive real-time event notifications for outbound delivery updates and inbound messages from your users — no polling required.
Real-time Events
Receive instant push notifications for message sent, delivered, read, and failed events
Inbound Messages
Handle replies from users — text, media, buttons, and list responses
HMAC Verification
Verify every payload with HMAC-SHA256 signatures to protect against spoofing
Auto Retry
Missed events are retried up to 5 times with exponential backoff
Webhook Secret
X-Sendexa-Signature header on every incoming request.Always Verify Signatures
<500ms
event to webhook
5
with backoff
6
delivery + inbound
SHA256
HMAC verification
message.sent
Message dispatched to WhatsApp network
message.delivered
Message delivered to recipient device
message.read
Message opened and read by the recipient
message.failed
Delivery failed — error details included in payload
message.inbound
User replied or sent a message to your WhatsApp number
message.deleted
User deleted the message before reading
{"event": "message.delivered","timestamp": "2024-01-15T10:30:03Z","data": {"messageId": "exa_wa_123456789_abc123def","wamid": "wamid.HBgLMjMzNTU1MzM5NTIVAgARGBI2QzE5QUI3RjZENkI2QTU3NjQA","to": "233244000000","type": "text","deliveredAt": "2024-01-15T10:30:03Z"}}
Signature Verification
Every webhook request includes an X-Sendexa-Signature header containing an HMAC-SHA256 hash of the raw request body signed with your webhook secret.
import crypto from 'crypto';function verifyWebhookSignature(rawBody: string,signature: string,secret: string): boolean {const expected = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');// Use timingSafeEqual to prevent timing attacksreturn crypto.timingSafeEqual(Buffer.from(signature),Buffer.from(expected));}
Webhook Handler Examples
import express from 'express';import crypto from 'crypto';const app = express();// Parse raw body for signature verificationapp.use('/webhooks/whatsapp', express.raw({ type: 'application/json' }));app.post('/webhooks/whatsapp', (req, res) => {const signature = req.headers['x-sendexa-signature'];const secret = process.env.WHATSAPP_WEBHOOK_SECRET;// 1. Verify signatureconst expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {return res.status(401).json({ error: 'Invalid signature' });}const payload = JSON.parse(req.body.toString());// 2. Handle eventsswitch (payload.event) {case 'message.delivered':console.log(`Delivered: ${payload.data.messageId}`);break;case 'message.read':console.log(`Read: ${payload.data.messageId}`);break;case 'message.failed':console.error(`Failed: ${payload.data.messageId}`, payload.data.error);// Trigger retry logic herebreak;case 'message.inbound':const { from, text } = payload.data;console.log(`Reply from ${from}: ${text?.body}`);// Route to your support systembreak;default:console.log(`Unknown event: ${payload.event}`);}// 3. Always respond 200 quicklyres.status(200).json({ received: true });});app.listen(3000);
Retry Behaviour
| Attempt | Delay | Notes |
|---|---|---|
| #1 | Immediate | First delivery attempt |
| #2 | 30 seconds | On non-2xx response or timeout |
| #3 | 2 minutes | Exponential backoff |
| #4 | 10 minutes | Exponential backoff |
| #5 | 30 minutes | Final attempt — event dropped if failed |
Best Practices
- Always respond with
HTTP 200within 5 seconds — process events asynchronously if needed - Verify the
X-Sendexa-Signatureheader on every request before processing - Make your handlers idempotent — the same event may be delivered more than once
- Use the
wamidas the idempotency key to deduplicate events - Log the raw payload before processing for easier debugging