WhatsApp Webhooks Integration Guide for Developers
Building a WhatsApp integration? This developer guide explains how to set up and use webhooks to receive real-time updates from WhatsApp Business API.
What Are Webhooks?
Definition
Webhooks are HTTP callbacks that notify your server when events happen in WhatsApp, such as incoming messages, delivery status updates, or template status changes.
How It Works
Event Flow:
User sends message to your WhatsApp
WhatsApp sends HTTP POST to your webhook
Your server receives the payload
Your server processes and responds
You send reply via API (if needed)
Setting Up Webhooks
Prerequisites
Requirements:
✅ Publicly accessible HTTPS endpoint
✅ Valid SSL certificate
✅ Server to process requests
✅ WhatsApp Business API access
✅ Verification token ready
Endpoint Requirements
Your webhook must:
• Use HTTPS (TLS 1.2+)
• Be publicly accessible
• Respond within 20 seconds
• Return 200 OK quickly
• Handle GET (verification)
• Handle POST (events)
Verification Setup
// Express.js verification endpoint
app.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
// Your verification token
const VERIFY_TOKEN = process.env.VERIFY_TOKEN;
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
console.log('Webhook verified');
res.status(200).send(challenge);
} else {
res.sendStatus(403);
}
});
Receiving Events
// Express.js event handler
app.post('/webhook', (req, res) => {
// Acknowledge receipt immediately
res.sendStatus(200);
// Process asynchronously
const body = req.body;
if (body.object === 'whatsapp_business_account') {
body.entry.forEach(entry => {
entry.changes.forEach(change => {
if (change.field === 'messages') {
handleMessages(change.value);
}
});
});
}
});
Webhook Events
Message Events
// Incoming message structure
{
"object": "whatsapp_business_account",
"entry": [{
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "966501234567",
"phone_number_id": "PHONE_NUMBER_ID"
},
"contacts": [{
"profile": {
"name": "Customer Name"
},
"wa_id": "966509876543"
}],
"messages": [{
"from": "966509876543",
"id": "wamid.xxx",
"timestamp": "1705744200",
"type": "text",
"text": {
"body": "Hello!"
}
}]
},
"field": "messages"
}]
}]
}
Message Types
// Text message
{
"type": "text",
"text": { "body": "Hello!" }
}
// Image message
{
"type": "image",
"image": {
"id": "MEDIA_ID",
"mime_type": "image/jpeg",
"sha256": "xxx",
"caption": "Check this out"
}
}
// Location message
{
"type": "location",
"location": {
"latitude": 24.7136,
"longitude": 46.6753,
"name": "Riyadh",
"address": "Saudi Arabia"
}
}
// Interactive reply
{
"type": "interactive",
"interactive": {
"type": "button_reply",
"button_reply": {
"id": "button_1",
"title": "Track Order"
}
}
}
Status Updates
// Delivery status
{
"statuses": [{
"id": "wamid.xxx",
"status": "delivered",
"timestamp": "1705744300",
"recipient_id": "966509876543",
"conversation": {
"id": "CONVERSATION_ID",
"origin": {
"type": "user_initiated"
}
},
"pricing": {
"billable": true,
"pricing_model": "CBP",
"category": "service"
}
}]
}
// Status types:
// sent - Message sent to WhatsApp
// delivered - Delivered to device
// read - User read the message
// failed - Delivery failed
Processing Events
Message Handler
async function handleMessages(value) {
const messages = value.messages || [];
const contacts = value.contacts || [];
for (const message of messages) {
const from = message.from;
const messageId = message.id;
const timestamp = message.timestamp;
const type = message.type;
// Get sender name
const contact = contacts.find(c => c.wa_id === from);
const name = contact?.profile?.name || 'Customer';
// Process by type
switch (type) {
case 'text':
await handleTextMessage(from, message.text.body, name);
break;
case 'image':
await handleImageMessage(from, message.image);
break;
case 'interactive':
await handleInteractiveMessage(from, message.interactive);
break;
default:
await handleUnknownMessage(from, type);
}
}
}
Status Handler
async function handleStatuses(statuses) {
for (const status of statuses) {
const messageId = status.id;
const statusType = status.status;
const recipientId = status.recipient_id;
// Update message status in database
await updateMessageStatus(messageId, statusType);
// Handle specific statuses
if (statusType === 'failed') {
const error = status.errors?.[0];
console.error(Message failed: ${error?.message});
await handleFailedMessage(messageId, error);
}
if (statusType === 'read') {
await trackReadReceipt(messageId, recipientId);
}
}
}
Security
Signature Verification
const crypto = require('crypto');
function verifySignature(req, secret) {
const signature = req.headers['x-hub-signature-256'];
if (!signature) return false;
const expectedSignature = 'sha256=' +
crypto
.createHmac('sha256', secret)
.update(JSON.stringify(req.body))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Use in middleware
app.post('/webhook', (req, res, next) => {
if (!verifySignature(req, process.env.APP_SECRET)) {
return res.sendStatus(401);
}
next();
});
Best Practices
Security checklist:
✅ Verify webhook signatures
✅ Use HTTPS only
✅ Validate payload structure
✅ Rate limit endpoints
✅ Log all requests
✅ Handle errors gracefully
✅ Use environment variables
✅ Rotate secrets regularly
Error Handling
Common Errors
// Error handling wrapper
async function processWebhook(body) {
try {
// Process normally
await handleWebhookBody(body);
} catch (error) {
// Log error
console.error('Webhook error:', error);
// Store for retry if needed
await storeFailedWebhook(body, error);
// Don't throw - still return 200
// to prevent WhatsApp retries
}
}
Retry Logic
// WhatsApp retries if no 200 response
Retry schedule:
1st retry: 15 minutes
2nd retry: 30 minutes
3rd retry: 1 hour
4th retry: 3 hours
5th retry: 6 hours
6th retry: 12 hours
7th retry: 24 hours
After 7 retries: Webhook disabled
Solution:
• Always return 200 quickly
• Process asynchronously
• Log failures for debugging
Performance
Async Processing
// Don't block the response
app.post('/webhook', async (req, res) => {
// Respond immediately
res.sendStatus(200);
// Process in background
setImmediate(async () => {
try {
await processWebhook(req.body);
} catch (error) {
console.error('Background processing error:', error);
}
});
});
Queue Processing
// Use a queue for reliability
const Queue = require('bull');
const webhookQueue = new Queue('webhooks');
app.post('/webhook', (req, res) => {
res.sendStatus(200);
webhookQueue.add({
body: req.body,
timestamp: Date.now()
});
});
webhookQueue.process(async (job) => {
await processWebhook(job.data.body);
});
Testing
Local Development
# Use ngrok for local testing
ngrok http 3000
Output:
https://abc123.ngrok.io -> http://localhost:3000
Use this URL for webhook configuration
Test Payloads
// Send test webhook
const testPayload = {
object: "whatsapp_business_account",
entry: [{
id: "123456789",
changes: [{
value: {
messaging_product: "whatsapp",
messages: [{
from: "966501234567",
id: "test_message_id",
timestamp: Date.now().toString(),
type: "text",
text: { body: "Test message" }
}]
},
field: "messages"
}]
}]
};
// POST to your local server
fetch('http://localhost:3000/webhook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testPayload)
});
Get Started
Ready to integrate webhooks?
---
Related Articles: