Skip to main content

Overview

The WhatsApp webhook integration receives real-time notifications from Meta’s WhatsApp Business API. It handles webhook verification, signature validation, and message processing through a BullMQ queue system. Endpoint: /api/whatsapp/webhook

Webhook Verification (GET)

Meta requires webhook verification during initial setup. This endpoint handles the verification challenge.

Request

GET /api/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=CHALLENGE_STRING
hub.mode
string
required
Must be subscribe for webhook verification
hub.verify_token
string
required
Verification token configured in WHATSAPP_VERIFY_TOKEN environment variable
hub.challenge
string
required
Random string provided by Meta to echo back

Response

200 OK
CHALLENGE_STRING
Returns the challenge string if verification succeeds, or 403 Forbidden if token mismatch occurs.

Example Implementation

server.mjs:97
import whatsappWebhook from './backend/whatsapp/webhook.js';

app.use('/api/whatsapp', whatsappWebhook);

Incoming Messages (POST)

Receives incoming WhatsApp messages from Meta’s webhook notifications.

Security: Signature Validation

All incoming webhooks must include an X-Hub-Signature-256 header for request integrity verification.
webhook.js:12-49
function validateSignature(req, res, next) {
    const signature = req.headers['x-hub-signature-256'];
    
    if (!signature) {
        console.warn("⚠️ Missing X-Hub-Signature-256");
        if (process.env.NODE_ENV === 'production' || APP_SECRET) {
             return res.status(401).send("Missing Signature");
        }
    }

    if (APP_SECRET && signature) {
        const elements = signature.split('=');
        const signatureHash = elements[1];
        
        // Use raw body for HMAC verification
        const payload = req.rawBody || JSON.stringify(req.body);
        
        const expectedHash = crypto.createHmac('sha256', APP_SECRET)
                                   .update(payload)
                                   .digest('hex');

        if (signatureHash !== expectedHash) {
             console.error("❌ Invalid Signature");
        }
    }
    
    next();
}
The signature is computed using HMAC-SHA256 with your WHATSAPP_APP_SECRET against the raw request body. Ensure req.rawBody is available via middleware.

Raw Body Middleware

To enable signature validation, configure Express to capture raw body:
server.mjs:61-65
app.use(express.json({
    verify: (req, res, buf) => {
        req.rawBody = buf;
    }
}));

Request Body

object
string
required
Must be whatsapp_business_account for WhatsApp events
entry
array
required
Array of webhook entries containing message data

Response

200 OK
The endpoint immediately returns 200 OK to prevent Meta webhook timeouts, then processes the message asynchronously via BullMQ.

Message Processing Flow

webhook.js:71-105
router.post('/webhook', validateSignature, async (req, res) => {
    // IMMEDIATE 200 OK
    res.sendStatus(200);

    const body = req.body;

    if (body.object === 'whatsapp_business_account') {
        try {
            const entry = body.entry?.[0];
            const changes = entry?.changes?.[0];
            const value = changes?.value;
            const message = value?.messages?.[0];

            if (message && message.type === 'text') {
                const wamid = message.id;
                
                // Add to Queue for processing
                await whatsappQueue.add('process-message', {
                    wamid,
                    from: message.from,
                    text: message.text.body,
                    timestamp: message.timestamp
                }, {
                    jobId: wamid, // Deduplication via BullMQ
                    removeOnComplete: true
                });
                
                console.log(`📥 Queued message from ${message.from}: ${wamid}`);
            }
        } catch (error) {
            console.error("❌ Error encolando mensaje:", error.message);
        }
    }
});
Messages are processed asynchronously after responding to Meta. This prevents webhook timeouts but requires proper error handling in the queue worker.

Example Webhook Payload

{
  "object": "whatsapp_business_account",
  "entry": [
    {
      "id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
      "changes": [
        {
          "value": {
            "messaging_product": "whatsapp",
            "metadata": {
              "display_phone_number": "15551234567",
              "phone_number_id": "PHONE_NUMBER_ID"
            },
            "contacts": [
              {
                "profile": {
                  "name": "Customer Name"
                },
                "wa_id": "573001234567"
              }
            ],
            "messages": [
              {
                "from": "573001234567",
                "id": "wamid.HBgNNTczMDA...",
                "timestamp": "1677649200",
                "text": {
                  "body": "Hola, necesito ayuda con mi pedido"
                },
                "type": "text"
              }
            ]
          },
          "field": "messages"
        }
      ]
    }
  ]
}

Environment Variables

WHATSAPP_APP_SECRET
string
required
App secret from Meta for HMAC signature validation
WHATSAPP_VERIFY_TOKEN
string
required
Custom verification token for webhook setup
REDIS_URL
string
Redis connection URL for BullMQ queue (optional, defaults to localhost)

Error Handling

ScenarioStatus CodeResponse
Missing signature (production)401Missing Signature
Invalid signature403Invalid Signature
Verification token mismatch403(empty)
Missing verification params400(empty)
Valid webhook200(empty)
Valid verification200Challenge string

Next Steps

Queue System

Learn how messages are processed via BullMQ

Session Management

Understand session lifecycle and bot handover

Build docs developers (and LLMs) love