# WhatsApp list menus

A list menu opens as a tappable button under a WhatsApp message and expands into rows grouped into sections, so the recipient picks one from a longer set of choices instead of typing free text. Use it once the choice list is longer than three; for three or fewer, [reply buttons](/docs/guides/whatsapp/message-types/interactive/reply-buttons) are the simpler shape.

## Send a list menu

Set `interactive.type` to `list`, with a `body_text` and a `list` object carrying `button_text` and at least one section:

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

```typescript
const msg = await bird.whatsapp.send({
  to: "+16505551234",
  from: "+13124495648",
  interactive: {
    type: "list",
    body_text: "Which shipping option do you prefer?",
    list: {
      button_text: "Shipping options",
      sections: [
        {
          title: "As soon as possible",
          rows: [{ slug: "priority_express", text: "Priority Mail Express" }],
        },
      ],
    },
  },
});
console.log(msg.id, msg.status);
```

```python
msg = client.whatsapp.send(
    to="+16505551234",
    from_="+13124495648",
    interactive={
        "type": "list",
        "body_text": "Which shipping option do you prefer?",
        "list": {
            "button_text": "Shipping options",
            "sections": [
                {
                    "title": "As soon as possible",
                    "rows": [{"slug": "priority_express", "text": "Priority Mail Express"}],
                }
            ],
        },
    },
)
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:     "list",
			BodyText: "Which shipping option do you prefer?",
			List: &bird.WhatsAppInteractiveListSend{
				ButtonText: "Shipping options",
				Sections: []bird.WhatsAppInteractiveListSectionSend{
					{
						Title: "As soon as possible",
						Rows: []bird.WhatsAppInteractiveListRowSend{
							{Slug: "priority_express", Text: "Priority Mail Express"},
						},
					},
				},
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(msg.Id, *msg.Status)
}
```

```php
$interactive = (new WhatsAppMessageSendRequestInteractive())
    ->setType('list')
    ->setBodyText('Which shipping option do you prefer?')
    ->setList(
        (new WhatsAppInteractiveSendList())
            ->setButtonText('Shipping options')
            ->setSections([
                (new WhatsAppInteractiveListSectionSend())
                    ->setTitle('As soon as possible')
                    ->setRows([
                        (new WhatsAppInteractiveListRowSend())
                            ->setSlug('priority_express')
                            ->setText('Priority Mail Express'),
                    ]),
            ]),
    );

$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":"list","body_text":"Which shipping option do you prefer?","list":{"button_text":"Shipping options","sections":[{"title":"As soon as possible","rows":[{"slug":"priority_express","text":"Priority Mail Express"}]}]}}'
```

```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": "list",
      "body_text": "Which shipping option do you prefer?",
      "list": {
        "button_text": "Shipping options",
        "sections": [
          {
            "title": "As soon as possible",
            "rows": [{ "slug": "priority_express", "text": "Priority Mail Express" }]
          }
        ]
      }
    }
  }'
```

<!-- /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 text header, a footer, a quote of an earlier message, a second section, and a row description:

```json
{
  "to": "+16505551234",
  "from": "+13124495648",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "interactive": {
    "type": "list",
    "header": { "type": "text", "text": "Choose a shipping option" },
    "body_text": "Which shipping option do you prefer?",
    "footer_text": "Lucky Shrub",
    "list": {
      "button_text": "Shipping options",
      "sections": [
        {
          "title": "As soon as possible",
          "rows": [
            {
              "slug": "priority_express",
              "text": "Priority Mail Express",
              "description": "Next day to 2 days"
            }
          ]
        },
        {
          "title": "I can wait a bit",
          "rows": [
            {
              "slug": "ground_advantage",
              "text": "Ground Advantage",
              "description": "2 to 5 days"
            }
          ]
        }
      ]
    }
  },
  "tags": [{ "name": "flow", "value": "shipping-choice" }],
  "metadata": { "order_id": "A1B2C3" }
}
```

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

A list menu uses `list.sections` rather than the shared button object; see the hub's [buttons](/docs/guides/whatsapp/message-types/interactive#buttons) section for how the other types use that shape instead.

## Sections and rows

`list.button_text` is the label on the button that opens the menu, up to 20 characters. Tapping it opens the menu: each section renders under its own `title`, and each row under a section shows its `text` label with its optional `description` underneath, in smaller type. A section's `title` is required even when the menu has only one section, because WhatsApp shows it above that section's rows regardless.

A row's `slug` is your own handle for that row, never shown to the recipient. Only `text` renders in the chat, and the `slug` is echoed verbatim on the reply, the same round trip the hub describes for buttons. Nothing validates the characters in `slug`: put whatever your own routing needs there.

## Limits

| Field                    | Bound                                                         |
| ------------------------ | ------------------------------------------------------------- |
| Sections per message     | 1 to 10                                                       |
| Rows per section         | 1 to 10                                                       |
| Rows across all sections | 10, total                                                     |
| Section `title`          | required, 1 to 24 characters                                  |
| Row `slug`               | required, 1 to 200 characters, any characters                 |
| Row `text` (label)       | required, 1 to 24 characters, unique across the whole message |
| Row `description`        | optional, up to 72 characters                                 |
| `list.button_text`       | required, 1 to 20 characters                                  |
| `body_text`              | required, 1 to 4096 characters                                |
| `footer_text`            | optional, up to 60 characters                                 |
| `header.text`            | 1 to 60 characters                                            |

Two of these bounds are easy to misread.

**The row cap is ten across all sections, not ten per section.** Each section independently allows up to 10 rows, but the message as a whole still tops out at 10 rows total. Three sections of 4 rows each pass every per-section check and still exceed the total, which is enforced separately from the per-section count and returns [E15055](/docs/api/errors/E15055) with a message naming how many rows the send actually offered.

**`body_text` is the only one that keeps the full 4096 characters.** Every other interactive type narrows its body to 1024; a list menu is the only one WhatsApp allows to use the full length, so reach for it when the choice needs more context than a short prompt.

## Reading the choice

A row tap 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": "list",
    "list": {
      "slug": "priority_express",
      "text": "Priority Mail Express",
      "description": "Next day to 2 days"
    }
  },
  "created_at": "2026-08-25T09:04:11Z"
}
```

The `slug` you set on the chosen row comes back verbatim, so you can branch on it directly. `description` is present only when the row carried one. 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.** A list menu is 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.
- **The header is text only, and a media header fails as a generic error.** A list is the only interactive type whose header cannot carry an image, video, or document. Sending one anyway does not come back as a WhatsApp-specific error code: it fails general request validation, so a developer debugging by error code sees only that the request failed validation and has to read the details to find the header.
- **The cross-section row cap is the trap.** Per-section limits and the message-wide total both cap at 10, so they read as the same number twice. They are not additive: a menu that passes every per-section check can still exceed the total, and only the total is what gets enforced.
- **Row labels must be unique across the whole message, not just within a section.** Two rows sharing a `text` label, even in different sections, are refused with [E15056](/docs/api/errors/E15056). This is Bird's own rule, not one Meta states: the reply names the label tapped, so a duplicate would leave the answer ambiguous, and distinct `slug` values underneath don't help resolve it.
- **A Meta-side rejection beyond Bird's own checks is vague and can arrive late.** Bird's row-count and duplicate-label checks catch the two most common mistakes before anything is created. Anything Meta rejects beyond that surfaces after the `202`, on the message's `last_error`, rather than on the send call itself.

[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
- [Reply buttons](/docs/guides/whatsapp/message-types/interactive/reply-buttons): for three or fewer choices
- [How sending works](/docs/guides/whatsapp/sending-whatsapp): the request envelope, the `202` model, and safe retries