Express.js Integration
The SDK provides a built-in Express.js webhook adapter that handles both webhook verification and message processing. This guide shows you how to build a complete WhatsApp bot with Express.
Quick Start
Installation
npm install meta-cloud-api express# orpnpm add meta-cloud-api expressBasic Setup
import express from 'express';import { webhookHandler } from 'meta-cloud-api/webhook/express';
const app = express();
// Create webhook handlerconst whatsapp = webhookHandler({ accessToken: process.env.WHATSAPP_ACCESS_TOKEN!, phoneNumberId: parseInt(process.env.WHATSAPP_PHONE_NUMBER_ID!), businessAcctId: process.env.WHATSAPP_BUSINESS_ACCOUNT_ID, webhookVerificationToken: process.env.WEBHOOK_VERIFICATION_TOKEN!,});
// Register a text message handlerwhatsapp.processor.onText(async (client, message) => { console.log('Received:', message.text.body);
await client.messages.text({ to: message.from, body: `Echo: ${message.text.body}`, });});
// Set up routesapp.get('/webhook', whatsapp.GET);app.post('/webhook', express.json(), whatsapp.POST);
// Start serverconst PORT = process.env.PORT || 3000;app.listen(PORT, () => { console.log(`Server running on port ${PORT}`);});Handler Registration Patterns
The Express adapter provides multiple routing patterns:
Pattern 1: Separate GET/POST (Recommended)
const { GET, POST, processor } = whatsapp;
// Clean destructuringapp.get('/webhook', GET);app.post('/webhook', express.json(), POST);
// Register handlers on processorprocessor.onText(async (client, message) => { // Handle text messages});Pattern 2: Auto-routing
const { webhook, processor } = whatsapp;
// Single handler for both GET and POSTapp.use('/webhook', express.json());app.all('/webhook', webhook);
processor.onText(async (client, message) => { // Handle text messages});Pattern 3: Direct Method Access
// Access handlers directlyapp.get('/webhook', whatsapp.GET);app.post('/webhook', express.json(), whatsapp.POST);
whatsapp.processor.onText(async (client, message) => { // Handle text messages});Message Handlers
The SDK provides type-safe handlers for all message types:
Text Messages
processor.onText(async (whatsapp, message) => { console.log(`From: ${message.from}`); console.log(`Text: ${message.text.body}`);
await whatsapp.messages.text({ to: message.from, body: `You said: ${message.text.body}`, });});Media Messages
processor.onImage(async (whatsapp, message) => { const { id, mime_type, caption } = message.image;
console.log(`Received image: ${id} (${mime_type})`); if (caption) { console.log(`Caption: ${caption}`); }
// Download the image const mediaUrl = await whatsapp.media.retrieveMediaUrl({ mediaId: id });
await whatsapp.messages.text({ to: message.from, body: `Thanks for the image! URL: ${mediaUrl.url}`, });});processor.onVideo(async (whatsapp, message) => { const { id, mime_type } = message.video;
await whatsapp.messages.text({ to: message.from, body: `Received your video (${mime_type})`, });});processor.onAudio(async (whatsapp, message) => { const { id, voice } = message.audio;
const messageType = voice ? 'voice message' : 'audio file';
await whatsapp.messages.text({ to: message.from, body: `Received your ${messageType}`, });});processor.onDocument(async (whatsapp, message) => { const { id, filename, mime_type } = message.document;
console.log(`Received document: ${filename}`);
await whatsapp.messages.text({ to: message.from, body: `Thanks for ${filename}`, });});Interactive Messages
// Handle button responsesprocessor.onInteractive(async (whatsapp, message) => { const { interactive } = message;
if (interactive.type === 'button_reply') { const buttonId = interactive.button_reply.id; console.log(`Button clicked: ${buttonId}`);
await whatsapp.messages.text({ to: message.from, body: `You clicked: ${interactive.button_reply.title}`, }); }
if (interactive.type === 'list_reply') { const listId = interactive.list_reply.id; console.log(`List item selected: ${listId}`);
await whatsapp.messages.text({ to: message.from, body: `You selected: ${interactive.list_reply.title}`, }); }
if (interactive.type === 'nfm_reply') { // Flow response const flowData = JSON.parse(interactive.nfm_reply.response_json); console.log('Flow response:', flowData); }});Location Messages
processor.onLocation(async (whatsapp, message) => { const { latitude, longitude, name, address } = message.location;
console.log(`Location: ${latitude}, ${longitude}`); if (name) console.log(`Name: ${name}`);
await whatsapp.messages.text({ to: message.from, body: `Received location: ${name || 'Unknown'}`, });});Contact Messages
processor.onContacts(async (whatsapp, message) => { const contacts = message.contacts;
for (const contact of contacts) { console.log(`Contact: ${contact.name.formatted_name}`);
if (contact.phones) { contact.phones.forEach(phone => { console.log(`Phone: ${phone.phone}`); }); } }
await whatsapp.messages.text({ to: message.from, body: `Received ${contacts.length} contact(s)`, });});Reaction Messages
processor.onReaction(async (whatsapp, message) => { const { message_id, emoji } = message.reaction;
if (emoji) { console.log(`Reacted with ${emoji} to message ${message_id}`); } else { console.log(`Removed reaction from message ${message_id}`); }});Status Updates
Track message delivery status:
processor.onStatus(async (whatsapp, status) => { console.log(`Message ${status.id}: ${status.status}`);
switch (status.status) { case 'sent': console.log('Message sent to WhatsApp'); break; case 'delivered': console.log('Message delivered to recipient'); break; case 'read': console.log('Message read by recipient'); break; case 'failed': console.log('Message failed:', status.errors); break; }});Webhook Field Handlers
Handle non-message webhook events:
Account Updates
processor.onAccountUpdate(async (whatsapp, update) => { console.log('Account updated:', update);
// Handle account changes if (update.account) { console.log('Account ID:', update.account.id); }});Template Updates
processor.onMessageTemplateStatusUpdate(async (whatsapp, update) => { console.log('Template status:', update);
const { event, message_template_id, message_template_name, reason } = update;
if (event === 'APPROVED') { console.log(`Template ${message_template_name} was approved!`); } else if (event === 'REJECTED') { console.log(`Template ${message_template_name} was rejected: ${reason}`); }});Phone Number Quality
processor.onPhoneNumberQualityUpdate(async (whatsapp, update) => { console.log('Quality rating:', update);
const { current_limit, event } = update;
if (event === 'FLAGGED') { console.warn('Phone number flagged! Current limit:', current_limit); }});Complete Example
Here’s a full-featured Express webhook server:
import 'dotenv/config';import express from 'express';import { webhookHandler } from 'meta-cloud-api/webhook/express';import { MessageTypesEnum } from 'meta-cloud-api/enums';
const app = express();
// Configurationconst whatsapp = webhookHandler({ accessToken: process.env.WHATSAPP_ACCESS_TOKEN!, phoneNumberId: parseInt(process.env.WHATSAPP_PHONE_NUMBER_ID!), businessAcctId: process.env.WHATSAPP_BUSINESS_ACCOUNT_ID, webhookVerificationToken: process.env.WEBHOOK_VERIFICATION_TOKEN!,});
// Text message handlerwhatsapp.processor.onText(async (client, message) => { const text = message.text.body.toLowerCase();
// Mark as read await client.messages.markAsRead({ messageId: message.id });
// Show typing indicator await client.messages.showTypingIndicator({ messageId: message.id });
// Command handling if (text === 'menu') { await client.messages.interactive({ to: message.from, type: 'button', body: { text: 'Choose an option:' }, action: { buttons: [ { type: 'reply', reply: { id: 'help', title: 'Help' } }, { type: 'reply', reply: { id: 'about', title: 'About' } }, ], }, }); return; }
// Echo await client.messages.text({ to: message.from, body: `You said: ${message.text.body}`, });});
// Interactive message handlerwhatsapp.processor.onInteractive(async (client, message) => { if (message.interactive.type === 'button_reply') { const buttonId = message.interactive.button_reply.id;
let response = ''; if (buttonId === 'help') { response = 'Send "menu" to see options.'; } else if (buttonId === 'about') { response = 'WhatsApp Bot built with meta-cloud-api'; }
await client.messages.text({ to: message.from, body: response, }); }});
// Image handlerwhatsapp.processor.onImage(async (client, message) => { await client.messages.text({ to: message.from, body: 'Thanks for the image!', });});
// Status handlerwhatsapp.processor.onStatus(async (client, status) => { console.log(`[${status.timestamp}] ${status.id}: ${status.status}`);});
// Routesapp.get('/webhook', whatsapp.GET);app.post('/webhook', express.json(), whatsapp.POST);
// Health checkapp.get('/', (req, res) => { res.json({ status: 'ok', message: 'WhatsApp webhook server' });});
// Start serverconst PORT = process.env.PORT || 3000;app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); console.log(`Webhook: http://localhost:${PORT}/webhook`);});Error Handling
Handle errors gracefully:
// Global error handlerprocessor.onText(async (whatsapp, message) => { try { await processMessage(message); } catch (error) { console.error('Error processing message:', error);
// Notify user await whatsapp.messages.text({ to: message.from, body: 'Sorry, something went wrong. Please try again.', }); }});
// Express error handlerapp.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error('Express error:', err); res.status(500).json({ error: 'Internal server error' });});Testing Locally
Use ngrok to test webhooks locally:
# Terminal 1: Start servernpm run dev
# Terminal 2: Start ngrokngrok http 3000
# Use ngrok URL in Meta Developer Portal# https://abc123.ngrok.io/webhookTest your webhook:
# Test verificationcurl "http://localhost:3000/webhook?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=test"
# Test webhook (mock payload)curl -X POST http://localhost:3000/webhook \ -H "Content-Type: application/json" \ -d '{ "object": "whatsapp_business_account", "entry": [{ "id": "123", "changes": [{ "value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "1234567890", "phone_number_id": "123456789" }, "contacts": [{"profile": {"name": "Test"}, "wa_id": "1234567890"}], "messages": [{ "from": "1234567890", "id": "wamid.test", "timestamp": "1234567890", "type": "text", "text": {"body": "Hello"} }] }, "field": "messages" }] }] }'Best Practices
Use Environment Variables
WHATSAPP_ACCESS_TOKEN=your_tokenWHATSAPP_PHONE_NUMBER_ID=123456789WHATSAPP_BUSINESS_ACCOUNT_ID=987654321WEBHOOK_VERIFICATION_TOKEN=your_secretPORT=3000Async Processing
For long-running operations, process in the background:
processor.onText(async (whatsapp, message) => { // Acknowledge immediately setImmediate(async () => { await performLongOperation(message); });});Rate Limiting
Implement rate limiting to avoid overwhelming the API:
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per window});
app.use('/webhook', limiter);Monitoring
Add logging and monitoring:
import morgan from 'morgan';
app.use(morgan('combined'));
processor.onText(async (whatsapp, message) => { console.log('[TEXT]', { from: message.from, text: message.text.body, timestamp: message.timestamp, });});Deployment
Environment Setup
# Production .envNODE_ENV=productionWHATSAPP_ACCESS_TOKEN=your_production_tokenWHATSAPP_PHONE_NUMBER_ID=123456789WHATSAPP_BUSINESS_ACCOUNT_ID=987654321WEBHOOK_VERIFICATION_TOKEN=your_production_secretPORT=3000Using Process Managers
# Install PM2npm install -g pm2
# Start with PM2pm2 start dist/server.js --name whatsapp-bot
# Monitorpm2 logs whatsapp-botpm2 monit