APIs & Integrações16 de fev. de 2026· 9 min leitura

How to Configure Webhooks in WhatsApp Business API: Complete Technical Guide

Practical guide for developers to configure WhatsApp Business API webhooks with Node.js and Python code examples, security validation, and troubleshooting.

If you're starting to integrate the WhatsApp Business API, configuring webhooks is the first real challenge. Without webhooks, your application doesn't receive messages, delivery notifications, or status updates — it's completely blind.

In this guide, I'll walk you through step by step how to configure, validate, and process Meta webhooks, with working code and solutions for the most common issues.

What Are Webhooks and Why You Need Them

Webhooks are callback URLs that Meta automatically calls when something happens on your WhatsApp account. Instead of polling (repeatedly checking for updates), your server receives events in real time.

Events you receive via webhook:

  • Incoming messages — text, image, audio, document, location
  • Delivery status — sent, delivered, read
  • Delivery errors — message undeliverable, invalid number
  • Account changes — template updates, profile changes

Without configured webhooks, your application simply doesn't work as a two-way communication channel.

Anatomy of a Meta Webhook Request

Before implementing, you need to understand the structure Meta sends. Every request is a POST with a JSON payload.

Base Payload Structure

{
  "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": "John Smith" },
                "wa_id": "15559876543"
              }
            ],
            "messages": [
              {
                "from": "15559876543",
                "id": "wamid.xxxxx",
                "timestamp": "1708099200",
                "text": { "body": "Hi, I'd like to know about product X" },
                "type": "text"
              }
            ]
          },
          "field": "messages"
        }
      ]
    }
  ]
}

Delivery Status Event

{
  "value": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15551234567",
      "phone_number_id": "PHONE_NUMBER_ID"
    },
    "statuses": [
      {
        "id": "wamid.xxxxx",
        "status": "delivered",
        "timestamp": "1708099260",
        "recipient_id": "15559876543"
      }
    ]
  }
}

Possible statuses are: sent, delivered, read, and failed.

Step 1: Create the Webhook Endpoint

Your server needs to respond to two types of requests:

  1. GET — Initial verification (Meta confirms your URL is valid)
  2. POST — Real-time event reception

Node.js with Express

const express = require('express');
const app = express();

app.use(express.json());

const VERIFY_TOKEN = process.env.WEBHOOK_VERIFY_TOKEN;

// Webhook verification (GET)
app.get('/webhook', (req, res) => {
  const mode = req.query['hub.mode'];
  const token = req.query['hub.verify_token'];
  const challenge = req.query['hub.challenge'];

  if (mode === 'subscribe' && token === VERIFY_TOKEN) {
    console.log('Webhook verified successfully');
    return res.status(200).send(challenge);
  }

  return res.sendStatus(403);
});

// Event reception (POST)
app.post('/webhook', (req, res) => {
  // IMPORTANT: respond 200 immediately
  res.sendStatus(200);

  const body = req.body;

  if (body.object !== 'whatsapp_business_account') return;

  body.entry?.forEach(entry => {
    entry.changes?.forEach(change => {
      const value = change.value;

      // Process incoming messages
      if (value.messages) {
        value.messages.forEach(message => {
          console.log(`Message from ${message.from}: ${message.text?.body}`);
          // Process the message here (save to DB, reply, etc.)
        });
      }

      // Process delivery statuses
      if (value.statuses) {
        value.statuses.forEach(status => {
          console.log(`Status: ${status.status} for ${status.recipient_id}`);
        });
      }
    });
  });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Python with Flask

from flask import Flask, request, jsonify
import os

app = Flask(__name__)

VERIFY_TOKEN = os.environ.get('WEBHOOK_VERIFY_TOKEN')

@app.route('/webhook', methods=['GET'])
def verify_webhook():
    mode = request.args.get('hub.mode')
    token = request.args.get('hub.verify_token')
    challenge = request.args.get('hub.challenge')

    if mode == 'subscribe' and token == VERIFY_TOKEN:
        print('Webhook verified successfully')
        return challenge, 200

    return 'Forbidden', 403

@app.route('/webhook', methods=['POST'])
def receive_webhook():
    body = request.get_json()

    if body.get('object') != 'whatsapp_business_account':
        return 'OK', 200

    for entry in body.get('entry', []):
        for change in entry.get('changes', []):
            value = change.get('value', {})

            # Process incoming messages
            for message in value.get('messages', []):
                sender = message.get('from')
                text = message.get('text', {}).get('body', '')
                print(f'Message from {sender}: {text}')

            # Process delivery statuses
            for status in value.get('statuses', []):
                print(f"Status: {status['status']} for {status['recipient_id']}")

    return 'OK', 200

if __name__ == '__main__':
    app.run(port=3000)

Step 2: Configure in Meta Business Manager

  1. Go to Meta for Developers and open your app
  2. In the sidebar, navigate to WhatsApp > Configuration
  3. In the Webhook section, click Edit
  4. Enter your Callback URL with your server's public URL (e.g., https://yourdomain.com/webhook)
  5. Enter the Verify token — must be the same value as WEBHOOK_VERIFY_TOKEN on your server
  6. Click Verify and save
  7. After verification, subscribe to fields you want to receive: select at least messages

Meta will send a GET request to your URL with hub.mode, hub.verify_token, and hub.challenge parameters. If your server responds correctly, the webhook is activated.

Step 3: Signature Validation (Security)

Meta signs every request with an HMAC-SHA256 hash using your App Secret. Validating this signature ensures the request actually came from Meta.

Node.js

const crypto = require('crypto');

function validateSignature(req, buf) {
  const signature = req.headers['x-hub-signature-256'];
  if (!signature) return false;

  const appSecret = process.env.META_APP_SECRET;
  const expectedSignature = 'sha256=' +
    crypto.createHmac('sha256', appSecret).update(buf).digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Middleware to validate before processing
app.use('/webhook', express.json({
  verify: (req, res, buf) => {
    if (!validateSignature(req, buf)) {
      throw new Error('Invalid signature');
    }
  }
}));

Python

import hmac
import hashlib

def validate_signature(request):
    signature = request.headers.get('X-Hub-Signature-256', '')
    app_secret = os.environ.get('META_APP_SECRET')

    expected = 'sha256=' + hmac.new(
        app_secret.encode(),
        request.data,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

@app.before_request
def check_signature():
    if request.path == '/webhook' and request.method == 'POST':
        if not validate_signature(request):
            return 'Invalid signature', 403

Never skip signature validation in production. Without it, anyone can send fake requests to your endpoint.

Step 4: Testing Locally with ngrok

During development, you need a public URL. ngrok creates a secure tunnel to your localhost.

# Install ngrok
npm install -g ngrok

# Start your local server
node server.js

# In another terminal, create the tunnel
ngrok http 3000

ngrok will generate a URL like https://abc123.ngrok-free.app. Use this URL in Meta Business Manager as your callback URL.

Important tips:

  • The URL changes every time you restart ngrok (free tier)
  • Use ngrok http 3000 --log=stdout to see requests in real time
  • The ngrok dashboard at http://localhost:4040 shows all received requests — very useful for debugging

Event Structure by Type

Text Message

{
  "type": "text",
  "text": { "body": "Message content" }
}

Image Message

{
  "type": "image",
  "image": {
    "id": "MEDIA_ID",
    "mime_type": "image/jpeg",
    "sha256": "HASH",
    "caption": "Optional caption"
  }
}

Audio Message

{
  "type": "audio",
  "audio": {
    "id": "MEDIA_ID",
    "mime_type": "audio/ogg"
  }
}

Document Message

{
  "type": "document",
  "document": {
    "id": "MEDIA_ID",
    "mime_type": "application/pdf",
    "filename": "proposal.pdf"
  }
}

Location Message

{
  "type": "location",
  "location": {
    "latitude": 37.7749,
    "longitude": -122.4194,
    "name": "San Francisco",
    "address": "Market Street, 1000"
  }
}

To download media (image, audio, document), use the GET /{media_id} endpoint with your access token to get the download URL.

Troubleshooting: Common Issues

Webhook not receiving events

Cause 1: Invalid or self-signed SSL Meta requires a valid SSL certificate. Self-signed certificates don't work. Use Let's Encrypt (free) or a service like Cloudflare.

Cause 2: Server not responding 200 in time Your endpoint must return 200 OK in less than 20 seconds. If it takes longer, Meta considers it a failure.

Cause 3: Not subscribed to the correct fields After configuring the webhook, you need to subscribe to fields. Go to WhatsApp > Configuration > Webhook and check the desired fields (e.g., messages).

Meta retries the webhook and duplicates messages

Meta automatically retries when it doesn't receive 200. To avoid duplicate processing:

const processedMessages = new Set();

app.post('/webhook', (req, res) => {
  res.sendStatus(200); // Respond IMMEDIATELY

  req.body.entry?.forEach(entry => {
    entry.changes?.forEach(change => {
      change.value.messages?.forEach(message => {
        // Idempotency: ignore already-processed messages
        if (processedMessages.has(message.id)) return;
        processedMessages.add(message.id);

        // Process the message asynchronously
        processMessageAsync(message).catch(console.error);
      });
    });
  });
});

In production, replace the Set with Redis or a database to persist between restarts.

Error 400 on verification

Check that WEBHOOK_VERIFY_TOKEN on your server is exactly the same as what you configured in Meta Business Manager. Extra spaces or invisible characters are a common cause.

Implementation Best Practices

1. Respond 200 Before Processing

The most important rule: return 200 immediately and process the event asynchronously. If you process before responding and take more than 20 seconds, Meta will consider it a failure and retry.

app.post('/webhook', (req, res) => {
  res.sendStatus(200); // First: respond

  // Then: process in background
  processWebhookEvent(req.body)
    .catch(err => console.error('Error processing webhook:', err));
});

2. Implement Idempotency

Meta may send the same event more than once (retries). Use the message.id or status.id as an idempotency key to avoid processing duplicates.

3. Use Queues for Processing

In high-volume applications, put events in a queue (Redis Queue, Bull, SQS) instead of processing inline:

const Queue = require('bull');
const webhookQueue = new Queue('webhook-events', process.env.REDIS_URL);

app.post('/webhook', (req, res) => {
  res.sendStatus(200);
  webhookQueue.add(req.body);
});

webhookQueue.process(async (job) => {
  await processWebhookEvent(job.data);
});

4. Monitor and Log

Log all received events for debugging and auditing. When issues arise, logs are your first source of diagnosis.

5. Handle Every Message Type

Don't assume every message is text. Always check the type field before accessing data:

function handleMessage(message) {
  switch (message.type) {
    case 'text':
      return handleText(message.text.body);
    case 'image':
      return handleMedia(message.image);
    case 'audio':
      return handleMedia(message.audio);
    case 'document':
      return handleMedia(message.document);
    case 'location':
      return handleLocation(message.location);
    default:
      console.log(`Unsupported type: ${message.type}`);
  }
}

Conclusion

Webhooks are the foundation of any functional WhatsApp Business API integration. With a properly configured endpoint, security validation implemented, and async processing, your application will be ready to receive and process messages in real time.

Key takeaways:

  • Respond 200 immediately — process afterwards
  • Validate the signature — never trust unverified requests
  • Implement idempotency — Meta retries
  • Use valid HTTPS — self-signed certificates don't work
  • Test with ngrok — validate locally before deploying

If you're just getting started with the WhatsApp Business API, also check out my guide on how to create an App on Meta for Developers to set up the entire environment before implementing webhooks.

Artigos Relacionados

  • APIs & Integrações

    Como Configurar Webhooks na WhatsApp Business API: Guia Técnico Completo

  • APIs & Integrações

    WhatsApp Business API: Guia Completo Para Empresas em 2026

  • APIs & Integrações

    Rate Limits WhatsApp API: Como Evitar Bloqueios e Escalar

.