# 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](/docs/guides/whatsapp/message-types/interactive/list-menus) instead.

## Send reply buttons

Set `interactive.type` to `button`, with a `body_text` and one to three `buttons`, each a `quick_reply`:

<!-- bird:tabs typescript,python,go,php,cli,curl -->

```typescript
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);
```

```python
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)
```

```go
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)
}
```

```php
$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();
```

```cli
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
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" } }
      ]
    }
  }'
```

<!-- /bird:tabs -->

`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:

```json
{
  "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](/docs/guides/whatsapp/message-types/interactive#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](/docs/guides/whatsapp/message-types/interactive#buttons) section for the shared button shape.

## Headers and footers

A header is optional, and it is one of four shapes:

```text
"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`:

```json
{
  "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](/docs/guides/whatsapp/message-types/interactive#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](/docs/guides/whatsapp/message-types#the-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`](/docs/api/errors/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](/docs/api/errors/E15057) and [E15058](/docs/api/errors/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](/docs/guides/whatsapp/message-types/interactive#errors) and [How sending works](/docs/guides/whatsapp/sending-whatsapp).

## Next steps

- [WhatsApp interactive messages](/docs/guides/whatsapp/message-types/interactive): what all six interactive types share
- [List menus](/docs/guides/whatsapp/message-types/interactive/list-menus): for more than three choices
- [How sending works](/docs/guides/whatsapp/sending-whatsapp): the request envelope, the `202` model, and safe retries