Webhook Verification
When you set up a webhook endpoint in the Meta Developer Portal, WhatsApp needs to verify that you own and control the endpoint. This guide explains how webhook verification works and how to implement it.
Verification Flow
sequenceDiagram participant Meta as Meta Developer Portal participant Your Server
Note over Meta,Your Server: Initial Setup Meta->>Your Server: GET /webhook?hub.mode=subscribe&<br/>hub.verify_token=YOUR_TOKEN&<br/>hub.challenge=RANDOM_STRING
alt Token matches Your Server->>Meta: 200 OK<br/>Body: RANDOM_STRING Note over Meta: ✅ Verification successful else Token doesn't match Your Server->>Meta: 403 Forbidden Note over Meta: ❌ Verification failed endHow It Works
When you configure your webhook in the Meta Developer Portal:
-
You provide:
- Callback URL: Your webhook endpoint (e.g.,
https://example.com/webhook) - Verify Token: A secret string you choose (e.g.,
my-secret-token-123)
- Callback URL: Your webhook endpoint (e.g.,
-
WhatsApp sends a GET request to your endpoint:
GET /webhook?hub.mode=subscribe&hub.verify_token=my-secret-token-123&hub.challenge=1234567890 -
Your server must:
- Check that
hub.modeequalssubscribe - Check that
hub.verify_tokenmatches your secret token - Return the
hub.challengevalue as plain text with a 200 status
- Check that
-
If verification succeeds, WhatsApp saves your webhook configuration
Implementation
The WebhookProcessor handles verification automatically. All framework adapters (Express, Next.js) include verification support.
Using Framework Adapters
import express from 'express';import { webhookHandler } from 'meta-cloud-api/webhook/express';
const app = express();
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!,});
// GET request automatically handledapp.get('/webhook', whatsapp.GET);
// POST requests for actual webhooksapp.post('/webhook', express.json(), whatsapp.POST);
app.listen(3000);import { webhookHandler } from 'meta-cloud-api/webhook/nextjs-app';
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!,});
// Export GET and POST handlersexport const { GET, POST } = whatsapp.webhook;import { nextjsWebhookHandler } from 'meta-cloud-api';
export const config = { api: { bodyParser: false, // Important for webhook processing },};
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!,});
// Handles both GET (verification) and POST (webhooks)export default whatsapp.webhook;Manual Implementation
If you’re building a custom adapter, use WebhookProcessor directly:
import { WebhookProcessor } from 'meta-cloud-api';
const processor = new WebhookProcessor({ 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!,});
// Handle GET requestapp.get('/webhook', async (req, res) => { const mode = req.query['hub.mode']; const token = req.query['hub.verify_token']; const challenge = req.query['hub.challenge'];
const result = await processor.processVerification( mode as string, token as string, challenge as string );
res.status(result.status) .set(result.headers) .send(result.body);});Setting Up in Meta Developer Portal
Step 1: Configure Webhook
- Go to Meta Developer Portal
- Select your WhatsApp Business App
- Navigate to WhatsApp > Configuration
- In the Webhook section, click Edit
- Enter your Callback URL (must be HTTPS):
https://your-domain.com/webhook
- Enter your Verify Token (must match your environment variable):
my-secret-token-123
- Click Verify and Save
Step 2: Subscribe to Webhook Fields
After verification succeeds, subscribe to the webhook fields you need:
- ✅ messages - Incoming messages from users
- ✅ message_status - Message delivery status updates
- ⚪ account_alerts - Account notifications
- ⚪ account_update - Account changes
- ⚪ account_review_update - Review status changes
- ⚪ business_capability_update - Capability changes
- ⚪ flows - WhatsApp Flows events
- ⚪ message_template_status_update - Template status
- ⚪ phone_number_name_update - Phone number name changes
- ⚪ phone_number_quality_update - Quality rating changes
Testing Verification
Local Testing with ngrok
During development, use ngrok to expose your local server:
# Terminal 1: Start your servernpm run dev# Server running on http://localhost:3000
# Terminal 2: Start ngrokngrok http 3000# Forwarding https://abc123.ngrok.io -> http://localhost:3000Use the ngrok HTTPS URL in the Meta Developer Portal:
https://abc123.ngrok.io/webhookManual Verification Test
Test your verification endpoint manually:
curl "https://your-domain.com/webhook?hub.mode=subscribe&hub.verify_token=my-secret-token-123&hub.challenge=test123"# Should return: test123Common Issues
403 Forbidden
Problem: Verification token doesn’t match
Solution:
// Make sure this matches what you entered in Meta Developer PortalwebhookVerificationToken: process.env.WEBHOOK_VERIFICATION_TOKEN!,Check your .env file:
WEBHOOK_VERIFICATION_TOKEN=my-secret-token-123Connection Timeout
Problem: Server not responding or URL not accessible
Solutions:
- Ensure your server is running
- Check firewall rules allow incoming HTTPS traffic
- Verify SSL certificate is valid (use Let’s Encrypt)
- For ngrok, make sure tunnel is active
URL Not Found (404)
Problem: Webhook route not configured
Solution: Ensure you’ve set up the GET handler at the correct path:
// Expressapp.get('/webhook', whatsapp.GET);
export const { GET, POST } = whatsapp.webhook;
// Next.js Pages Router: pages/api/webhook.tsexport default whatsapp.webhook;Security Considerations
Use Strong Verification Tokens
Generate a random, secure token:
# Generate a secure random tokennode -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Store it in your .env file:
WEBHOOK_VERIFICATION_TOKEN=a1b2c3d4e5f6...HTTPS Only
WhatsApp requires HTTPS for production webhooks. In development:
- Use ngrok for local testing
- Use proper SSL certificates in production (Let’s Encrypt, Cloudflare, etc.)
Signature Verification
While the verify token authenticates the initial setup, you should also verify the signature of incoming webhook POST requests:
// Coming in future SDK versionsprocessor.enableSignatureVerification(process.env.APP_SECRET);Environment Variables
Required environment variables for webhook verification:
WHATSAPP_ACCESS_TOKEN=your_access_tokenWHATSAPP_PHONE_NUMBER_ID=123456789WHATSAPP_BUSINESS_ACCOUNT_ID=987654321WEBHOOK_VERIFICATION_TOKEN=your_secure_random_tokenTroubleshooting
Verification Keeps Failing
- Check server logs for incoming requests
- Verify token matches exactly (case-sensitive)
- Ensure GET handler is responding with plain text, not JSON
- Test with curl to isolate issues
- Check ngrok web interface (http://localhost:4040) to see requests