Documentation
Sign inGet started

WhatsApp reply buttons

Reply buttons put up to three tappable choices under a WhatsApp message, so the recipient answers with a tap instead of free text. Use them for a quick decision, like confirming or cancelling a booking. For more than three choices, use list menus instead.

Send reply buttons

Set interactive.type to button, with a body_text and one to three buttons, each a quick_reply:
const msg = await bird.whatsapp.send({
  to: "+16505551234",
  from: "+13124495648",
  interactive: {
    type: "button",
    body_text: "Your gardening workshop is scheduled for 9am tomorrow.",
    buttons: [{ type: "quick_reply", quick_reply: { slug: "change-booking", text: "Change" } }],
  },
});
console.log(msg.id, msg.status);
from is required on every service message: a number your workspace owns, not a Bird-managed one. The full shape adds an optional header, footer, a quote of an earlier message, and a second button:
Codebeispiel
{
  "to": "+16505551234",
  "from": "+13124495648",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "interactive": {
    "type": "button",
    "header": {
      "type": "image",
      "url": "https://cdn.example.com/banners/workshop.png"
    },
    "body_text": "Your gardening workshop is scheduled for 9am tomorrow.",
    "footer_text": "Lucky Shrub, your gateway to succulents",
    "buttons": [
      { "type": "quick_reply", "quick_reply": { "slug": "change-booking", "text": "Change" } },
      { "type": "quick_reply", "quick_reply": { "slug": "cancel-booking", "text": "Cancel" } }
    ]
  },
  "tags": [{ "name": "category", "value": "booking" }],
  "metadata": { "order_id": "A-1" }
}
in_reply_to_message_id quotes an earlier message in the same conversation. See the hub's quoting a message to correlate a reply for how resolution works and what it can miss.
This type sends only quick_reply buttons. A cta_url button belongs to a separate interactive.type and cannot appear alongside buttons; see the hub's buttons section for the shared button shape.

Headers and footers

A header is optional, and it is one of four shapes:
Codebeispiel
"header": { "type": "text",     "text": "New workshop dates" }
"header": { "type": "image",    "url": "https://cdn.example.com/a.png" }
"header": { "type": "video",    "url": "https://cdn.example.com/a.mp4" }
"header": { "type": "document", "url": "https://cdn.example.com/a.pdf" }
A media header (image, video, or document) carries its file as a public https URL that WhatsApp fetches at send time, rather than an uploaded media handle. footer_text is optional and adds a line below the buttons.

Limits

FieldBound
buttons1 to 3 entries, every one a quick_reply
quick_reply.slugrequired, 1 to 256 characters
quick_reply.text (label)required, 1 to 20 characters, unique within the message
body_textrequired, 1 to 1024 characters
footer_textoptional, 1 to 60 characters
header.text1 to 60 characters
Bird checks that button labels (quick_reply.text) are unique, but it does not check that slug values are unique, even though each slug is meant to identify one button. Two buttons sharing a slug both send and deliver, and their replies come back indistinguishable.

Reading the reply

A press arrives as its own inbound message, carrying interactive_reply:
Codebeispiel
{
  "id": "wam_01kyb2m4xq7whs0d8n3prv6tez",
  "direction": "inbound",
  "from": { "phone_number": "+16505551234" },
  "to": { "phone_number": "+13124495648" },
  "status": "received",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "interactive_reply": {
    "type": "button",
    "button": {
      "slug": "cancel-booking",
      "text": "Cancel"
    }
  },
  "created_at": "2026-08-25T09:04:11Z"
}
The slug you set on the send comes back verbatim, so you can branch on it directly without a lookup table. You see this reply through the message list or GET /v1/whatsapp/messages/{id}; see the hub's reading a reply for that path in full.

Limits and edge cases

  • The customer service window has to be open. Reply buttons are a service message, deliverable only inside an open window; see the hub's customer service window. The window check fails open, so a 202 is not proof the window was actually open when the send goes out.
  • from must be a number your workspace owns. Omitting it, or naming a number that isn't a connected sender, is rejected before the send is created.
  • Labels must be unique, or the send is refused. Two buttons with the same quick_reply.text fail with 422 E15056 WhatsAppInteractiveDuplicateLabel, because Meta would otherwise reject the duplicate after the send is already accepted and charged.
  • The label is what the recipient sees; the slug never is. Putting user-facing copy in slug is a silent no-op, since only text renders in the chat.
  • A media header URL that WhatsApp can't fetch fails after the send is accepted. Bird does not validate the header url the way it validates a media message's URL, so an http:// URL or one that returns an error passes the request and then fails asynchronously, with media_rejected on the message's last_error.
  • Sending Meta's own field names fails the request. This type rejects unknown properties outright, so JSON copied from Meta's Cloud API reference, such as a body object or an action.buttons wrapper, needs reshaping into Bird's flat fields first.
E15057 and E15058 can fire on any type that carries in_reply_to_message_id, when the quoted message doesn't resolve or can't be quoted. For the errors any WhatsApp send can hit, a closed window, a missing or invalid sender, or an invalid recipient, see the hub's errors and How sending works.

Next steps