# WhatsApp media carousels

A media carousel is a set of two to ten cards the recipient swipes through side by side, each with its own image or video, its own short text, and its own buttons. Use it to show several items at once, such as a handful of products, rather than sending one message per item.

## Send a carousel

Set `interactive.type` to `carousel`, with a message-level `body_text` and a `cards` array of 2 to 10 entries:

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

```typescript
const msg = await bird.whatsapp.send({
  to: "+16505551234",
  from: "+13124495648",
  interactive: {
    type: "carousel",
    body_text: "Here are two of our latest arrivals, each under $25:",
    cards: [
      {
        header: { type: "image", url: "https://cdn.example.com/plants/blue-echeveria.jpeg" },
        buttons: [
          {
            type: "cta_url",
            cta_url: { text: "Buy now", url: "https://shop.example.com/blue-echeveria" },
          },
        ],
      },
      {
        header: { type: "image", url: "https://cdn.example.com/plants/zebra-haworthia.jpeg" },
        buttons: [
          {
            type: "cta_url",
            cta_url: { text: "Buy now", url: "https://shop.example.com/zebra-haworthia" },
          },
        ],
      },
    ],
  },
});
console.log(msg.id, msg.status);
```

```python
msg = client.whatsapp.send(
    to="+16505551234",
    from_="+13124495648",
    interactive={
        "type": "carousel",
        "body_text": "Here are two of our latest arrivals, each under $25:",
        "cards": [
            {
                "header": {"type": "image", "url": "https://cdn.example.com/plants/blue-echeveria.jpeg"},
                "buttons": [{"type": "cta_url", "cta_url": {"text": "Buy now", "url": "https://shop.example.com/blue-echeveria"}}],
            },
            {
                "header": {"type": "image", "url": "https://cdn.example.com/plants/zebra-haworthia.jpeg"},
                "buttons": [{"type": "cta_url", "cta_url": {"text": "Buy now", "url": "https://shop.example.com/zebra-haworthia"}}],
            },
        ],
    },
)
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:     "carousel",
			BodyText: "Here are two of our latest arrivals, each under $25:",
			Cards: &[]bird.WhatsAppInteractiveCardSend{
				{
					Header:  bird.WhatsAppInteractiveCardHeaderSend{Type: "image", Url: "https://cdn.example.com/plants/blue-echeveria.jpeg"},
					Buttons: []bird.WhatsAppInteractiveButtonSend{{Type: "cta_url", CtaUrl: &bird.WhatsAppInteractiveCtaUrlSend{Text: "Buy now", Url: "https://shop.example.com/blue-echeveria"}}},
				},
				{
					Header:  bird.WhatsAppInteractiveCardHeaderSend{Type: "image", Url: "https://cdn.example.com/plants/zebra-haworthia.jpeg"},
					Buttons: []bird.WhatsAppInteractiveButtonSend{{Type: "cta_url", CtaUrl: &bird.WhatsAppInteractiveCtaUrlSend{Text: "Buy now", Url: "https://shop.example.com/zebra-haworthia"}}},
				},
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(msg.Id, *msg.Status)
}
```

```php
$interactive = (new WhatsAppMessageSendRequestInteractive())
    ->setType('carousel')
    ->setBodyText('Here are two of our latest arrivals, each under $25:')
    ->setCards([
        (new WhatsAppInteractiveCardSend())
            ->setHeader((new WhatsAppInteractiveCardSendHeader())->setType('image')->setUrl('https://cdn.example.com/plants/blue-echeveria.jpeg'))
            ->setButtons([
                (new WhatsAppInteractiveButtonSend())
                    ->setType('cta_url')
                    ->setCtaUrl((new WhatsAppInteractiveButtonSendCtaUrl())->setText('Buy now')->setUrl('https://shop.example.com/blue-echeveria')),
            ]),
        (new WhatsAppInteractiveCardSend())
            ->setHeader((new WhatsAppInteractiveCardSendHeader())->setType('image')->setUrl('https://cdn.example.com/plants/zebra-haworthia.jpeg'))
            ->setButtons([
                (new WhatsAppInteractiveButtonSend())
                    ->setType('cta_url')
                    ->setCtaUrl((new WhatsAppInteractiveButtonSendCtaUrl())->setText('Buy now')->setUrl('https://shop.example.com/zebra-haworthia')),
            ]),
    ]);

$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":"carousel","body_text":"Here are two of our latest arrivals, each under $25:","cards":[{"header":{"type":"image","url":"https://cdn.example.com/plants/blue-echeveria.jpeg"},"buttons":[{"type":"cta_url","cta_url":{"text":"Buy now","url":"https://shop.example.com/blue-echeveria"}}]},{"header":{"type":"image","url":"https://cdn.example.com/plants/zebra-haworthia.jpeg"},"buttons":[{"type":"cta_url","cta_url":{"text":"Buy now","url":"https://shop.example.com/zebra-haworthia"}}]}]}'
```

```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": "carousel",
      "body_text": "Here are two of our latest arrivals, each under $25:",
      "cards": [
        {
          "header": { "type": "image", "url": "https://cdn.example.com/plants/blue-echeveria.jpeg" },
          "buttons": [{ "type": "cta_url", "cta_url": { "text": "Buy now", "url": "https://shop.example.com/blue-echeveria" } }]
        },
        {
          "header": { "type": "image", "url": "https://cdn.example.com/plants/zebra-haworthia.jpeg" },
          "buttons": [{ "type": "cta_url", "cta_url": { "text": "Buy now", "url": "https://shop.example.com/zebra-haworthia" } }]
        }
      ]
    }
  }'
```

<!-- /bird:tabs -->

`from` is required on every service message: a number your workspace owns, not a Bird-managed one. The full shape adds a card's own text, a second quick-reply button, and a quote of an earlier message:

```json
{
  "to": "+16505551234",
  "from": "+13124495648",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "interactive": {
    "type": "carousel",
    "body_text": "Here are two of our latest arrivals, each under $25:",
    "cards": [
      {
        "header": { "type": "image", "url": "https://cdn.example.com/plants/blue-echeveria.jpeg" },
        "body_text": "Blue Echeveria. Powdery blue leaves.",
        "buttons": [
          { "type": "quick_reply", "quick_reply": { "slug": "buy-echeveria", "text": "Buy" } },
          { "type": "quick_reply", "quick_reply": { "slug": "info-echeveria", "text": "Details" } }
        ]
      },
      {
        "header": { "type": "image", "url": "https://cdn.example.com/plants/zebra-haworthia.jpeg" },
        "body_text": "Zebra Haworthia. White stripes on deep green leaves.",
        "buttons": [
          { "type": "quick_reply", "quick_reply": { "slug": "buy-haworthia", "text": "Buy" } },
          { "type": "quick_reply", "quick_reply": { "slug": "info-haworthia", "text": "Details" } }
        ]
      }
    ]
  },
  "tags": [{ "name": "category", "value": "catalog" }],
  "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.

A carousel takes no message-level header and no footer: the message's `body_text` is the only text above the cards. See the hub's [buttons](/docs/guides/whatsapp/message-types/interactive#buttons) section for the shared button shape this type's cards reuse.

## Cards

Each card carries its own media header, its own short text, and its own buttons:

- **`header`** is required on every card, and it is `image` or `video` only: no text and no document header, unlike the other interactive types.
- **`body_text`** is optional. It sits below the card's media, capped shorter than a message body, and allows at most two line breaks.
- **`buttons`** is required: either one `cta_url` button or up to three `quick_reply` buttons, never a mix on the same card.

Cards render left to right in the order they appear in the `cards` array. A card has no footer and no index field of its own; its position in the array is its position in the carousel.

## Every card carries the same buttons

Every card in a carousel must carry **the same button types, the same number of them, in the same order**. A carousel where card 1 has one `cta_url` button and card 2 has two `quick_reply` buttons is refused, and so is a carousel where every card has two `quick_reply` buttons but in a different order.

The reason is how WhatsApp renders the message: a carousel is one card view with a shared layout, not a set of independently laid-out cards. A card with a different button row would break that shared layout, so WhatsApp requires every card to match and Bird checks it before the send is created or charged. A mismatch returns [E15059](/docs/api/errors/E15059).

Button labels are a separate rule, and it is scoped differently: a label must be unique **within a card**, not across the whole carousel. "Buy now" on every one of ten cards is fine; "Buy now" twice on the same card returns [E15056](/docs/api/errors/E15056).

## Limits

| Field                                             | Bound                                                                    |
| ------------------------------------------------- | ------------------------------------------------------------------------ |
| `cards`                                           | 2 to 10 entries                                                          |
| Card `header`                                     | required on every card; `image` or `video` only                          |
| Card `header.url`                                 | required, no maximum length                                              |
| Card `body_text`                                  | optional, 1 to 160 characters, at most 2 line breaks                     |
| Card `buttons`                                    | 1 to 3 entries: one `cta_url`, or up to three `quick_reply`, never mixed |
| Button label (`quick_reply.text`, `cta_url.text`) | required, 1 to 20 characters, unique within the card                     |
| `quick_reply.slug`                                | required, 1 to 256 characters                                            |
| `cta_url.url`                                     | required, 1 to 2000 characters                                           |
| Message `body_text`                               | required, 1 to 1024 characters                                           |
| Message header, footer                            | not allowed on a carousel: no `header`, no `footer_text`                 |

Bird caps `quick_reply` buttons at three per card. Meta itself states no numeric limit, only that a card takes either one link button or one or more reply buttons, so this ceiling is Bird's own, not WhatsApp's.

## Reading the reply

Only a `quick_reply` card button produces a reply. A tap on it 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": "buy-echeveria",
      "text": "Buy"
    }
  },
  "created_at": "2026-08-25T09:04:11Z"
}
```

The `slug` you set on the tapped button comes back verbatim on `interactive_reply.button.slug`, the same shape a reply-buttons tap produces. 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.

A `cta_url` button on a card opens its link in the recipient's browser and sends nothing back, the same as a standalone [link button](/docs/guides/whatsapp/message-types/interactive/cta-url-buttons).

## Free-form carousels and template carousels

This page covers the free-form carousel you send inline with `interactive.type: "carousel"`, deliverable only inside an open customer service window and never reviewed by Meta. [WhatsApp templates](/docs/guides/whatsapp/templates) has its own, separate carousel: a template component authored once, submitted to Meta for approval, and sent by slug like any other template, including outside the window. The two share the word "carousel" and Meta's 2-to-10 card range, and nothing else: different wire shapes, different review paths, and a template carousel's card count is fixed at the template's approval rather than chosen per send. If you're browsing templates and see "carousel" there, that's the template type, not this page.

## Limits and edge cases

- **The customer service window has to be open.** A carousel 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.
- **Card media has to be publicly reachable when the send dispatches.** Bird does not store or proxy the file: WhatsApp fetches each card's `url` itself, at send time, so a signed URL has to outlive the send.
- **A card media URL that WhatsApp can't fetch is accepted, then fails asynchronously, and is still charged.** Bird's request validation only checks that a card's `url` is a well-formed URI, not that WhatsApp can reach it or that it uses `https`. An oversize file, a 404, an unresolvable host, or the wrong file type all come back as a `202` at accept, then `whatsapp.accepted` then `whatsapp.sent` then `whatsapp.failed`, with `media_rejected` on the message's `last_error` and the cost of the send already charged with no refund path. Test each card's URL before sending, since a broken one is not caught until after the fact.
- **Every card must carry the same buttons.** See [Every card carries the same buttons](#every-card-carries-the-same-buttons) above; this is the one carousel rule the request schema can't express on its own, so it's checked separately and returns [E15059](/docs/api/errors/E15059) rather than a generic validation error.
- **No message-level header or footer.** A carousel's only text above the cards is `body_text`; there's nowhere to put small print the way the other types use `footer_text`.
- **The reply carries no card index.** A card's `quick_reply` tap reports only `{slug, text}`, the same shape as a reply-buttons tap, with no field naming which card it came from. If you need to know which card was tapped, encode the card in each button's `slug`, such as `buy-echeveria` rather than a bare `buy`.
- **A `cta_url` card button generates no inbound event.** If you need to know a card was engaged with, use `quick_reply` buttons on that card instead, or track the click on your own destination URL.

Beyond E15059, the only interactive error specific to a carousel is [E15056](/docs/api/errors/E15056) for a repeated button label on one card. [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
- [WhatsApp templates](/docs/guides/whatsapp/templates): for a carousel that sends outside the customer service window
- [How sending works](/docs/guides/whatsapp/sending-whatsapp): the request envelope, the `202` model, and safe retries