01 · How it works, in 6 steps
How does the messages webhook work?
- 1
Subscribe to the messages field
Set a verified HTTPS callback URL, tick
messagesin the app's webhook fields and subscribe each WABA to the app. One endpoint can serve many numbers;metadata.phone_number_idsays which one was messaged. - 2
Verify, acknowledge, enqueue
Check
X-Hub-Signature-256over the raw body, return 200 within a few seconds and process from a queue. Slow handlers trigger retries, and Meta keeps retrying for up to 7 days. - 3
Split messages from statuses
Loop over every
entryandchangesitem. Ifvalue.messagesexists, it is inbound; ifvalue.statusesexists, hand it to the status handler. One POST can contain both. - 4
Route on type
Branch on
messages[].type. Readinteractive.button_reply.idorlist_reply.idfor menu choices,button.payloadfor template quick replies andinteractive.nfm_reply.response_jsonfor Flow answers. - 5
Fetch media and keep context
Media messages carry an ID, not a file, so media retrieval is a second step: exchange the ID for a URL with GET /{MEDIA_ID} (webhook media IDs last 7 days, the URL 5 minutes). Store
context.idso a reply attaches to the message it answers. - 6
Deduplicate by wamid
Store each
messages[].idand skip any you have seen. Order is not guaranteed either, so sort a thread bytimestamp, not by arrival time.
02Try it
Hands-on: use the controls to work through it yourself.
Read a button tap the way your router should
{"object": "whatsapp_business_account","entry": [{"id": "102290129340398","changes": [{"field": "messages","value": {"messaging_product": "whatsapp","metadata": {"display_phone_number": "919876543210","phone_number_id": "106540352242922"},"contacts": [{"profile": {"name": "Rohan"},"wa_id": "919812345678","user_id": "IN.1349120865530274318"}],"messages": [{"from": "919812345678","from_user_id": "IN.1349120865530274318","id": "wamid.HBgMOTE5ODEyMzQ1Njc4FQIAEhgUM0VCMDRDNTlBQTI5RjM0QkQ5MTkA","timestamp": "1760868000","context": {"from": "919876543210","id": "wamid.HBgMOTE5ODEyMzQ1Njc4FQIAERgSQzJFNDdGQjUyNUE0OTEA"},"type": "interactive","interactive": {"type": "button_reply","button_reply": {"id": "slot_tomorrow_11","title": "Tomorrow 11 am"}}}]}}]}]}
Field 8: entry.0.changes.0.value.messages.0.timestamp — Unix seconds, as a string. Sort threads by it; delivery order is not guaranteed.
03What is the messages webhook?
The messages webhook is the messages field of the WhatsApp Business Platform's webhooks, and the one every integration needs first. Each time a WhatsApp user sends something to your business number, Meta POSTs it to your callback URL as JSON. It is the only way to receive messages on the Cloud API; there is no inbox to poll.
Inside the usual webhook payload envelope, value carries metadata (the phone_number_id and display number that received it), a contacts array — the webhook contacts object, with the sender's profile.name, wa_id and, since April 2026, their business-scoped user ID in user_id — and a messages array. Every message has from, id (the wamid), timestamp and type, plus an object named after the type: text, image, audio, video, document, sticker, location, contacts, interactive (a button_reply, list_reply or Flow nfm_reply), button (a template quick reply), reaction, order, system or unsupported.
Two optional objects carry context. context appears on a contextual reply, when the user answers a specific message (its id is your earlier wamid), taps a button on an interactive message or template, or forwards something (forwarded, frequently_forwarded). The webhook referral object, referral, appears on the first message from a Click-to-WhatsApp ad, with the ad's source_id, headline and ctwa_clid. Note that the same messages field also carries statuses arrays — sent, delivered and read reports for messages you sent. Those are the statuses webhook, a different payload with a different job.
Key takeaways
- The messages webhook delivers every message a WhatsApp user sends to your business number as a JSON POST to your callback URL.
- Route on
messages[].type: template quick replies arrive asbutton, reply buttons, lists and Flow answers asinteractive. - Media arrives as an ID, not a file: webhook media IDs expire after 7 days and download URLs after 5 minutes.
- The same
messagesfield also carriesstatusesarrays, so check which array is present before reading it. - Since April 2026 every inbound payload carries a business-scoped user ID, and a username user's phone number can be left out.
04 · In code
The messages webhook in code
export async function onMessagesChange(change) {if (change.field !== "messages") return;const v = change.value;const numberId = v.metadata.phone_number_id; // reply from this numberfor (const msg of v.messages ?? []) {if (await seen(msg.id)) continue; // retries resend the same wamidconst sender = msg.from ?? msg.from_user_id; // phone can be omitted for username usersconst replyTo = msg.context?.id; // the wamid this message answersswitch (msg.type) {case "text":await inbox.add(numberId, sender, msg.text.body, replyTo);break;case "interactive": {const i = msg.interactive;if (i.type === "nfm_reply") await flows.submit(sender, JSON.parse(i.nfm_reply.response_json));else await bot.choice(sender, (i.button_reply ?? i.list_reply).id, replyTo);break;}case "button": // quick reply on a templateawait bot.choice(sender, msg.button.payload, replyTo);break;case "image": case "video": case "audio": case "document": case "sticker":await media.fetchLater(msg[msg.type].id); // webhook media IDs last 7 daysbreak;case "reaction":await inbox.react(msg.reaction.message_id, msg.reaction.emoji);break;default: // location, contacts, order, system, unsupportedawait inbox.addRaw(numberId, sender, msg);}}if (v.statuses) await statusQueue.add(v.statuses); // same field, different payload}
Deduplicate, route on type, keep context, and pass any statuses to their own handler.
05 · Mistakes to avoid
Common mistakes with the messages webhook
Assuming every POST holds a message
Most
messageswebhooks are statuses, not inbound messages. Code that readsvalue.messages[0]unconditionally crashes on them. Check which array is present before reading it.Keying users only on the phone number
With usernames rolling out,
fromandwa_idcan be omitted for some users, whilefrom_user_idandcontacts[].user_idalways carry the BSUID. Store both identifiers and match on whichever is present. A number change also changes the BSUID, announced by asystemmessage rather than a separate webhook field.Confusing button and interactive
A tap on a template's quick-reply button arrives as
type: buttonwithbutton.payload; a tap on a non-template reply button arrives astype: interactivewithbutton_reply.id. Handle both.Fetching media too late
A job that runs a week later finds the media ID gone. Download media soon after the webhook, store the file in your own storage and keep only your copy's reference.
06 · Pro tips
Best practices for the messages webhook
- Give every button and list row a stable, meaningful ID; it is what your router reads, not the title.
- Keep referral data from Click-to-WhatsApp ads with the lead — ctwa_clid is what conversion reporting needs.
- Log unsupported messages with their errors object so you know what customers tried to send.
- Mark a message as read only after a person or the bot has actually processed it.
07 · Why it matters
Why does the messages webhook matter?
Every customer reply, button tap and order starts as a messages webhook. If your handler misreads a type, ignores context or drops a payload, a customer's answer never reaches the bot or the agent, and they wait on a business that seems to ignore them. Getting this one handler right — fast, deduplicated and complete across types — is what makes WhatsApp a two-way channel.
08Example: A reschedule tap at a Lucknow diagnostics lab
A Lucknow diagnostics lab sends a utility template with two quick-reply buttons for tomorrow's home sample collection. The patient taps "Change slot" and the lab's endpoint receives a messages webhook with type: button, button.payload: CHANGE_SLOT and context.id pointing at the template's wamid. The bot answers inside the new service window with a reply-button message of three slots; the patient's choice comes back as type: interactive with a button_reply.id the lab's booking system understands.
09 · Key facts
The messages webhook: key facts and limits
- Webhook field
- messages (inbound messages and statuses)
- Inbound array
- value.messages[] with from, id, timestamp, type
- Sender identity
- contacts[].wa_id, contacts[].user_id (BSUID since April 2026)
- Reply context
- messages[].context.id = the wamid being answered
- Media
- ID only; webhook media IDs expire after 7 days
- Payload size
- Up to 3 MB
- Ordering
- Not guaranteed; sort by timestamp
10Messages Webhook FAQs
What is the difference between the messages webhook and the statuses webhook?+
Does an inbound message open the 24-hour customer service window?+
Why is the from field missing on some messages?+
Can one webhook POST contain more than one message?+
* Per-message rates for India, ex-GST: marketing, utility and authentication effective 1 July 2026; service messages ₹0.145 since 1 October 2026 (they were ₹0 until 30 September 2026). Volume commitments earn discounts — final rate is confirmed on your account; applicable GST extra. Rates for other countries differ (see the international rate table on /pricing).