Skip to content

Playbook · 6 steps

Messages Webhook

Also called: Inbound message webhook · messages field · Incoming message notification

6steps

Level
Foundation
Reading time
About 6 min

Reviewed

Definition
The messages webhook delivers every message a WhatsApp user sends to your business number — text, media, button taps, locations, reactions — as a JSON POST.

01 · How it works, in 6 steps

How does the messages webhook work?

  1. 1

    Subscribe to the messages field

    Set a verified HTTPS callback URL, tick messages in the app's webhook fields and subscribe each WABA to the app. One endpoint can serve many numbers; metadata.phone_number_id says which one was messaged.

  2. 2

    Verify, acknowledge, enqueue

    Check X-Hub-Signature-256 over 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. 3

    Split messages from statuses

    Loop over every entry and changes item. If value.messages exists, it is inbound; if value.statuses exists, hand it to the status handler. One POST can contain both.

  4. 4

    Route on type

    Branch on messages[].type. Read interactive.button_reply.id or list_reply.id for menu choices, button.payload for template quick replies and interactive.nfm_reply.response_json for Flow answers.

  5. 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.id so a reply attaches to the message it answers.

  6. 6

    Deduplicate by wamid

    Store each messages[].id and skip any you have seen. Order is not guaranteed either, so sort a thread by timestamp, 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

8 / 8
{
"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"
}
}
}
]
}
}
]
}
]
}
An inbound reply-button tap, field by field: who received it, who sent it, what they were answering and the ID your code acts on. Step through the annotations in the order a handler reads them.

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 as button, reply buttons, lists and Flow answers as interactive.
  • Media arrives as an ID, not a file: webhook media IDs expire after 7 days and download URLs after 5 minutes.
  • The same messages field also carries statuses arrays, 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 number
for (const msg of v.messages ?? []) {
if (await seen(msg.id)) continue; // retries resend the same wamid
const sender = msg.from ?? msg.from_user_id; // phone can be omitted for username users
const replyTo = msg.context?.id; // the wamid this message answers
switch (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 template
await 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 days
break;
case "reaction":
await inbox.react(msg.reaction.message_id, msg.reaction.emoji);
break;
default: // location, contacts, order, system, unsupported
await 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 messages webhooks are statuses, not inbound messages. Code that reads value.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, from and wa_id can be omitted for some users, while from_user_id and contacts[].user_id always carry the BSUID. Store both identifiers and match on whichever is present. A number change also changes the BSUID, announced by a system message rather than a separate webhook field.

  • Confusing button and interactive

    A tap on a template's quick-reply button arrives as type: button with button.payload; a tap on a non-template reply button arrives as type: interactive with button_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?+
Both arrive on the messages field. A messages array is something a user sent to you; a statuses array reports what happened to something you sent (sent, delivered, read, failed). Route them to different handlers.
Does an inbound message open the 24-hour customer service window?+
Yes. Any inbound message, including a tap on one of your buttons, opens or restarts the window, so you can reply with free-form messages for the next 24 hours. In India those service replies have been charged at ₹0.145 each since 1 October 2026.
Why is the from field missing on some messages?+
Because the user has adopted a WhatsApp username. Meta then omits their phone number unless you exchanged messages or calls in the last 30 days or they are in your contact book. The business-scoped user ID is still included, so key users on it as well.
Can one webhook POST contain more than one message?+
Yes, so never read only index 0. A single POST can carry several entry items, each with several changes, and one value can hold more than one message or status. Loop over every level and deduplicate each message by its wamid.

See InfiQ pricing

* 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).

From definition to your first message

InfiQ is an official Meta Business Partner. Start a 7-day free trial — no card needed — and see this running on your own number.