WhatsApp interactive messages
An interactive message is body text plus something for the recipient to tap: a WhatsApp button, a menu, a link, a card, or a request for their location or contact details. Where a template answer means parsing free text, a WhatsApp menu or a set of WhatsApp buttons gives the recipient a fixed set of choices and gives you back a value you defined. This page covers what the six types share; each type's own page covers its wire shape and its own limits.
The six types
| Type | Bird interactive.type | Header | Footer | Body max |
|---|---|---|---|---|
| Reply buttons | button | text, image, video, document | yes | 1024 |
| List menus | list | text only | yes | 4096 |
| Link buttons | cta_url | text, image, video, document | yes | 1024 |
| Media carousels | carousel | none on the message; image or video per card | no | 1024 message, 160 per card |
| Location requests | location_request_message | none | no | 1024 |
| Contact info requests | request_contact_info | none | no | 1024 |
Every type is free-form: deliverable only inside an open customer service window, and never reviewed by Meta the way a template is.
Interactive messages are free-form content, so the customer service window rule applies: see the customer service window for what that means and what a closed window returns.
Every interactive send also requires from, a number your workspace owns. Bird's managed numbers cannot carry it, so an interactive send needs a number of your own connected first.
The interactive content arm
interactive is one of the mutually exclusive content fields on POST /v1/whatsapp/messages, beside template, text, image, and the rest: exactly one may be present on a send. Inside interactive, type names which of the six variants this is, and that variant's own field carries the rest (buttons, list, cta_url, or cards). The schema bars every other variant's field, so mixing two variants on one send fails validation before it reaches a handler.
For the request envelope, the 202 response model, and safe retries, see Sending WhatsApp messages rather than this page re-teaching them.
Here is a minimal interactive message: two WhatsApp buttons on a reply-buttons send, one language at a time.
const msg = await bird.whatsapp.send({
to: "+15551234567",
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" } },
{ type: "quick_reply", quick_reply: { slug: "cancel-booking", text: "Cancel" } },
],
},
});
console.log(msg.id, msg.status);msg = client.whatsapp.send(
to="+15551234567",
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"}},
{"type": "quick_reply", "quick_reply": {"slug": "cancel-booking", "text": "Cancel"}},
],
},
)
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: "+15551234567",
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"}},
{Type: "quick_reply", QuickReply: &bird.WhatsAppInteractiveQuickReplyButtonSend{Slug: "cancel-booking", Text: "Cancel"}},
},
},
})
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')),
(new WhatsAppInteractiveButtonSend())
->setType('quick_reply')
->setQuickReply((new WhatsAppInteractiveButtonSendQuickReply())->setSlug('cancel-booking')->setText('Cancel')),
]);
$message = $bird->whatsapp->send(
to: '+15551234567',
from: '+13124495648',
interactive: $interactive,
);
echo $message->getId(), ' ', $message->getStatus();bird whatsapp send \
--from +13124495648 \
--interactive '{"body_text":"Your gardening workshop is scheduled for 9am tomorrow.","buttons":[{"quick_reply":{"slug":"change-booking","text":"Change"},"type":"quick_reply"},{"quick_reply":{"slug":"cancel-booking","text":"Cancel"},"type":"quick_reply"}],"type":"button"}' \
--to +15551234567curl -X POST "https://{region}.platform.bird.com/v1/whatsapp/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "+15551234567",
"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" } },
{ "type": "quick_reply", "quick_reply": { "slug": "cancel-booking", "text": "Cancel" } }
]
}
}'Buttons
Four of the six types place a button, and all of them draw on the same shape: a discriminated object whose type is quick_reply or cta_url, each carrying its own nested field of the same name. A quick_reply button carries slug and text; a cta_url button carries text and url. Which types accept which button shape:
- Reply buttons send only quick_reply buttons, 1 to 3 of them.
- Link buttons send exactly one cta_url button.
- Media carousels put buttons on each card: either one cta_url button, or up to three quick_reply buttons, and every card in the carousel must agree.
- List menus use rows inside sections rather than this button object, covered on their own page.
A quick_reply button's slug is your own handle for that button. It is never shown to the recipient, only its text label is, and the slug is echoed verbatim on the reply. That round trip is what makes a reply correlatable to the button that produced it, so this is worth saying once, here, rather than on each leaf page.
Reading a reply
Pressing a button or choosing a menu row sends its own inbound message, carrying an interactive_reply object. interactive_reply.type is button or list; whichever it is, the nested object carries the slug and text you declared, the tapped label the recipient actually saw. The two request types, location requests and contact info requests, answer differently: a location request's reply is an ordinary inbound location message, and a contact info request's reply is an inbound contact card, not an interactive_reply at all.
A reply reaches you through the message list and GET /v1/whatsapp/messages/{id}, the same way any inbound WhatsApp message does. To act on one as it arrives rather than polling, subscribe to the whatsapp.received webhook: its payload carries interactive_reply, so it already names the button or row that was tapped. Receiving interactive replies covers the read shape of a tap, the webhook payload, and the taps that arrive on another arm.
Quoting a message to correlate a reply
in_reply_to_message_id on a send quotes an earlier message from the same conversation, and every message, sent or received, echoes it back on a read. It is one field for both directions.
The correlation this buys you is asymmetric. A tap on a WhatsApp button or menu row carries Meta's own context, so in_reply_to_message_id resolves to the message that offered it. A shared contact card carries no context at all, so it resolves to nothing: you correlate a contact info request's reply on from and timing, not on this field.
Resolution goes through a message-context store, and a miss omits the field rather than reporting one. That is indistinguishable, on the wire, from a reply that answers nothing at all. An integration that needs reliable correlation should not rely on this field alone: carry your own metadata on the send and match on that instead.
The window a message stays quotable in is limited to 15 days; past it the send fails with a 404 E15071, because Bird no longer holds the provider id a quote needs. Sending WhatsApp messages owns the send-side field: its length, its resolution, and the request shape.
Errors
Three error codes are specific to interactive content. Each only fires on the types that have the field it checks, so the fourth column names which types can actually reach each one.
| Code | Status | What triggers it | Applies to |
|---|---|---|---|
| E15055 WhatsAppInteractiveLimitExceeded | 422 | The message exceeds a limit for its type; today, more than 10 rows across a list's sections. | List menus only |
| E15056 WhatsAppInteractiveDuplicateLabel | 422 | Two buttons or rows in the same message share a label. | Any type with labelled buttons or rows: reply buttons, list menus, media carousels |
| E15059 WhatsAppInteractiveCarouselButtonsMismatch | 422 | A carousel's cards do not all carry the same buttons. | Media carousels only |
Every interactive send can also hit the errors any WhatsApp send can: a closed customer service window, a missing or invalid sender, an invalid recipient, or ambiguous content. Those are shared across every WhatsApp content type, not specific to interactive messages; see Sending WhatsApp messages for that list rather than a copy of it here.
Next steps
- Sending WhatsApp messages: the request envelope, the 202 model, and safe retries
- WhatsApp events: follow delivery per message, over the API or webhooks
- WhatsApp templates: the messages you can still send once the window is closed
Related resources
Continue with the documentation, guides and examples for this topic. Resources are in English.
Watch the guideConnecting WhatsApp to Bird: from buying a number to a live channelUnderstand the conceptWhat is the 24-hour customer service window on WhatsApp?Use the toolWhatsApp message builderExplore the capabilityWhatsApp
Try the practice and get an implementation brief