> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wsapi.chat/llms.txt
> Use this file to discover all available pages before exploring further.

# Buttons and lists

> Send messages with tappable buttons or a pick-one list, and read the choice back as an event

WSAPI can send two kinds of interactive message:

* **Buttons** — `POST /messages/buttons`. A message with 1 to 3 buttons under the text. Use them for short, closed choices: confirm or cancel an appointment, yes or no, or a link, a code to copy or a number to call.
* **Lists** — `POST /messages/list`. A message with one button that opens a list of up to 10 options, grouped into sections. Use it when there are more than three options, or when each option needs a short description: time slots, menu items, departments.

When the recipient taps a reply button or picks a row, you receive a `message` event with what they chose. See [Button and list replies](/events/interactive-replies).

## Send buttons

Reply buttons send the choice back to you. Give each one an `id` — it is what you receive in the event, so make it something your code can route on.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.wsapi.chat/messages/buttons \
    -H "Content-Type: application/json" \
    -H "X-Api-Key: YOUR_API_KEY" \
    -H "X-Instance-Id: YOUR_INSTANCE_ID" \
    -d '{
      "to": "1234567890@s.whatsapp.net",
      "text": "Can we confirm your appointment for tomorrow at 10:00?",
      "footer": "Reply with a button",
      "buttons": [
        { "id": "confirm", "text": "Confirm" },
        { "id": "reschedule", "text": "Reschedule" },
        { "id": "cancel", "text": "Cancel" }
      ]
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.wsapi.chat/messages/buttons", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": "YOUR_API_KEY",
      "X-Instance-Id": "YOUR_INSTANCE_ID",
    },
    body: JSON.stringify({
      to: "1234567890@s.whatsapp.net",
      text: "Can we confirm your appointment for tomorrow at 10:00?",
      footer: "Reply with a button",
      buttons: [
        { id: "confirm", text: "Confirm" },
        { id: "reschedule", text: "Reschedule" },
        { id: "cancel", text: "Cancel" },
      ],
    }),
  });
  const { id } = await response.json();
  // Store id next to the appointment: the reply event carries it as replyTo.id
  ```
</CodeGroup>

The response is `201 Created` with the ID of the message:

```json theme={null}
{ "id": "3EB07C7B838E3240C9E150" }
```

Keep that ID. The reply event carries it as `replyTo.id`, which is how you tell which message — and so which appointment, order or ticket — the recipient answered.

### Link, copy and call buttons

Buttons of different types can be mixed in one message. These three act on the recipient's device and send nothing back:

```json theme={null}
{
  "to": "1234567890@s.whatsapp.net",
  "header": "Your order has shipped",
  "text": "Order #1042 is on its way. Track it, copy your coupon for next time, or call us.",
  "footer": "Example Store",
  "buttons": [
    { "type": "url", "text": "Track order", "url": "https://example.com/track/1042" },
    { "type": "copy", "text": "Copy coupon", "copyCode": "THANKS10" },
    { "type": "call", "text": "Call us", "phoneNumber": "+1234567890" }
  ]
}
```

### Button types

| `type` | Required fields | What the tap does | Event |
| - | - | - | - |
| `reply` (default) | `id`, `text` | Sends the choice back to you as a reply to the message | `message` with `type: "button_reply"` |
| `url` | `text`, `url` | Opens `url` in the recipient's browser | None |
| `copy` | `text`, `copyCode` | Copies `copyCode` to the recipient's clipboard | None |
| `call` | `text`, `phoneNumber` | Starts a call to `phoneNumber` from the recipient's phone | None |

`type` can be omitted for reply buttons.

## Send a list

A list shows one button (`buttonText`) under the message. Tapping it opens the options, grouped into titled sections. The recipient picks one.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.wsapi.chat/messages/list \
    -H "Content-Type: application/json" \
    -H "X-Api-Key: YOUR_API_KEY" \
    -H "X-Instance-Id: YOUR_INSTANCE_ID" \
    -d '{
      "to": "1234567890@s.whatsapp.net",
      "header": "Book a table",
      "text": "Pick a time slot for your reservation.",
      "footer": "Example Bistro",
      "buttonText": "See times",
      "sections": [
        {
          "title": "Lunch",
          "rows": [
            { "id": "slot-1200", "title": "12:00", "description": "Terrace available" },
            { "id": "slot-1330", "title": "13:30" }
          ]
        },
        {
          "title": "Dinner",
          "rows": [
            { "id": "slot-1900", "title": "19:00" },
            { "id": "slot-2030", "title": "20:30", "description": "Indoor seating only" }
          ]
        }
      ]
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.wsapi.chat/messages/list", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": "YOUR_API_KEY",
      "X-Instance-Id": "YOUR_INSTANCE_ID",
    },
    body: JSON.stringify({
      to: "1234567890@s.whatsapp.net",
      header: "Book a table",
      text: "Pick a time slot for your reservation.",
      footer: "Example Bistro",
      buttonText: "See times",
      sections: [
        {
          title: "Lunch",
          rows: [
            { id: "slot-1200", title: "12:00", description: "Terrace available" },
            { id: "slot-1330", title: "13:30" },
          ],
        },
        {
          title: "Dinner",
          rows: [
            { id: "slot-1900", title: "19:00" },
            { id: "slot-2030", title: "20:30", description: "Indoor seating only" },
          ],
        },
      ],
    }),
  });
  const { id } = await response.json();
  ```
</CodeGroup>

A list with a single section does not need a section `title`.

## The AI label

Set `aiLabel: true` on either endpoint to show WhatsApp's **AI** badge on the message — useful when the message comes from an assistant or bot and you want to say so.

```json theme={null}
{
  "to": "1234567890@s.whatsapp.net",
  "text": "I can help with your booking. What would you like to do?",
  "aiLabel": true,
  "buttons": [
    { "id": "book", "text": "New booking" },
    { "id": "change", "text": "Change booking" }
  ]
}
```

The badge only applies to 1:1 chats. In a group, `aiLabel` is ignored and the message is sent without it.

## Limits

The limits follow the ones WhatsApp publishes for its Cloud API.

**Both endpoints**

| Field | Limit |
| - | - |
| `text` | Required, up to 1024 characters |
| `header` | Optional, up to 60 characters, text only |
| `footer` | Optional, up to 60 characters, text only |

**Buttons**

| Field | Limit |
| - | - |
| `buttons` | 1 to 3 buttons |
| `buttons[].text` | Required, up to 20 characters |
| `buttons[].id` | Required on `reply` buttons, unique within the message, up to 256 characters |
| `buttons[].url` | Required on `url` buttons; must be an `http://` or `https://` URL |
| `buttons[].copyCode` | Required on `copy` buttons, up to 256 characters |
| `buttons[].phoneNumber` | Required on `call` buttons; an international number, e.g. `+1234567890` |

**Lists**

| Field | Limit |
| - | - |
| `buttonText` | Required, up to 20 characters |
| `sections` | 1 to 10 sections |
| Rows | At least 1 per section, at most **10 in total** across all sections |
| `sections[].title` | Up to 24 characters; required on every section when there is more than one |
| `rows[].id` | Required, up to 200 characters, unique across the whole list |
| `rows[].title` | Required, up to 24 characters |
| `rows[].description` | Optional, up to 72 characters |

A request that breaks a limit is rejected with `400 Bad Request` and a message naming the field, and nothing is sent:

```json theme={null}
{
  "status": 400,
  "detail": "validation error: buttons[1].id \"confirm\" is duplicated"
}
```

## Handling the reply

A reply arrives as a `message` event with `type: "button_reply"` or `type: "list_reply"`. Route on the `id` you set on the button or row, and use `replyTo.id` — the ID returned by the send call — to find the message it answers. Only `reply` buttons and list rows produce events.

<Card title="Button and list replies" icon="reply" href="/events/interactive-replies">
  Payloads, correlation, and what to watch for in your handler
</Card>

## Good to know

* **WhatsApp controls how they are displayed.** Interactive messages use WhatsApp features that may change as the app evolves. A `201` means WhatsApp accepted the message, so accepting a typed answer as well (for example, "confirm") is a good habit.
* **The AI label applies to 1:1 chats only**, and is ignored in groups.
* **Header and footer are text only.** Images, videos and documents in the header are not supported.
* **The common send options are not available.** These endpoints do not take `mentions`, `replyTo`, `isForwarded` or `ephemeralExpiration`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.