How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a WhatsApp webhook?
A WhatsApp webhook is an HTTP POST that Meta sends to the callback URL configured on a Meta app when something happens on a WhatsApp Business Account the app is subscribed to. Two things dominate: a customer sends your number a message, or a message you sent changes status to sent, delivered, read or failed. Both arrive under the same subscription field, messages.
The envelope is generic to all Meta products, which explains its depth. object names the product, entry is a list of accounts, each entry has a list of changes, and each change has a field and a value. For WhatsApp the value holds either a messages array (incoming) or a statuses array (updates on outgoing messages), alongside metadata identifying which of your phone numbers was involved.
What a WhatsApp webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
X-Hub-Signature-256: sha256=d57c68ca6f92289e6987922ff26938930f6e66a2d161ef06abdf1859230aa23c
X-Hub-Signature: sha1=7d38cdd689735b008b3c702edd92eea23791c5f6
Content-Type: application/json
User-Agent: facebookexternalua
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "15550783881",
"phone_number_id": "106540352242922"
},
"contacts": [
{
"profile": {
"name": "Ada Lovelace"
},
"wa_id": "16505551234"
}
],
"messages": [
{
"from": "16505551234",
"id": "wamid.HBgLMTY1MDU1NTEyMzQVAgASGBQzQUVYQU1QTEUwMDAwMDAwMDAwMQA=",
"timestamp": "1767225600",
"type": "text",
"text": {
"body": "Is my order on the way?"
}
}
]
}
}
]
}
]
}
The message is at entry[0].changes[0].value.messages[0], and all three lists can hold more than one item, so loop rather than index. The content sits under a key named after type: text, image, audio, document, location, interactive and so on. timestamp is a string of Unix seconds. To reply you need metadata.phone_number_id (the sender on your side) and from (the customer), and media arrives as an id to download through the API, not as a URL.
Headers WhatsApp sends
| Header | Example | What it is |
|---|---|---|
X-Hub-Signature-256 | sha256=d57c68ca6f92289e6987922ff26938930f6e… | HMAC-SHA256 of the body, hex, prefixed sha256=, keyed with the app secret. |
X-Hub-Signature | sha1=7d38cdd689735b008b3c702edd92eea23791c5f6 | Legacy HMAC-SHA1 of the same body. Verify the SHA-256 one instead. |
Content-Type | application/json | The body is JSON. |
User-Agent | facebookexternalua | Identifies the sender. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How WhatsApp webhooks are signed
Meta computes an HMAC-SHA256 of the request body keyed with your app secret — the secret of the Meta app, found in its basic settings, not an access token — and sends sha256= plus the hex digest in X-Hub-Signature-256. No timestamp is included, so de-duplicate on the message id if a replay would matter.
One detail trips up hand-written checks: Meta signs the body as it sends it, in which non-ASCII characters are written as escaped Unicode sequences. Verify against the bytes that arrived and the question never comes up; decode and re-encode the JSON first and an emoji is enough to break the match. Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function.
const crypto = require('crypto');
// rawBody: Buffer or string of the body exactly as received
function verifyWhatsApp(rawBody, header, appSecret) {
const expected = 'sha256=' +
crypto.createHmac('sha256', appSecret).update(rawBody).digest('hex');
const a = Buffer.from(header || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
The verification request, retries and duplicates
Saving the callback URL triggers a GET request with three query parameters: hub.mode=subscribe, hub.verify_token holding a string you typed into the same form, and hub.challenge holding a number. The endpoint must confirm the token is the one it expects and respond 200 with the challenge value as the body. The webhook URLs created on this site return the challenge for any such request without checking the verify token, since they cannot know what you typed; that is fine for inspecting traffic and wrong for a real endpoint.
Each notification must be answered with a 200. If it is not, Meta retries with decreasing frequency for up to several days, per its documentation at the time of writing, so a handler that was down receives a backlog when it returns. Deliveries can repeat and can arrive out of order: a read status may land before delivered. Meta may also group several changes into one request.
Catch a real WhatsApp webhook
- Press Create webhook URL at the top of this page and copy the address.
- In the Meta developer dashboard, open a test app that has the WhatsApp product added and go to the WhatsApp Configuration page.
- Edit the webhook, paste the address as the callback URL, type any string as the verify token, and confirm. The
GETchallenge shows up in the inbox and Meta accepts the URL. - Subscribe to the
messagesfield. The dashboard can send a sample payload for a field, which is the fastest first test. - Send a WhatsApp message from your own phone to the app's test number and read the notification in the inbox above.
Use the test number that comes with the app. Message webhooks contain phone numbers, names and message text; a public inbox is not a place for conversations with real customers.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- Incoming messages and status updates share a field. Check whether
value.messagesorvalue.statusesis present before reading either. - The app secret is not the access token. Signatures are keyed with the app secret; the token only authorises API calls.
- The same envelope serves other Meta products. Messenger and Instagram webhooks use the identical verification request and signature header with a different
objectandvalue.
api-tester.com is independent and is not affiliated with WhatsApp. Providers change header names, limits and retry schedules; treat the official WhatsApp documentation as the source of truth.
Related examples and tools
Dropbox Webhook Example
A Dropbox webhook tells you who has changes and nothing about what changed.
Asana Webhook Example
Asana hands you the signing secret instead of asking for one: it arrives once, in a header, during a handshake your endpoint has to answer.
Okta Webhook Example
An Okta event hook delivers System Log events to your endpoint, authenticated by a header and secret you choose rather than by a signature.
Stripe Webhook Example
What Stripe actually sends to your endpoint — the event envelope, the Stripe-Signature header and the retry behaviour — with a webhook URL you can create here to catch a real one.
GitHub Webhook Example
The headers and JSON GitHub posts when a repository event fires, how X-Hub-Signature-256 is computed, and what happens when your endpoint is down.
Facebook Graph API Tester
Call the Graph API from the browser — the same API that sends WhatsApp replies.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks signed with an HMAC of the body
References
- Official WhatsApp documentation — The provider's own guide to the requests described on this page.
- Meta for Developers: get started with webhooks — The verification request and payload validation, shared by all Meta webhooks.
- Meta for Developers: messages webhook reference — The structure of the messages field.
- RFC 2104: HMAC — The keyed-hash construction this signature is built on.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
WhatsApp webhook FAQ
What does a WhatsApp webhook payload look like?
A JSON object with object set to whatsapp_business_account and an entry array. Each entry has changes, and each change has a field and a value containing messaging_product, metadata, and either a messages array for incoming messages or a statuses array for delivery updates.
How do I verify a WhatsApp webhook URL?
Meta sends a GET request with hub.mode, hub.verify_token and hub.challenge. Check that hub.mode is subscribe and that the verify token matches the one you configured, then respond with status 200 and the hub.challenge value as the body.
How do I validate X-Hub-Signature-256 from WhatsApp?
Compute an HMAC-SHA256 of the raw request body using the Meta app secret, hex-encode it, prefix it with sha256= and compare it with the header in constant time.
Where is the message text in a WhatsApp webhook?
At entry[0].changes[0].value.messages[0].text.body for a text message. Other message types put their content under a key matching the type, such as image or location.
Does WhatsApp retry failed webhooks?
Yes. If the endpoint does not answer 200, Meta retries with decreasing frequency for up to several days according to its documentation at the time of writing, so handlers must tolerate duplicates.