What is an interactive reply?
An interactive reply is the message your webhook receives when a customer answers an Interactive Message, or a button on a template, by tapping instead of typing. It reaches you through the Messages Webhook like any inbound message, but instead of free text it carries the identifier you attached to the option, so your bot, CRM or inbox knows exactly which choice was made without interpreting language.
The shape depends on what was tapped. A reply button produces a message with type: interactive and interactive.type: button_reply, holding the button's id and title. A row in a List Message produces interactive.type: list_reply with the row's id, title and description. A Quick Reply Button on an approved template is different again: the message has type: button and a button object with payload, the value you attached when you sent the template (otherwise the button text), and text, the label. A WhatsApp Flow returns interactive.type: nfm_reply with the submitted answers as a JSON string in response_json; the Flow Response Message page covers that one. Button and list replies carry context.id, the WAMID of the message that held the tapped control.
An interactive reply is not the same as a Contextual Reply. A contextual reply is any message, typed or not, that quotes an earlier message with WhatsApp's reply gesture; it also shows up as a context object, but its content is whatever the customer wrote. An interactive reply is specifically the structured answer a tap produces. Taps on URL buttons, call buttons and copy-code buttons produce no interactive reply at all, because they act on the customer's phone instead of sending a message back.
Key takeaways
- An interactive reply is the inbound message a tap creates; it carries the ID you set, so you route on data, not on typed text.
- Reply-button taps arrive as
interactive.button_replyand list picks asinteractive.list_reply, each with anidand atitle. - Taps on a template's quick-reply buttons arrive differently:
type: button, withbutton.payloadandbutton.text. context.idnames the message whose control was tapped, which lets you reject stale menus and late taps.- Flow submissions come back as
nfm_replywith the answers in aresponse_jsonstring.
Figure 1
What does a button tap look like at your webhook?
{"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": "Meera"},"wa_id": "919812345678"}],"messages": [{"context": {"from": "919876543210","id": "wamid.HBgMOTE5ODEyMzQ1Njc4FQIAERgSU0xPVF9BU0tfODgyMTMA"},"from": "919812345678","id": "wamid.HBgMOTE5ODEyMzQ1Njc4FQIAEhgUM0E5RjJCN0M0RDVFNkYxMjM0NTYA","timestamp": "1760862120","type": "interactive","interactive": {"type": "button_reply","button_reply": {"id": "slot:v1:ORD-88213:evening","title": "Evening"}}}]}}]}]}
Field 8: entry.0.changes.0.value.metadata.phone_number_id — Which of your numbers received the tap; answer from the same one.
An interactive reply in code
// Normalise every kind of tap into one shape your handlers can use.export function readTap(message) {const tapped = message.context?.id ?? null; // WAMID of the message that was tappedif (message.type === "interactive") {const i = message.interactive;if (i.type === "button_reply") return tap(i.button_reply.id, i.button_reply.title, tapped);if (i.type === "list_reply") return tap(i.list_reply.id, i.list_reply.title, tapped);if (i.type === "nfm_reply") return { action: "flow", data: JSON.parse(i.nfm_reply.response_json), tapped };}if (message.type === "button") return tap(message.button.payload, message.button.text, tapped); // template quick replyreturn null; // typed text, media, location: not a tap}function tap(id, label, tapped) {const [action, version, ...rest] = String(id).split(":");return { action, version, ref: rest.join(":"), label, tapped };}// In the queue worker that processes messages webhooks:export async function onMessage(message) {if (await alreadyProcessed(message.id)) return; // retried webhookconst t = readTap(message);if (!t) return handleFreeText(message);if (!(await isStillCurrent(t.tapped))) return sendFreshOptions(message.from);const handler = handlers[t.action];return handler ? handler(t, message.from) : logUnknownTap(t);}
Route on IDs and payloads, check `context.id`, and deduplicate on the inbound message ID.
How does an interactive reply work?
Design the IDs before the copy
Make IDs namespaced, versioned and self-describing, such as
slot:v1:ORD-88213:evening. Button IDs can be up to 256 characters and list row IDs up to 200, so there is room for the state you need.Parse by message type
Switch on
message.type. Forinteractive, switch again oninteractive.type:button_reply,list_replyornfm_reply. Forbutton, read the template quick-replypayload.Check what was tapped
Look up
context.idin the messages you sent. If that menu has expired or been superseded, a slot taken, a price changed, reply with fresh options instead of acting on the old choice.Make the handler idempotent
Meta may deliver the same webhook more than once. Skip any inbound
idyou have already processed, and make the business action itself safe to repeat, such as confirming an order only once.Answer while the window is open
A tap is a customer message, so it opens or refreshes the 24-hour customer service window. Reply straight away with the next step as a free-form message.
Common mistakes with an interactive reply
Handling only button_reply
Code written for reply buttons silently ignores list picks and template quick replies, which arrive as
list_replyand astype: button. Handle every shape in one router.Routing on the title
Labels get edited, translated and A/B tested. Route on
idorpayload, and keep the title only for display and logs.Acting on stale taps
Old messages stay in the chat, so a customer can tap a slot or price from last week's menu. Check
context.idagainst what you sent, or encode a date or version in the ID, and offer fresh options when it has expired.Not deduplicating
Webhooks can be delivered more than once, and the same choice can reach you twice. Use Webhook Deduplication on the inbound message ID and make actions such as payments or confirmations idempotent.
Waiting for a webhook from a URL button
URL and call buttons never produce an interactive reply. Measure them with tagged links, landing-page events or call logs.
Example: COD confirmation for a Surat saree seller
A Surat saree seller sends a cash-on-delivery confirmation for order ORD-88213 as a utility template with two quick-reply buttons, attaching the payloads cod:v1:ORD-88213:confirm and cod:v1:ORD-88213:cancel. The buyer taps Confirm; the webhook shows type: button with that payload, the order service marks the order confirmed and the bot sends reply buttons asking for a delivery time. Her tap on Evening arrives as a button_reply with the ID slot:v1:ORD-88213:evening. When the same webhook is delivered again a minute later, the handler finds the message ID already processed and does nothing.
Why it matters
Why does an interactive reply matter?
Tapped answers are where WhatsApp automation becomes dependable. Routing on button_reply.id instead of interpreting "yes pls" or "हाँ" lets a bot confirm a cash-on-delivery order, book a slot or escalate to an agent with no language model and no guesswork. That only holds if the handler copes with the three slightly different payload shapes, with webhooks that arrive twice and with customers tapping a menu from last week.Best practices for an interactive reply
- Put a version in IDs so a menu redesign does not misroute taps on older messages.
- Log unknown IDs instead of dropping them; they reveal old menus still being used in customers' chats.
- Keep personal data out of IDs and payloads, since they pass through webhooks and logs.
- Confirm the choice back to the customer in your next message so the chat reads clearly later.
Interactive Reply FAQs
- What is the difference between button_reply and type button in WhatsApp webhooks?
button_replycomes from reply buttons in a free-form interactive message;type: buttoncomes from a quick-reply button on a template. The first carriesidandtitleinsideinteractive, the second carriespayloadandtextinsidebutton.- How do I know which message a customer's tap belongs to?
- Read
context.idon the inbound message. It is the WAMID of the message that held the tapped button or list, so you can match it to the order, ticket or menu version you sent. - Can the same tap reach my webhook twice?
- Plan for it. Meta may redeliver a webhook, and older messages stay in the chat where options can be tapped again later. Deduplicate on the inbound message ID and make each action safe to repeat.
- Do URL buttons send a webhook when tapped?
- No. CTA URL, call and copy-code buttons act on the customer's phone and send nothing back to your webhook. Track them with tagged links or landing-page analytics instead.
- Does an interactive reply open the 24-hour customer service window?
- Yes. A tap is a message from the customer, so it opens the window or restarts its 24-hour clock, and you can answer with free-form messages straight away.