Webhooks deliver real-time notifications when WhatsApp template messages dispatched from the API change status. Receive instant updates when messages are sent, delivered, read, or failed.
When you dispatch an approved template via POST /v1/templates/send, WhatsApp delivers event notifications through each stage of the message lifecycle:
Dispatched from Meta Cloud API and en route
Delivered to recipient device (Double grey ticks)
Opened & read by recipient (Double blue ticks)
Delivery failed (includes error reason)
Every delivery notification is sent with the event name messages.status:
| Field | Type | Description |
|---|---|---|
| event | string | Always messages.status. |
| timestamp | string | ISO 8601 UTC timestamp of the webhook event. |
| data.messageId | string | WhatsApp Message ID (wamid.HBgL...) returned in the send template API response. |
| data.status | string | Current delivery status: sent | delivered | read | failed. |
| data.recipientPhone | string | Recipient phone number in E.164 format (91XXXXXXXXXX). |
| data.timestamp | number | Unix epoch timestamp in seconds. |
| data.error | object | null | Included when status is failed. Contains error code, title, and clear failure explanation. |
{
"event": "messages.status",
"timestamp": "2026-08-19T10:30:05.000Z",
"data": {
"messageId": "wamid.HBgLMzE5NDU...",
"status": "delivered",
"recipientPhone": "91XXXXXXXXXX",
"timestamp": 1724063405
}
}{
"event": "messages.status",
"timestamp": "2026-08-19T10:30:15.000Z",
"data": {
"messageId": "wamid.HBgLMzE5NDU...",
"status": "read",
"recipientPhone": "91XXXXXXXXXX",
"timestamp": 1724063415
}
}{
"event": "messages.status",
"timestamp": "2026-08-19T10:30:05.000Z",
"data": {
"messageId": "wamid.HBgLMzE5NDU...",
"status": "failed",
"recipientPhone": "91XXXXXXXXXX",
"timestamp": 1724063405,
"error": {
"code": 131026,
"title": "Delivery Failed",
"message": "Delivery blocked by WhatsApp quality policy for this recipient."
}
}
}All webhook requests sent to your server include the x-webhook-signature header (HMAC SHA-256). Verify this header to ensure incoming payloads are authentic.
const express = require('express');
const crypto = require('crypto');
const app = express();
// IMPORTANT: Capture exact raw body Buffer for HMAC verification
app.use(express.json({
verify: (req, _res, buf) => {
req.rawBody = buf;
}
}));
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
app.post('/webhook/whatsapp', (req, res) => {
const signature = req.headers['x-webhook-signature'];
// 1. Enforce signature header requirement
if (!signature || typeof signature !== 'string') {
return res.status(401).json({ error: 'Missing x-webhook-signature header' });
}
if (!WEBHOOK_SECRET) {
console.error('WEBHOOK_SECRET environment variable is not configured');
return res.status(500).json({ error: 'Server webhook secret unconfigured' });
}
// 2. Strip optional 'sha256=' prefix
const cleanSignature = signature.startsWith('sha256=')
? signature.slice(7)
: signature;
if (!/^[a-f0-9]{64}$/i.test(cleanSignature)) {
return res.status(401).json({ error: 'Malformed signature format' });
}
// 3. Compute HMAC SHA-256 over exact raw bytes
const rawBody = req.rawBody || Buffer.from('');
const expectedHash = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
// 4. Constant-time comparison to prevent timing attacks
const sigBuffer = Buffer.from(cleanSignature, 'hex');
const expectedBuffer = Buffer.from(expectedHash, 'hex');
if (
sigBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(sigBuffer, expectedBuffer)
) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
// 5. Process authenticated event payload
const { event, data } = req.body;
if (event === 'messages.status') {
const { messageId, status, recipientPhone, error } = data;
if (status === 'delivered') {
console.log(`✅ Message ${messageId} delivered to ${recipientPhone}`);
} else if (status === 'read') {
console.log(`👁️ Message ${messageId} read by ${recipientPhone}`);
} else if (status === 'failed') {
console.error(`❌ Message ${messageId} failed: ${error?.message}`);
}
}
// Always return 200 OK promptly
res.status(200).json({ received: true });
});
app.listen(3000, () => console.log('Webhook server running on port 3000'));200 OK before doing long asynchronous tasks.