(302) 414-9101
1001 S Main St, STE 600, Kalispell, MT 59901
contact@zarghamlabs.com

How to Set Up WhatsApp Webhooks: Receiving Messages in Real Time in 2026

What Is a WhatsApp Webhook and Why Do You Need One?

A WhatsApp webhook is an HTTP callback that Meta’s servers send to your application whenever something happens: a customer sends a message, reads a message, clicks a button, or reacts to a template. Instead of your server polling the WhatsApp API constantly asking “any new messages?”, webhooks push data to you the moment events occur — in real time, at scale, with no unnecessary API calls.

If you’re building any form of WhatsApp automation — a chatbot, a CRM integration, an order management system, or a customer support platform like Messenjo — webhooks are the foundation. This guide walks through the complete setup process from Meta’s perspective and your server’s perspective.

Prerequisites

Before setting up your webhook, you need: a Meta Developer account, a WhatsApp Business App created in the Meta Developer Portal, a WhatsApp Business API phone number, and a publicly accessible HTTPS endpoint (your webhook URL). Local development is possible using ngrok or Cloudflare Tunnel to expose your localhost.

Step 1: Create Your Webhook Endpoint

Your webhook URL must handle two types of requests: a GET request for initial verification, and POST requests for incoming event data. Here’s a minimal FastAPI implementation:

from fastapi import FastAPI, Request, HTTPException
import hashlib
import hmac
import json

app = FastAPI()

WEBHOOK_VERIFY_TOKEN = "your_verify_token_here"
WHATSAPP_APP_SECRET = "your_app_secret_here"

@app.get("/webhook")
async def verify_webhook(
    hub_mode: str = None,
    hub_verify_token: str = None,
    hub_challenge: str = None
):
    """Meta calls this to verify your webhook endpoint."""
    if hub_mode == "subscribe" and hub_verify_token == WEBHOOK_VERIFY_TOKEN:
        return int(hub_challenge)
    raise HTTPException(status_code=403, detail="Verification failed")

@app.post("/webhook")
async def receive_webhook(request: Request):
    """Meta sends all WhatsApp events here."""
    # Verify signature
    body = await request.body()
    signature = request.headers.get("X-Hub-Signature-256", "")
    expected = "sha256=" + hmac.new(
        WHATSAPP_APP_SECRET.encode(),
        body,
        hashlib.sha256
    ).hexdigest()
    
    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=401, detail="Invalid signature")
    
    payload = json.loads(body)
    await process_webhook(payload)
    return {"status": "ok"}

Step 2: Register Your Webhook in Meta Developer Portal

In the Meta Developer Portal, navigate to your WhatsApp App → Configuration → Webhooks. Enter your webhook URL and verify token. Click “Verify and Save” — Meta will immediately send a GET request to your endpoint. If it returns the challenge correctly, the webhook is verified.

Then subscribe to the webhook fields you need. For a full WhatsApp integration, subscribe to: messages (incoming messages), message_deliveries (delivery status), message_reads (read receipts), message_reactions (emoji reactions), and messaging_postbacks (button clicks).

Step 3: Parse the Webhook Payload

Every WhatsApp webhook payload follows the same outer structure. The inner message object varies by type:

// Outer structure (always the same)
{
  "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": "15559876543" }],
        "messages": [
          // message object here — varies by type
        ]
      },
      "field": "messages"
    }]
  }]
}

Text Message Payload

{
  "id": "wamid.xxx",
  "from": "15559876543",
  "timestamp": "1700000000",
  "type": "text",
  "text": { "body": "Hello, I need help with my order" }
}

Interactive Button Reply Payload

{
  "id": "wamid.xxx",
  "from": "15559876543",
  "timestamp": "1700000000",
  "type": "interactive",
  "interactive": {
    "type": "button_reply",
    "button_reply": {
      "id": "btn_confirm_order",
      "title": "Confirm Order"
    }
  }
}

Status Update Payload (delivery/read)

{
  "id": "wamid.xxx",
  "status": "read",  // sent | delivered | read | failed
  "timestamp": "1700000000",
  "recipient_id": "15559876543"
}

Step 4: Build Your Message Router

A clean webhook handler routes incoming events to the right processor based on message type:

async def process_webhook(payload: dict):
    entry = payload.get("entry", [])
    for entry_item in entry:
        for change in entry_item.get("changes", []):
            value = change.get("value", {})
            
            # Handle incoming messages
            for message in value.get("messages", []):
                msg_type = message.get("type")
                
                if msg_type == "text":
                    await handle_text_message(message, value)
                elif msg_type == "interactive":
                    await handle_button_reply(message, value)
                elif msg_type == "image":
                    await handle_image_message(message, value)
                elif msg_type == "document":
                    await handle_document_message(message, value)
                elif msg_type == "location":
                    await handle_location_message(message, value)
            
            # Handle status updates
            for status in value.get("statuses", []):
                await handle_status_update(status)

Step 5: Secure Your Webhook

Never skip signature verification. Every legitimate POST from Meta includes an X-Hub-Signature-256 header containing an HMAC-SHA256 hash of the request body signed with your App Secret. Always verify this before processing any payload — without it, anyone who knows your webhook URL can send fake events.

Additional security practices: use a non-guessable webhook path (not /webhook but something like /wh/a7f3b2c9), rate-limit your endpoint, and return 200 OK immediately even before processing (process asynchronously via a queue) — Meta will retry if it doesn’t get a 200 within 20 seconds.

Handling Webhook Retries

Meta retries failed webhooks up to 24 hours. This means your processing logic must be idempotent — processing the same message twice should produce the same result, not duplicate records. Always check if a message ID has already been processed before acting on it:

async def handle_text_message(message: dict, value: dict):
    message_id = message["id"]
    
    # Idempotency check
    if await redis.exists(f"processed:{message_id}"):
        return  # Already handled
    
    # Process message
    await save_message_to_db(message, value)
    await trigger_bot_response(message, value)
    
    # Mark as processed (expire after 48 hours)
    await redis.setex(f"processed:{message_id}", 172800, "1")

Testing Your Webhook Locally

Use ngrok to expose your local development server to the internet during testing:

# Install ngrok and expose port 8000
ngrok http 8000

# Your webhook URL will be something like:
# https://abc123.ngrok-free.app/webhook

# Test with a curl simulation
curl -X POST https://abc123.ngrok-free.app/webhook \
  -H "Content-Type: application/json" \
  -d '{"object":"whatsapp_business_account","entry":[...]}'

The Meta Developer Portal also has a Webhooks test tool that sends sample payloads of each event type directly to your endpoint without requiring a real WhatsApp message.

Common Webhook Issues and Fixes

Issue Likely Cause Fix
Verification failing Returning string instead of int for challenge Return int(hub_challenge) not the string
Signature mismatch Using wrong app secret (use App Secret, not Token) Find App Secret in App Settings → Basic
Not receiving messages Not subscribed to “messages” field Enable in Webhooks → Manage Subscriptions
Duplicate processing No idempotency check, Meta retried Store message IDs in Redis with TTL
Timeouts (Meta stops retrying) Processing takes >20s before returning 200 Return 200 immediately, process via Celery task

Using a Platform Instead of Building from Scratch

Building webhook infrastructure from scratch is the right choice if you’re embedding WhatsApp into your own product. If you’re building a customer-facing automation platform, Messenjo handles all webhook infrastructure, message routing, and event processing for you — so you can focus on the automation logic rather than the plumbing.

For related technical deep-dives, see our guides on WhatsApp Business API Access Setup, FastAPI development services, and n8n automation integration.

Leave A Comment