- Add comprehensive guide for integrating WhatsApp Business API using Twilio phone numbers - Include complete workflow: from number purchase to Meta configuration - Add 5 detailed guides: quick-start, webhooks, examples, troubleshooting, quick-reference - Cover phone number verification via voice call (Twilio requirement) - Document all Meta credentials: Access Token, Phone Number ID, WABA ID, Verify Token, Application ID - Provide code examples in BASIC, Node.js, and Python - Integrate into Appendix B: External Services (no new chapter) - All example values randomized for security
24 KiB
Troubleshooting Guide
This comprehensive guide helps you diagnose and resolve common issues when integrating WhatsApp Business API with Twilio phone numbers in General Bots.
Table of Contents
- Diagnostic Tools
- Verification Issues
- Webhook Problems
- API Errors
- Message Delivery Failures
- Twilio-Specific Issues
- Meta-Specific Issues
- Performance Issues
- Security Issues
Diagnostic Tools
Essential Commands
# Test webhook connectivity
curl -X POST https://your-webhook-url/webhooks/whatsapp \
-H "Content-Type: application/json" \
-d '{"test": true}'
# Check Meta API status
curl https://developers.facebook.com/status/
# Verify Twilio number configuration
twilio phone-numbers:list +553322980098
# Test Meta API connectivity
curl -X GET "https://graph.facebook.com/v18.0/1158433381968079" \
-H "Authorization: Bearer EAAQdlso6aM8BOwl..."
Log Locations
# General Bots logs
tail -f .gbot/logs/bot.log
# Webhook server logs (Node.js)
pm2 logs whatsapp-webhook
# Webhook server logs (Python)
journalctl -u whatsapp-webhook -f
# Twilio debugger
https://console.twilio.com/us1/develop/monitor/debugger
# Meta webhook debugging
https://developers.facebook.com/apps/YOUR_APP_ID/webhooks/
Verification Issues
Issue: Phone Number Verification Fails
Symptoms:
- Meta cannot verify your Twilio number
- "Verification failed" error after call completes
- No verification code received
Diagnosis:
# Check Twilio number has Voice capability
twilio phone-numbers:info +553322980098
# Test incoming call handling
# Call your Twilio number from another phone
# Check if webhook is triggered
Solutions:
-
Verify Voice Webhook Configuration
# Check webhook URL is correct curl -X POST https://your-domain.com/twilio/voice \ -d "CallSid=CA123&From=+1234567890&To=+553322980098" # Expected: TwiML XML response -
Verify Verification Method
- Ensure "Phone Call" is selected (not SMS)
- Twilio numbers don't support SMS for verification
- Meta must call your number
-
Check TwiML Response
<!-- Your webhook must return valid TwiML --> <?xml version="1.0" encoding="UTF-8"?> <Response> <Gather action="https://your-domain.com/twilio/gather" method="POST" numDigits="6"> <Say>Please enter your verification code.</Say> </Gather> </Response> -
Test Webhook Locally
# Use ngrok for local testing ngrok http 3000 # Update Twilio webhook to ngrok URL # Test with actual Meta verification call
Issue: Verification Code Not Captured
Symptoms:
- Call completes but no code received
- Email not forwarded
- Code not logged
Diagnosis:
# Check webhook server logs
tail -f /var/log/whatsapp-webhook/app.log
# Verify Gather action is configured
# Test DTMF capture with a test call
Solutions:
-
Implement Email Forwarding
// Add to your gather handler app.post('/twilio/gather', (req, res) => { const code = req.body.Digits; // Send email sendEmail({ to: 'your-email@example.com', subject: 'Verification Code', body: `Code: ${code}` }); // Also log it console.log('Verification Code:', code); res.type('text/xml'); res.send('<Response><Say>Thank you</Say></Response>'); }); -
Use Voicemail Fallback
<Redirect> https://twimlets.com/voicemail?Email=your-email@example.com&Transcribe=true </Redirect> -
Add Logging
REM BASIC logging ON WEBHOOK POST TO "/twilio/gather" DO LET CODE$ = GET FORM VALUE "Digits" LOG "Verification Code: " + CODE$ PRINT "Code logged" END ON
Webhook Problems
Issue: Webhook Verification Fails
Symptoms:
- Meta webhook setup shows "Verification failed"
- "Challenge mismatch" error
- 403 Forbidden response
Diagnosis:
# Test webhook verification endpoint
curl "https://your-domain.com/webhooks/whatsapp?hub.verify_token=YOUR_TOKEN&hub.challenge=CHALLENGE"
# Expected response: The challenge string
Solutions:
-
Match Verify Token Exactly
// In config.csv whatsapp-webhook-verify-token,4qIogZadggQ.BEoMeciXIdl_MlkV_1DTx8Z_i0bYPxtSJwKSbH0FKlY // In your code const VERIFY_TOKEN = '4qIogZadggQ.BEoMeciXIdl_MlkV_1DTx8Z_i0bYPxtSJwKSbH0FKlY'; app.get('/webhooks/whatsapp', (req, res) => { const token = req.query['hub.verify_token']; if (token === VERIFY_TOKEN) { res.send(req.query['hub.challenge']); } else { res.sendStatus(403); } }); -
Return Challenge Correctly
- Must return the challenge as plain text
- Must respond with 200 OK status
- Must not include HTML or JSON formatting
-
Check URL Accessibility
# Ensure URL is publicly accessible curl -v https://your-domain.com/webhooks/whatsapp # Check firewall rules sudo ufw status # Verify SSL certificate openssl s_client -connect your-domain.com:443
Issue: Webhook Not Receiving Messages
Symptoms:
- Webhook endpoint configured but no messages received
- Messages appear in Meta inbox but not in bot
- No webhook logs
Diagnosis:
# Check webhook subscriptions in Meta dashboard
# Navigate to: WhatsApp > API Setup > Webhook > Subscriptions
# Verify webhook field subscription
curl -X GET "https://graph.facebook.com/v18.0/YOUR_APP_ID/subscriptions" \
-H "Authorization: Bearer EAAQdlso6aM8BOwl..."
Solutions:
-
Subscribe to Messages Field
In Meta Dashboard: 1. Go to WhatsApp > API Setup > Webhook 2. Click "Manage" webhook fields 3. Subscribe to: messages 4. Save changes -
Check Webhook URL
// Ensure correct webhook path app.post('/webhooks/whatsapp', (req, res) => { console.log('Webhook received:', JSON.stringify(req.body, null, 2)); res.status(200).send('OK'); }); -
Verify Response Time
- Webhook must respond within 3 seconds
- Process heavy operations asynchronously
- Return 200 OK immediately
app.post('/webhooks/whatsapp', (req, res) => { // Acknowledge immediately res.status(200).send('OK'); // Process asynchronously setImmediate(() => { processWebhook(req.body); }); });
API Errors
Issue: 401 Unauthorized
Symptoms:
- API calls return 401 status
- "Invalid access token" error
- Messages fail to send
Diagnosis:
# Test access token validity
curl -X GET "https://graph.facebook.com/v18.0/me" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Expected: JSON with app ID and name
# 401 means token is invalid or expired
Solutions:
-
Generate New Access Token
In Meta Dashboard: 1. Go to WhatsApp > API Setup 2. Click "Temporary Access Token" 3. Copy and update config.csv 4. Restart bot -
Check Token Format
# config.csv # Token must start with EAAQ... whatsapp-api-key,EAAQdlso6aM8BOwlhc3yM6bbJkGyibQPGJd87zFDHtfaFoJDJPohMl2c5nXs4yYuuHwoXJWx0rQKo0VXgTwThPYzqLEZArOZBhCWPBUpq7YlkEJXFAgB6ZAb3eoUzZAMgNZCZA1sg11rT2G8e1ZAgzpRVRffU4jmMChc7ybcyIwbtGOPKZAXKcNoMRfUwssoLhDWr -
Verify Token Permissions
In Meta Dashboard: 1. Go to App Review > Permissions and Features 2. Ensure "WhatsApp" permission is granted 3. Check for any additional required permissions
Issue: 470 Message Rate Limit
Symptoms:
- Messages fail with 470 error
- "Rate limit exceeded" message
- Bulk sending stops working
Diagnosis:
# Check API rate limits
# WhatsApp Business API: 1000 messages/second per WABA
# Monitor message queue
tail -f .gbot/logs/message-queue.log
Solutions:
-
Implement Rate Limiting
class RateLimiter { constructor(maxRequests = 50, timeWindow = 60) { this.maxRequests = maxRequests; this.timeWindow = timeWindow; this.requests = []; } async send(whatsapp, to, message) { while (!this.canMakeRequest()) { await this.sleep(1000); } this.recordRequest(); return await whatsapp.sendText(to, message); } canMakeRequest() { const now = Date.now(); this.requests = this.requests.filter(t => now - t < this.timeWindow * 1000); return this.requests.length < this.maxRequests; } recordRequest() { this.requests.push(Date.now()); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } const rateLimiter = new RateLimiter(50, 60); await rateLimiter.send(whatsapp, to, message); -
Use Message Queuing
REM BASIC message queuing SUB SEND WHATSAPP WITH RATE LIMIT TO NUMBER$, MESSAGE$ ADD TO QUEUE "whatsapp-outbound", NUMBER$ + "|" + MESSAGE$ END SUB -
Monitor Usage
In Meta Dashboard: 1. Go to WhatsApp > API Usage 2. Monitor message volume 3. Check for rate limit warnings
Issue: Invalid Phone Number Format
Symptoms:
- "Invalid phone number" error
- Messages not delivered
- 400 Bad Request
Diagnosis:
# Test phone number format
# Correct format: 5511999999999 (no +, no spaces, no dashes)
Solutions:
-
Format Phone Numbers Correctly
function formatPhoneNumber(phone) { // Remove all non-digits let cleaned = phone.replace(/\D/g, ''); // Remove leading + or 00 if present cleaned = cleaned.replace(/^(\+|00)/, ''); // Ensure country code is present if (!cleaned.startsWith('55') && cleaned.length === 11) { cleaned = '55' + cleaned; } return cleaned; } const formatted = formatPhoneNumber('+55 (11) 99999-9999'); // Result: 5511999999999 -
Validate Before Sending
REM BASIC validation SUB SEND VALIDATED WHATSAPP NUMBER$, MESSAGE$ LET CLEANED$ = "" FOR I = 1 TO LEN(NUMBER$) LET CH$ = MID$(NUMBER$, I, 1) IF CH$ >= "0" AND CH$ <= "9" THEN CLEANED$ = CLEANED$ + CH$ END IF NEXT I IF LEN(CLEANED$) < 10 OR LEN(CLEANED$) > 15 THEN LOG "Invalid phone number: " + NUMBER$ EXIT SUB END IF SEND WHATSAPP TO CLEANED$ WITH MESSAGE$ END SUB
Message Delivery Failures
Issue: Messages Not Delivered (24-Hour Window)
Symptoms:
- Messages work when user messages first
- Fail after 24 hours of inactivity
- "24-hour window" error
Diagnosis:
# Check last message timestamp
# Meta allows business-initiated messages only within 24 hours of last user message
Solutions:
-
Use Message Templates
In Meta Dashboard: 1. Go to WhatsApp > Message Templates 2. Create and submit template for approval 3. Use template for business-initiated messages -
Send Template Message
async function sendTemplate(to, templateName, parameters = []) { const response = await axios.post( `${baseURL}/${phoneNumberId}/messages`, { messaging_product: 'whatsapp', to: to, type: 'template', template: { name: templateName, language: { code: 'pt_BR' }, components: [{ type: 'body', parameters: parameters.map(p => ({ type: 'text', text: p })) }] } }, { headers: { 'Authorization': `Bearer ${accessToken}` } } ); return response.data; } -
Track Last Interaction
// Store last message time const lastInteraction = new Map(); async function sendMessage(to, message) { const lastTime = lastInteraction.get(to); const now = Date.now(); const hoursSince = (now - lastTime) / (1000 * 60 * 60); if (hoursSince > 24) { // Use template await sendTemplate(to, 'reengagement_template', []); } else { // Use regular message await sendText(to, message); } lastInteraction.set(to, now); }
Issue: Media Messages Fail to Send
Symptoms:
- Text messages work, media fails
- "Media upload failed" error
- Images not delivered
Diagnosis:
# Test media URL accessibility
curl -I https://your-media-url.com/image.jpg
# Expected: 200 OK with Content-Type: image/jpeg
Solutions:
-
Verify Media URL
- Must be publicly accessible
- Must use HTTPS
- Must return correct Content-Type
- Should be under 5MB for images
-
Upload Media First
async function uploadMedia(mediaUrl) { const response = await axios.post( `${baseURL}/${phoneNumberId}/media`, { file: mediaUrl, type: 'image/jpeg' }, { headers: { 'Authorization': `Bearer ${accessToken}` } } ); return response.data.id; } async function sendImageWithUpload(to, imageUrl, caption) { const mediaId = await uploadMedia(imageUrl); await sendMediaById(to, mediaId, caption); } -
Use Approved Media Hosts
- Meta's media servers (preferred)
- AWS S3 with public access
- CloudFront CDN
- Avoid self-hosted media for reliability
Twilio-Specific Issues
Issue: TwiML Bin Not Working
Symptoms:
- TwiML Bin returns 404
- Invalid TwiML error
- Webhook not triggered
Diagnosis:
# Test TwiML Bin URL
curl -X POST https://handler.twilio.com/twiml/EH123...
Solutions:
-
Validate TwiML Syntax
<!-- Correct TwiML structure --> <?xml version="1.0" encoding="UTF-8"?> <Response> <Gather action="https://your-domain.com/gather" method="POST"> <Say voice="alice">Enter your code</Say> </Gather> </Response> -
Use Custom Webhook Instead
// Replace TwiML Bin with custom server app.post('/twilio/voice', (req, res) => { const twiml = new twilio.twiml.VoiceResponse(); const gather = twiml.gather({ action: '/twilio/gather', method: 'POST' }); gather.say('Enter your code'); res.type('text/xml'); res.send(twiml.toString()); });
Issue: Call Not Forwarded to Email
Symptoms:
- Voicemail not received
- Email not sent
- Transcription not working
Diagnosis:
# Check TwiML voicemail URL
https://twimlets.com/voicemail?Email=your-email@example.com
Solutions:
-
Implement Custom Voicemail
app.post('/twilio/gather', (req, res) => { const code = req.body.Digits; // Send email with code transporter.sendMail({ from: 'bot@example.com', to: 'your-email@example.com', subject: 'WhatsApp Verification Code', text: `Your code is: ${code}` }); // Also send via SMS as backup client.messages.create({ to: '+5511999999999', from: process.env.TWILIO_NUMBER, body: `Code: ${code}` }); res.type('text/xml'); res.send('<Response><Say>Code sent to email</Say></Response>'); }); -
Add Multiple Notification Channels
- Email (primary)
- SMS backup
- Database logging
- Webhook notification
Meta-Specific Issues
Issue: Phone Number Not Approved
Symptoms:
- Number shows "Not Verified"
- Cannot send messages
- "Number quality" error
Diagnosis:
# Check number status in Meta Dashboard
# WhatsApp Accounts > Phone Numbers > Select Number
Solutions:
-
Verify Number Quality
- Number must be from reputable provider
- Avoid VOIP numbers
- Use local numbers for target country
-
Complete Business Verification
In Meta Dashboard: 1. Go to Business Settings > Business Verification 2. Submit business documents 3. Wait for approval (7-14 days) -
Request Higher Limits
In Meta Dashboard: 1. Go to WhatsApp > API Settings 2. Request increased messaging limits 3. Provide business justification
Issue: Webhook Signature Verification Fails
Symptoms:
- All webhooks rejected
- "Invalid signature" errors
- Security check failures
Diagnosis:
# Check X-Hub-Signature-256 header
curl -X POST https://your-domain.com/webhooks/whatsapp \
-H "X-Hub-Signature-256: sha256=..." \
-d '{"test": true}'
Solutions:
-
Implement Signature Verification
const crypto = require('crypto'); function verifySignature(payload, signature, appSecret) { const expectedSignature = 'sha256=' + crypto.createHmac('sha256', appSecret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } app.post('/webhooks/whatsapp', (req, res) => { const signature = req.headers['x-hub-signature-256']; const payload = JSON.stringify(req.body); if (!verifySignature(payload, signature, APP_SECRET)) { return res.status(403).send('Invalid signature'); } // Process webhook processWebhook(req.body); res.status(200).send('OK'); }); -
Get App Secret
In Meta Dashboard: 1. Go to App Settings > Basic 2. Copy App Secret 3. Store securely in environment variable
Performance Issues
Issue: Slow Webhook Response
Symptoms:
- Webhooks timeout
- Meta shows "Webhook slow" warning
- Messages delayed
Diagnosis:
# Measure webhook response time
time curl -X POST https://your-domain.com/webhooks/whatsapp \
-H "Content-Type: application/json" \
-d '{"object":"whatsapp_business_account","entry":[]}'
Solutions:
-
Optimize Webhook Handler
app.post('/webhooks/whatsapp', (req, res) => { // Acknowledge immediately res.status(200).send('OK'); // Process asynchronously setImmediate(() => { processMessageAsync(req.body); }); }); async function processMessageAsync(data) { // Heavy processing here await handleMessage(data); } -
Use Worker Queues
const { Queue } = require('bull'); const webhookQueue = new Queue('webhook-processing', { redis: { host: 'localhost', port: 6379 } }); app.post('/webhooks/whatsapp', async (req, res) => { await webhookQueue.add('process', req.body); res.status(200).send('OK'); }); webhookQueue.process('process', async (job) => { await handleMessage(job.data); }); -
Monitor Response Times
const responseTime = require('response-time'); app.use(responseTime((req, res, time) => { console.log(`${req.method} ${req.path} ${time}ms`); }));
Issue: High Memory Usage
Symptoms:
- Bot crashes with out of memory
- Slow response times
- High server load
Diagnosis:
# Check memory usage
node --inspect app.js
# Open chrome://inspect in Chrome
# Monitor process
pm2 monit
Solutions:
-
Limit Conversation State
// Don't store unlimited conversation history const MAX_HISTORY = 50; const conversations = new Map(); function addToConversation(phone, message) { let history = conversations.get(phone) || []; history.push({ message, time: Date.now() }); if (history.length > MAX_HISTORY) { history = history.slice(-MAX_HISTORY); } conversations.set(phone, history); } -
Use Redis for State
const redis = require('redis'); const client = redis.createClient(); async function setState(phone, key, value) { await client.hset(`conversation:${phone}`, key, JSON.stringify(value)); } async function getState(phone, key) { const value = await client.hget(`conversation:${phone}`, key); return value ? JSON.parse(value) : null; } -
Implement Cleanup
// Clean up old conversations setInterval(() => { const now = Date.now(); const MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours for (const [phone, data] of conversations.entries()) { if (now - data.lastActivity > MAX_AGE) { conversations.delete(phone); } } }, 60 * 60 * 1000); // Every hour
Security Issues
Issue: Access Token Exposed
Symptoms:
- Token found in logs
- Token in version control
- Unauthorized API usage
Solutions:
-
Use Environment Variables
# .env file (add to .gitignore) WHATSAPP_API_KEY=EAAQdlso6aM8BOwl... WHATSAPP_PHONE_NUMBER_ID=1158433381968079// config.js module.exports = { whatsapp: { apiKey: process.env.WHATSAPP_API_KEY, phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID } }; -
Secure Config File
# Set proper permissions chmod 600 .gbot/config.csv chown bot-user:bot-group .gbot/config.csv -
Rotate Tokens Regularly
In Meta Dashboard: 1. Go to WhatsApp > API Setup 2. Generate new temporary token 3. Update config.csv 4. Restart bot 5. Invalidate old token
Issue: Webhook Abuse
Symptoms:
- Excessive webhook calls
- Spam messages
- High server load
Solutions:
-
Rate Limit Webhooks
const rateLimit = require('express-rate-limit'); const webhookLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minute max: 100, // 100 requests per minute keyGenerator: (req) => { return req.body.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.from || 'unknown'; } }); app.post('/webhooks/whatsapp', webhookLimiter, (req, res) => { // Process webhook }); -
Validate Payload
function validateWebhookPayload(data) { if (!data.object === 'whatsapp_business_account') { return false; } if (!data.entry || !Array.isArray(data.entry)) { return false; } return true; } app.post('/webhooks/whatsapp', (req, res) => { if (!validateWebhookPayload(req.body)) { return res.status(400).send('Invalid payload'); } // Process webhook }); -
Monitor for Anomalies
const messageCounts = new Map(); function checkForAbuse(phoneNumber) { const count = messageCounts.get(phoneNumber) || 0; messageCounts.set(phoneNumber, count + 1); if (count > 100) { console.warn(`Potential abuse from ${phoneNumber}`); // Block or throttle } }
Getting Help
If you're still experiencing issues after following this guide:
-
Check Community Resources
-
Enable Debug Logging
REM Enable detailed logging SET DEBUG MODE TO "verbose" LOG ALL WEBHOOK REQUESTS LOG ALL API RESPONSES -
Collect Diagnostic Information
# Export logs tar -czf whatsapp-debug-$(date +%Y%m%d).tar.gz \ .gbot/logs/ \ /var/log/whatsapp-webhook/ # Include configuration (redact sensitive data) # Include error messages # Include timestamps -
Create Support Ticket
- Include diagnostic tarball
- Describe expected vs actual behavior
- List steps to reproduce
- Include error messages
For the latest troubleshooting information, see Webhook Configuration Guide or Code Examples.