WhatsApp Webhooks: Receive Messages in Real Time

Aug 18, 2026  •  5 min read
WhatsApp Webhooks: Receive Messages in Real Time

Sending WhatsApp messages is the easy half. Receiving them is where most integrations quietly fall apart.

The naive approach is to poll: hit a /messages endpoint every few seconds and diff the results. It works in development and becomes a problem the moment you have more than a handful of conversations. You burn requests, you get messages seconds or minutes late, and you still miss things when two arrive between polls.

Webhooks invert the relationship. Instead of you asking, the platform tells you. A message arrives on your WhatsApp number, and within a second your server gets an HTTP POST describing what happened.

How a WhatsApp webhook actually works

Four steps, and only one of them is yours to build.

You expose an HTTPS endpoint that accepts POST requests. You register that URL with your WhatsApp API provider. When something happens on your number, the provider serializes it as JSON and POSTs it to your URL. Your endpoint returns a 2xx status quickly, and the provider marks it delivered.

That last part matters more than it looks. Everything about webhook reliability follows from the fact that the sender is waiting for your response.

There is a meaningful difference in what you receive depending on which kind of API you are on. The official Cloud API delivers events through Meta's infrastructure with its own verification handshake and payload structure, wrapped in entry and changes arrays that take some getting used to. Session-based providers deliver a flatter payload closer to what actually happened. The comparison between the two covers the wider trade-offs, but for webhook purposes the practical difference is that session-based events cover more ground: reactions, revocations and connection state have no clean equivalent on the official platform.

The events worth handling

A typical session-based API emits two families of events.

Message events.

EventFires when
message.receivedSomeone sends you a message
message.sentYour outbound message leaves the queue
message.ackDelivery state changes: sent, delivered, read
message.failedAn outbound message could not be delivered
message.revokedThe sender deleted a message for everyone
message.reactionSomeone reacts to a message with an emoji

Session events.

EventFires when
session.statusConnection state changes
session.qrA new QR code is issued for linking
session.authenticatedThe number successfully links
session.disconnectedThe session drops

Most people wire up message.received and stop. That is a mistake, and the event people regret skipping is session.disconnected. A dropped session is silent: your sends queue, nothing errors loudly, and you discover the problem when a customer complains. Route that event to whatever wakes your team up.

message.ack is the other underused one. It carries the delivery lifecycle, which is what you need for anything resembling a delivery report or a "message not delivered, try SMS" fallback.

Payload structure

A message.received payload looks like this:


json

{
"event": "message.received",
"timestamp": 1693420800,
"data": {
"from": "+1234567890",
"to": "+0987654321",
"type": "text",
"body": "Hello, world!",
"isGroup": false
}
}

Three fields deserve attention.

type tells you what you are dealing with: text, image, video, audio, document. Branch on it early, because a media event carries a URL or reference rather than a body, and code that assumes body exists will throw on the first photo somebody sends.

isGroup distinguishes a group message from a direct one. If you are running an autoresponder, check this before replying. A bot that answers every message in a 200-person group is a bad afternoon for everyone.

timestamp is when the event happened, not when you received it. Under retry conditions those can differ by minutes. Use it, not Date.now(), when ordering matters.

Verifying the signature

This is the section to not skip. Your webhook endpoint is a public URL that accepts POST requests. Without verification, anyone who guesses it can inject fake customer messages into your system, and if those messages drive automated actions, that is a real vulnerability rather than a theoretical one.

The standard approach is HMAC. You configure a shared secret, the provider computes an HMAC-SHA256 of the raw request body using that secret and sends it in a header, and you recompute it and compare.

Node.js with Express:


js

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

// Raw body is required. Parsed JSON will not match the signature.
app.use('/webhooks/whatsapp', express.raw({ type: 'application/json' }));

app.post('/webhooks/whatsapp', (req, res) => {
const signature = req.get('X-Wahttp-Signature');
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');

const valid =
signature &&
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

if (!valid) return res.sendStatus(401);

res.sendStatus(200); // acknowledge first
handleEvent(JSON.parse(req.body)); // process after
});

Python with Flask:


python

import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/webhooks/whatsapp")
def whatsapp_webhook():
signature = request.headers.get("X-Wahttp-Signature", "")
expected = hmac.new(
os.environ["WEBHOOK_SECRET"].encode(),
request.get_data(), # raw bytes, not request.json
hashlib.sha256,
).hexdigest()

if not hmac.compare_digest(signature, expected):
abort(401)

enqueue(request.get_json())
return "", 200

Two failure modes account for nearly every "my signature never matches" support ticket.

Using the parsed body instead of the raw one. JSON parsing and re-serialization changes key order and whitespace, which changes the hash. Capture bytes before any middleware touches them.

Using == instead of a constant-time comparison. A normal string comparison returns as soon as it finds a mismatched character, which leaks timing information an attacker can use to derive the signature byte by byte. Use timingSafeEqual or compare_digest.

Building an endpoint that survives production

Acknowledge immediately, process afterwards. Return 200 within a couple of hundred milliseconds and push the payload onto a queue. If you call a language model, write to three tables and hit a CRM before responding, you will eventually exceed the provider's timeout, get marked as failed, and receive the same event again while the first one is still running.

Expect duplicates and design for them. Retries mean at-least-once delivery, not exactly-once. Store a message ID, check it before acting, discard repeats. Without this, a network blip becomes two order confirmations to the same customer.

Return the right status codes. A 2xx means delivered. A 4xx tells the provider not to bother retrying, which is correct for a payload you will never be able to process. A 5xx or a timeout triggers the retry schedule, which is what you want during a transient outage. Returning 200 on an error you could have recovered from throws the event away permanently.

Never trust the payload's content. A from field is not proof of identity beyond the signature you already verified, and message bodies are arbitrary user input. If webhook content reaches a language model, remember that anyone who can message your number can put text in front of it.

Log everything for the first month. Store raw payloads with timestamps. Every webhook bug is easier to diagnose with the actual bytes in front of you, and good providers keep delivery logs on their side too, which turns "did it fire?" into a question you can answer in seconds rather than an argument.

Testing locally

Your development machine has no public URL, so use a tunnel:


bash

ngrok http 3000

Register the resulting HTTPS URL as your webhook endpoint. Requests hit ngrok and get forwarded tolocalhost:3000, and the inspector at http://127.0.0.1:4040 lets you replay any request, which is far faster than messaging your own number repeatedly.

For the parts where the tunnel is not the problem, most providers include a message tester and webhook delivery logs in the dashboard. Sending a test event and watching where it lands isolates the failure quickly, and the feature overview shows what those tools look like.

When events stop arriving

Work through it in this order.

Is the session still connected? A dropped session emits no message events because there are no messages. Check status before anything else.

Is your endpoint reachable from outside your network? curl It is from a machine that is not yours. Localhost URLs, VPN-only hosts, and self-signed certificates all fail here.

Are you returning 2xx? Check the provider's delivery log. Repeated failures cause some providers to back off or disable the endpoint entirely.

Is a proxy consuming the body? Cloudflare, load balancers, and API gateways can transform requests in ways that break signature verification. Compare the bytes your handler receives against what was sent.

Is the event type registered? Endpoints often filter which events they receive. If message.ack never arrives, check it is subscribed rather than assuming it is broken.

Exact header names and endpoint paths do change between versions, so confirm against the API reference rather than copying a blog post, including this one.

Common Questions Asked by Users

What is a WhatsApp webhook? An HTTP callback. You register a URL, and the WhatsApp API POSTs a JSON payload to it whenever an event occurs on your number, so you learn about incoming messages in real time instead of polling.

How do I receive WhatsApp messages through an API? Register a webhook endpoint and handle the message.received event. Polling is possible but slower, heavier, and prone to gaps.

How do I verify a webhook signature? Compute an HMAC-SHA256 of the raw request body using your shared secret, and compare it to the signature header with a constant-time comparison. Use raw bytes rather than parsed JSON, or the hash will never match.

How do I track WhatsApp message delivery status? Subscribe to message.ack, which reports the transition through sent, delivered and read, and message.failed for messages that could not be delivered.

What happens if my endpoint is down? Providers retry on a backoff schedule. Events are usually queued for a period and delivered when you recover, though sustained failures can disable the endpoint. Check the delivery log after any outage.

Can I receive group messages? Yes. Group messages arrive through the same events with isGroup set to true. Filter on it before triggering any automated reply.

Do I need HTTPS? Yes. Providers require it, and you are transmitting customer message content, so it would be necessary regardless.

WhatsApp