Next.js Integration
The SDK provides built-in Next.js webhook adapters for both App Router and Pages Router. This guide shows you how to implement WhatsApp webhooks in your Next.js application.
Choosing Your Router
Next.js offers two routing systems:
- App Router (Next.js 13+) - Modern, uses
app/directory - Pages Router (Legacy) - Traditional, uses
pages/directory
Choose the adapter that matches your Next.js project.
App Router Integration
Installation
npm install meta-cloud-api# orpnpm add meta-cloud-apiSetup
Create a webhook route handler in app/api/webhook/route.ts:
import { webhookHandler } from 'meta-cloud-api/webhook/nextjs-app';
// 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 message handlerswhatsapp.processor.onText(async (client, message) => { console.log('Received:', message.text.body);
await client.messages.text({ to: message.from, body: `Echo: ${message.text.body}`, });});
// Export route handlersexport const { GET, POST } = whatsapp.webhook;That’s it! Your webhook is ready at /api/webhook.
Message Handlers
Create organized handler files for better code structure:
export * from './text';export * from './image';export * from './interactive';import type { WhatsApp, WebhookMessage } from 'meta-cloud-api';
export async function handleTextMessage( whatsapp: WhatsApp, message: WebhookMessage) { const text = message.text?.body.toLowerCase();
// Mark as read await whatsapp.messages.markAsRead({ messageId: message.id });
// Command handling if (text === 'menu') { await whatsapp.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 whatsapp.messages.text({ to: message.from, body: `You said: ${text}`, });}import type { WhatsApp, WebhookMessage } from 'meta-cloud-api';
export async function handleImageMessage( whatsapp: WhatsApp, message: WebhookMessage) { const { id, mime_type, caption } = message.image!;
console.log(`Received image: ${id} (${mime_type})`);
await whatsapp.messages.text({ to: message.from, body: caption ? `Nice image! "${caption}"` : 'Thanks for the image!', });}Import and register handlers in your route:
import { webhookHandler } from 'meta-cloud-api/webhook/nextjs-app';import { handleTextMessage, handleImageMessage } from '@/lib/messageHandlers';
const 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 handlerswhatsapp.processor.onText(handleTextMessage);whatsapp.processor.onImage(handleImageMessage);
export const { GET, POST } = whatsapp.webhook;Complete App Router Example
import { webhookHandler } from 'meta-cloud-api/webhook/nextjs-app';import { handleTextMessage, handleImageMessage, handleDocumentMessage, handleContactMessage, handleLocationMessage, handleInteractiveMessage,} from '@/lib/messageHandlers';
const 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 message handlerswhatsapp.processor.onText(handleTextMessage);whatsapp.processor.onImage(handleImageMessage);whatsapp.processor.onDocument(handleDocumentMessage);whatsapp.processor.onContacts(handleContactMessage);whatsapp.processor.onLocation(handleLocationMessage);whatsapp.processor.onInteractive(handleInteractiveMessage);
// Register status handlerwhatsapp.processor.onStatus(async (client, status) => { console.log(`Message ${status.id}: ${status.status}`);});
// Register webhook field handlerswhatsapp.processor.onAccountUpdate(async (client, update) => { console.log('Account updated:', update);});
export const { GET, POST } = whatsapp.webhook;Pages Router Integration
Setup
Create a webhook API route in pages/api/webhook.ts:
import { nextjsWebhookHandler } from 'meta-cloud-api';
// Disable body parser for webhook processingexport const config = { api: { bodyParser: false, },};
// Create webhook handlerconst whatsapp = nextjsWebhookHandler({ 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 message handlerswhatsapp.processor.onText(async (client, message) => { console.log('Received:', message.text.body);
await client.messages.text({ to: message.from, body: `Echo: ${message.text.body}`, });});
// Export the webhook handlerexport default whatsapp.webhook;Complete Pages Router Example
import { nextjsWebhookHandler } from 'meta-cloud-api';import { handleTextMessage, handleImageMessage, handleInteractiveMessage,} from '@/lib/messageHandlers';
export const config = { api: { bodyParser: false, },};
const whatsapp = nextjsWebhookHandler({ 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 handlerswhatsapp.processor.onText(handleTextMessage);whatsapp.processor.onImage(handleImageMessage);whatsapp.processor.onInteractive(handleInteractiveMessage);
whatsapp.processor.onStatus(async (client, status) => { console.log(`Status: ${status.id} - ${status.status}`);});
export default whatsapp.webhook;Handler Examples
Interactive Messages
Handle button and list responses:
import type { WhatsApp, WebhookMessage } from 'meta-cloud-api';
export async function handleInteractiveMessage( whatsapp: WhatsApp, message: WebhookMessage) { const { interactive } = message;
if (!interactive) return;
if (interactive.type === 'button_reply') { const { id, title } = interactive.button_reply;
switch (id) { case 'help': await whatsapp.messages.text({ to: message.from, body: 'Send "menu" to see available commands.', }); break;
case 'about': await whatsapp.messages.text({ to: message.from, body: 'WhatsApp Bot powered by meta-cloud-api', }); break;
default: await whatsapp.messages.text({ to: message.from, body: `You clicked: ${title}`, }); } }
if (interactive.type === 'list_reply') { const { id, title } = interactive.list_reply;
await whatsapp.messages.text({ to: message.from, body: `You selected: ${title}`, }); }
if (interactive.type === 'nfm_reply') { // Handle Flow response const flowData = JSON.parse(interactive.nfm_reply.response_json); console.log('Flow response:', flowData);
await whatsapp.messages.text({ to: message.from, body: 'Thanks for completing the flow!', }); }}Location Messages
import type { WhatsApp, WebhookMessage } from 'meta-cloud-api';
export async function handleLocationMessage( whatsapp: WhatsApp, message: WebhookMessage) { const { latitude, longitude, name, address } = message.location!;
console.log(`Location received: ${latitude}, ${longitude}`);
await whatsapp.messages.text({ to: message.from, body: `Thanks for sharing your location${name ? `: ${name}` : ''}!`, });}Document Messages
import type { WhatsApp, WebhookMessage } from 'meta-cloud-api';
export async function handleDocumentMessage( whatsapp: WhatsApp, message: WebhookMessage) { const { id, filename, mime_type } = message.document!;
console.log(`Document: ${filename} (${mime_type})`);
// Download document const mediaUrl = await whatsapp.media.retrieveMediaUrl({ mediaId: id });
await whatsapp.messages.text({ to: message.from, body: `Received: ${filename}`, });}Contact Messages
import type { WhatsApp, WebhookMessage } from 'meta-cloud-api';
export async function handleContactMessage( whatsapp: WhatsApp, message: WebhookMessage) { const contacts = message.contacts!;
for (const contact of contacts) { console.log(`Contact: ${contact.name.formatted_name}`); }
await whatsapp.messages.text({ to: message.from, body: `Received ${contacts.length} contact${contacts.length > 1 ? 's' : ''}!`, });}TypeScript Types
All handlers are fully typed:
import type { WhatsApp, WebhookMessage, TextMessage, ImageMessage, InteractiveMessage,} from 'meta-cloud-api';
// Generic message handlertype MessageHandler = ( whatsapp: WhatsApp, message: WebhookMessage) => Promise<void>;
// Type-safe text handlertype TextMessageHandler = ( whatsapp: WhatsApp, message: TextMessage) => Promise<void>;
// Type-safe image handlertype ImageMessageHandler = ( whatsapp: WhatsApp, message: ImageMessage) => Promise<void>;Environment Variables
Create a .env.local file:
WHATSAPP_ACCESS_TOKEN=your_access_tokenWHATSAPP_PHONE_NUMBER_ID=123456789WHATSAPP_BUSINESS_ACCOUNT_ID=987654321WEBHOOK_VERIFICATION_TOKEN=your_secure_tokenTesting Locally
Using ngrok
# Terminal 1: Start Next.js dev servernpm run dev# Running on http://localhost:3000
# Terminal 2: Start ngrokngrok http 3000# Forwarding https://abc123.ngrok.io -> http://localhost:3000Use the ngrok URL in Meta Developer Portal:
https://abc123.ngrok.io/api/webhookTesting Verification
# Test GET request (verification)curl "http://localhost:3000/api/webhook?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=test123"# Should return: test123Best Practices
Error Handling
Wrap handlers in try-catch blocks:
whatsapp.processor.onText(async (client, message) => { try { await processMessage(message); } catch (error) { console.error('Error processing message:', error);
// Notify user of error await client.messages.text({ to: message.from, body: 'Sorry, something went wrong. Please try again.', }); }});Background Processing
For long operations, process asynchronously:
whatsapp.processor.onText(async (client, message) => { // Acknowledge immediately setImmediate(async () => { await performLongOperation(message); });});Logging
Use Next.js logging best practices:
whatsapp.processor.onText(async (client, message) => { console.log({ timestamp: new Date().toISOString(), type: 'text_message', from: message.from, text: message.text.body, });});Rate Limiting
Implement rate limiting for webhook endpoints:
import { LRUCache } from 'lru-cache';
const tokenCache = new LRUCache<string, number>({ max: 500, ttl: 60000, // 1 minute});
export function rateLimit(identifier: string, limit: number = 10): boolean { const count = tokenCache.get(identifier) || 0;
if (count >= limit) { return false; }
tokenCache.set(identifier, count + 1); return true;}Deployment
Vercel
Next.js webhooks work seamlessly on Vercel:
- Push code to GitHub
- Import project in Vercel
- Add environment variables in Vercel dashboard
- Deploy
- Use Vercel URL in Meta Developer Portal:
https://your-app.vercel.app/api/webhook
Other Platforms
Works on any Node.js hosting:
- Netlify - Use Netlify Functions
- Railway - Direct Next.js deployment
- DigitalOcean - App Platform
- AWS - Amplify or EC2
- Self-hosted - Docker or PM2
Environment Variables in Production
Set in your hosting platform’s dashboard:
WHATSAPP_ACCESS_TOKEN=your_production_tokenWHATSAPP_PHONE_NUMBER_ID=123456789WHATSAPP_BUSINESS_ACCOUNT_ID=987654321WEBHOOK_VERIFICATION_TOKEN=your_production_secretComparison: App Router vs Pages Router
Pros:
- Modern Next.js architecture
- Clean export syntax (
export const { GET, POST }) - Better TypeScript support
- Improved performance
Cons:
- Requires Next.js 13+
- Newer, less documentation
Best for: New projects, modern apps
Pros:
- Stable, mature API
- More documentation available
- Works with older Next.js versions
Cons:
- Older architecture
- Requires
bodyParser: falseconfig
Best for: Existing projects, legacy apps
Troubleshooting
Webhook Not Receiving Messages
- Check ngrok is running (development)
- Verify webhook URL in Meta Developer Portal
- Ensure HTTPS is used (not HTTP)
- Check Next.js logs for errors
- Verify webhook subscriptions are active
TypeScript Errors
# Ensure types are installednpm install --save-dev @types/node
# Check tsconfig.json{ "compilerOptions": { "moduleResolution": "bundler", "target": "ES2017" }}Body Parser Issues (Pages Router)
Make sure you’ve disabled body parser:
export const config = { api: { bodyParser: false, // Required! },};