# 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](/docs/guides/whatsapp/message-types/interactive/reply-buttons)                 | `button`                   | text, image, video, document                 | yes    | 1024                       |
| [List menus](/docs/guides/whatsapp/message-types/interactive/list-menus)                       | `list`                     | text only                                    | yes    | 4096                       |
| [Link buttons](/docs/guides/whatsapp/message-types/interactive/cta-url-buttons)                | `cta_url`                  | text, image, video, document                 | yes    | 1024                       |
| [Media carousels](/docs/guides/whatsapp/message-types/interactive/carousels)                   | `carousel`                 | none on the message; image or video per card | no     | 1024 message, 160 per card |
| [Location requests](/docs/guides/whatsapp/message-types/interactive/location-requests)         | `location_request_message` | none                                         | no     | 1024                       |
| [Contact info requests](/docs/guides/whatsapp/message-types/interactive/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](/docs/guides/whatsapp/message-types#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 [How sending works](/docs/guides/whatsapp/sending-whatsapp) 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.

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

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

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

```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:   "+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)
}
```

```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')),
        (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();
```

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

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

<!-- /bird:tabs -->

## 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](/docs/guides/whatsapp/message-types/interactive/location-requests) message, and a contact info request's reply is an inbound [contact card](/docs/guides/whatsapp/message-types/interactive/contact-info-requests), 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.

## 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; see [E15057](/docs/api/errors/E15057) for what happens once it closes. [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp#quoting-a-message) owns the send-side field: its length, its resolution, and the request shape.

## Errors

Five error codes are specific to interactive content. Three of the five only fire on the types that have the field they check, so the fourth column names which types can actually reach each one.

| Code                                                                           | Status | What triggers it                                                                             | Applies to                                                                         |
| ------------------------------------------------------------------------------ | ------ | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| [E15055 `WhatsAppInteractiveLimitExceeded`](/docs/api/errors/E15055)           | 422    | The message exceeds a limit for its type; today, more than 10 rows across a list's sections. | List menus only                                                                    |
| [E15056 `WhatsAppInteractiveDuplicateLabel`](/docs/api/errors/E15056)          | 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`](/docs/api/errors/E15059) | 422    | A carousel's cards do not all carry the same buttons.                                        | Media carousels only                                                               |
| [E15057 `WhatsAppInReplyToNotFound`](/docs/api/errors/E15057)                  | 422    | The quoted message is not one this workspace holds.                                          | Any type, when it carries `in_reply_to_message_id`                                 |
| [E15058 `WhatsAppInReplyToNotQuotable`](/docs/api/errors/E15058)               | 422    | The quoted message cannot be quoted.                                                         | Any type, when it carries `in_reply_to_message_id`                                 |

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 [How sending works](/docs/guides/whatsapp/sending-whatsapp) for that list rather than a copy of it here.

## Next steps

- [How sending works](/docs/guides/whatsapp/sending-whatsapp): the request envelope, the `202` model, and safe retries
- [WhatsApp events](/docs/guides/whatsapp/events): follow delivery per message, over the API or webhooks
- [WhatsApp templates](/docs/guides/whatsapp/templates): the messages you can still send once the window is closed