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);msg = client.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"}}],
},
)
print(msg.id, msg.status)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
msg, err := client.Whatsapp.Send(context.Background(), bird.WhatsappSendParams{
To: "+16505551234",
From: "+13124495648",
Interactive: &bird.WhatsAppInteractiveSend{
Type: "button",
BodyText: "Your gardening workshop is scheduled for 9am tomorrow.",
Buttons: &[]bird.WhatsAppInteractiveButtonSend{
{Type: "quick_reply", QuickReply: &bird.WhatsAppInteractiveQuickReplyButtonSend{Slug: "change-booking", Text: "Change"}},
},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)
}$interactive = (new WhatsAppMessageSendRequestInteractive())
->setType('button')
->setBodyText('Your gardening workshop is scheduled for 9am tomorrow.')
->setButtons([
(new WhatsAppInteractiveButtonSend())
->setType('quick_reply')
->setQuickReply((new WhatsAppInteractiveButtonSendQuickReply())->setSlug('change-booking')->setText('Change')),
]);
$message = $bird->whatsapp->send(
to: '+16505551234',
from: '+13124495648',
interactive: $interactive,
);
echo $message->getId(), ' ', $message->getStatus();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"}}]}'curl -X POST "https://{region}.platform.bird.com/v1/whatsapp/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"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" } }
]
}
}'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:
Exemplo de código
{
"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:
Exemplo de código
"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
| Field | Bound |
|---|---|
| buttons | 1 to 3 entries, every one a quick_reply |
| quick_reply.slug | required, 1 to 256 characters |
| quick_reply.text (label) | required, 1 to 20 characters, unique within the message |
| body_text | required, 1 to 1024 characters |
| footer_text | optional, 1 to 60 characters |
| header.text | 1 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:
Exemplo de código
{
"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
- WhatsApp interactive messages: what all six interactive types share
- List menus: for more than three choices
- How sending works: the request envelope, the 202 model, and safe retries