WhatsApp Webhook Setup: How to Receive Incoming Messages with the Cloud API
A WhatsApp webhook is how you receive replies and delivery statuses. The verify token handshake, the messages field, the payload shape, and the six reasons a webhook goes silent.
A WhatsApp webhook is the endpoint Meta calls on your server whenever someone messages your business number or the status of a message you sent changes. Without one, the Cloud API is send-only: you can push template messages out and you will never see a single reply. Setting it up takes two things, a public HTTPS URL that answers a verification handshake, and a subscription to the messages field on your app. Everything else is detail.
This is the half of the WhatsApp Business Platform that most guides skip. There is plenty written about sending a message with the API, and very little about receiving one, which is why so many implementations stall at the point where a customer replies into a void.
What a WhatsApp webhook actually does
Meta's own description of the field you subscribe to is precise, and worth reading twice: the messages webhook "describes messages sent from a WhatsApp user to a business and the status of messages sent by a business to a WhatsApp user." That is two distinct jobs in one subscription. Inbound customer messages arrive there, and so do the delivery receipts for your outbound templates, the sent, delivered, read and failed statuses that tell you whether the campaign you paid for actually landed.
The direction matters. Your server never polls WhatsApp for new messages, because there is no endpoint to poll. Meta pushes to you. If your endpoint is down when a customer writes in, the message does not queue up somewhere for you to collect later, it goes into Meta's retry schedule, and your application only learns about it when a retry succeeds.
The two requests Meta sends your endpoint
One URL has to handle two completely different HTTP methods, and mixing them up is the single most common setup mistake.
| Request | When | What your endpoint must do |
|---|---|---|
| GET verification | Once, when you save the callback URL in the App Dashboard, and again whenever you change it | Read the hub.verify_token query parameter, confirm it matches the token you configured, then respond with the value of hub.challenge and nothing else |
| POST notification | Every inbound message and every status change, for the lifetime of the integration | Return HTTP 200 immediately, then process the payload |
The GET request carries three parameters: hub.mode, which is always set to subscribe, hub.challenge, which is an integer, and hub.verify_token, which is the string you chose in the dashboard. The verify token is not a secret Meta issues to you. You invent it, you paste the same value into the App Dashboard, and its only job is to prove that the endpoint answering is one you control. Any random string works, and a long one is better than the word "test", which a surprising number of production integrations still use.
How to set up a WhatsApp webhook
- Put a public HTTPS URL in front of your handler. Meta requires a valid TLS certificate, and its documentation is explicit that "self-signed certificates are not supported." A localhost address will never work, so during development you need a tunnel that terminates TLS with a publicly trusted certificate.
- Handle the GET. Compare the incoming verify token to yours. If it matches, return the
hub.challengevalue as the plain response body. Returning JSON, or wrapping the challenge in quotes, will fail verification. - Save the callback URL and verify token in the WhatsApp configuration of your Meta app. Meta calls the GET endpoint at that moment, so a handler that is not deployed yet means the save fails.
- Subscribe to the messages field. Verification alone subscribes you to nothing. If you skip this step the endpoint verifies cleanly and then stays silent forever, which is the most confusing possible failure mode.
- Return 200 before you do any work. Acknowledge first, queue the payload, process it in a background job. Anything slow inside the request, a database write, an AI call, a CRM sync, is a timeout waiting to happen.
What the WhatsApp webhook payload looks like
The structure is nested more deeply than people expect, and both entry and changes are arrays, so a single request can carry several events. Code that reads entry[0].changes[0] and stops will silently drop messages under load.
{
"object": "whatsapp_business_account",
"entry": [{
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": { "display_phone_number": "...", "phone_number_id": "..." },
"contacts": [{ "profile": { "name": "Jane Doe" }, "wa_id": "14155550132" }],
"messages": [{
"from": "14155550132",
"id": "wamid....",
"timestamp": "1786600000",
"type": "text",
"text": { "body": "Do you ship to Texas?" }
}]
}
}]
}]
}
Two fields do the real work later. The wa_id is the customer's WhatsApp identity and the value you send replies back to. The timestamp starts the clock on the 24 hour customer service window, inside which you can reply in free-form text without an approved template. Note that from October 1, 2026 those free-form replies become chargeable as service messages, so a lot of advice written before that date understates the running cost of a busy inbox.
Why is my WhatsApp webhook not receiving messages?
In practice it is almost always one of six things, roughly in order of how often they bite:
- You verified but never subscribed to the messages field. The dashboard shows a green tick for the callback URL, which reads like success. Subscription is a separate toggle.
- The app is in development mode and the sender is not listed as a test number on the app.
- Your endpoint is not returning 200. A 500, a redirect, or a 401 from authentication middleware sitting in front of the route all count as failure. Webhook routes usually need to be excluded from CSRF protection and from any login requirement.
- The certificate is wrong. A chain that is missing an intermediate will work in a browser and fail from Meta's servers.
- The number is still attached to the WhatsApp Business app rather than registered to the Cloud API, so the message never enters the Platform at all.
- You are looking at the wrong app. With several apps or business portfolios in one account, it is easy to subscribe the one that does not own the phone number.
Worth knowing before you panic: a failed delivery is not a lost message. Meta "retries delivery with decreasing frequency until the request succeeds, for up to 7 days." So an endpoint that was broken for an afternoon will usually receive the backlog once it recovers, often as a burst that your handler needs to survive. That retry behaviour is also an argument for keeping the endpoint reachable across releases, since a deploy that drops in-flight requests turns every release into a small outage. Teams that care about this tend to move to a deployment setup that swaps versions without dropping requests rather than restarting the process under traffic.
How do I verify the webhook payload is really from Meta?
Your callback URL is public, so anyone who discovers it can post fake customer messages to it. Meta signs every notification with an X-Hub-Signature-256 header, computed as an HMAC SHA256 of the raw request body using your app secret. Compute the same signature yourself, compare it to the part of the header after sha256=, and reject anything that does not match.
One implementation detail sinks more attempts than the cryptography does: you have to hash the raw body, exactly as it arrived. If your framework parses the JSON and you re-encode it before hashing, the bytes shift, and the signature will never match. Capture the raw payload before any middleware touches it.
Do you need to build the webhook yourself?
If you are shipping a product feature on top of WhatsApp, yes, and it is not a big job once the handshake is understood. If your actual goal is running campaigns and answering customers, building and operating a webhook is infrastructure you will end up maintaining forever: retries, signature checks, deduplication, status reconciliation, and a queue that does not fall over when a broadcast generates a few thousand delivery receipts in a minute.
That is the honest split. A WhatsApp API integration you build gives you total control. A platform gives you the same inbound stream already parsed, sitting in a shared inbox your team can answer from. Most businesses reach for the second once the first conversation volume arrives, because the webhook was never the point.
If you are still upstream of all this and just want customers to be able to start a chat, you do not need the API at all yet. A WhatsApp link generator gives you a click to chat link and QR code in a few seconds, and the Cloud API can wait until inbound volume justifies it.
Frequently asked questions
Is the WhatsApp webhook free?
Yes. Receiving webhooks costs nothing, and there is no per-notification fee. Meta bills for delivered template messages you send, not for inbound events. The one cost change on the horizon is that free-form replies inside the 24 hour window become chargeable from October 1, 2026.
Can I use a webhook with the WhatsApp Business app?
No. The WhatsApp Business app has no API and no webhooks. Webhooks exist only on the WhatsApp Business Platform, and since the On-Premises API was retired on October 23, 2025, that means the Cloud API. A number registered to the Platform is permanently removed from the app.
What is the hub.verify_token in a WhatsApp webhook?
It is a string you invent and paste into both your code and the Meta App Dashboard. When Meta sends the GET verification request it includes your token, and your endpoint returns the hub.challenge value only if the token matches. It proves you control the endpoint. It is not used again after verification, and it is not the same thing as the app secret used for signatures.
How many webhooks can one WhatsApp number have?
One callback URL per app. If several systems need the same events, receive once and fan the payload out yourself. This is why moving a number between providers requires reconfiguring the webhook: the new provider's app takes over the subscription.
Do webhooks arrive in order?
Do not assume so. Retries and the array structure mean a status update can reach you before the message it refers to, and a single request can contain several events. Key your storage on the message id, handle events idempotently, and reconcile on whichever arrives last.