# Carousel templates (cards with an image or video)

`POST /v1/templates`

**One message with 2 to 10 cards that scroll sideways, each with its own photo or video, text and buttons.** You build it from your system and send it to review, like any template:

1. **One sample per card:** `POST /v1/templates/media` with the image (JPG/PNG) or video (MP4) → `handle`. That is what Meta reviews; each send carries the real photo.
2. **Create:** `POST /v1/templates` with `category: "MARKETING"`, the main `body` and `carousel.cards[]`. Each card: `header { format: IMAGE|VIDEO, handle }`, an optional `body` and up to 2 buttons (`QUICK_REPLY`, `URL`, `PHONE_NUMBER`). We check Meta's rules before Meta: the same number of cards when sending, the same format in every card, the same buttons in the same order, card text up to 160 characters (in every card or none) and examples for every `{{n}}`. `POST /v1/templates/validate` tries it without creating anything; `/preview?format=html` draws the sideways strip.
3. **Wait** for `template.status_changed` with `APPROVED`.
4. **Send:** `POST /v1/messages` with `template.carousel.cards[]`, one per card and in order: the `header` (`image`/`video` with `mediaId` or `link`), the `body` values and the button values (`quick_reply` → your `payload`, `url` → the end of the link).
5. **The tap:** `message.received` arrives with `button.payload` intact and **`tap { cardIndex, buttonIndex }`**. Meta does not send those indexes: we resolve them from your `payload`.

**What Meta sets:** a carousel exists **only as MARKETING** and every send is charged as marketing; Meta approves or rejects it like any template; since 1 April 2025 it **does not deliver marketing to US numbers** (`131049`), and its per-person caps apply to all marketing.

## Examples

### cURL

```bash
# 1) One sample image per card (Meta reviews them) → handles
H1=$(curl -s -X POST https://api.waiaconnect.com/v1/templates/media -H "Authorization: Bearer wc_live_YOUR_API_KEY" -F file=@remera.jpg | jq -r .handle)
H2=$(curl -s -X POST https://api.waiaconnect.com/v1/templates/media -H "Authorization: Bearer wc_live_YOUR_API_KEY" -F file=@buzo.jpg | jq -r .handle)

# 2) Create the carousel (MARKETING, 2–10 cards, same buttons in every card)
BODY='{"connectionId":"conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","name":"novedades_semana","language":"es","category":"MARKETING","body":{"text":"Hola {{1}}, mirá lo nuevo de esta semana.","examples":["Martín"]},"carousel":{"cards":[{"header":{"format":"IMAGE","handle":"H1"},"body":{"text":"Remera {{1}} de algodón.","examples":["azul"]},"buttons":[{"type":"QUICK_REPLY","text":"Me interesa"},{"type":"URL","text":"Ver","url":"https://your-shop.example/p/{{1}}","example":"remera-azul"}]},{"header":{"format":"IMAGE","handle":"H2"},"body":{"text":"Buzo {{1}} de algodón.","examples":["gris"]},"buttons":[{"type":"QUICK_REPLY","text":"Me interesa"},{"type":"URL","text":"Ver","url":"https://your-shop.example/p/{{1}}","example":"buzo-gris"}]}]}}'
BODY=${BODY/H1/$H1}; BODY=${BODY/H2/$H2}
curl -s -X POST https://api.waiaconnect.com/v1/templates/validate -H "Authorization: Bearer wc_live_YOUR_API_KEY" -H "Content-Type: application/json" -d "$BODY"
curl -s -X POST https://api.waiaconnect.com/v1/templates -H "Authorization: Bearer wc_live_YOUR_API_KEY" -H "Content-Type: application/json" -d "$BODY"

# 3) Wait for the template.status_changed webhook with status APPROVED

# 4) Send it: one card per card of the template, in order
curl -s -X POST https://api.waiaconnect.com/v1/messages -H "Authorization: Bearer wc_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"connectionId":"conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","to":"5493511234567","type":"template","template":{"name":"novedades_semana","language":"es","body":["Sofía"],"carousel":{"cards":[{"header":{"type":"image","link":"https://your-shop.example/img/remera-azul.jpg"},"body":["azul"],"buttons":[{"type":"quick_reply","payload":"remera-azul:me-interesa"},{"type":"url","text":"remera-azul"}]},{"header":{"type":"image","link":"https://your-shop.example/img/buzo-gris.jpg"},"body":["gris"],"buttons":[{"type":"quick_reply","payload":"buzo-gris:me-interesa"},{"type":"url","text":"buzo-gris"}]}]}}}'

# 5) The tap arrives as message.received:
#    data.message.button.payload = "buzo-gris:me-interesa"  (yours, intact)
#    data.tap = { "cardIndex": 1, "buttonIndex": 0 }
```

### Node.js

```javascript
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const H = { Authorization: "Bearer wc_live_YOUR_API_KEY" };
const J = { ...H, "Content-Type": "application/json" };

// 1) One sample image per card (Meta reviews them) → handles
const sample = async (file) => {
  const form = new FormData();
  form.append("file", new Blob([await readFile(file)]), file);
  return (await (await fetch("https://api.waiaconnect.com/v1/templates/media", { method: "POST", headers: H, body: form })).json()).handle;
};
const [h1, h2] = [await sample("remera.jpg"), await sample("buzo.jpg")];

// 2) Create the carousel (MARKETING, 2–10 cards, same buttons in every card)
const def = {
  "connectionId": "conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "name": "novedades_semana",
  "language": "es",
  "category": "MARKETING",
  "body": {
    "text": "Hola {{1}}, mirá lo nuevo de esta semana.",
    "examples": [
      "Martín"
    ]
  },
  "carousel": {
    "cards": [
      {
        "header": {
          "format": "IMAGE",
          "handle": h1
        },
        "body": {
          "text": "Remera {{1}} de algodón.",
          "examples": [
            "azul"
          ]
        },
        "buttons": [
          {
            "type": "QUICK_REPLY",
            "text": "Me interesa"
          },
          {
            "type": "URL",
            "text": "Ver",
            "url": "https://your-shop.example/p/{{1}}",
            "example": "remera-azul"
          }
        ]
      },
      {
        "header": {
          "format": "IMAGE",
          "handle": h2
        },
        "body": {
          "text": "Buzo {{1}} de algodón.",
          "examples": [
            "gris"
          ]
        },
        "buttons": [
          {
            "type": "QUICK_REPLY",
            "text": "Me interesa"
          },
          {
            "type": "URL",
            "text": "Ver",
            "url": "https://your-shop.example/p/{{1}}",
            "example": "buzo-gris"
          }
        ]
      }
    ]
  }
};
const v = await (await fetch("https://api.waiaconnect.com/v1/templates/validate", { method: "POST", headers: J, body: JSON.stringify(def) })).json();
if (!v.ok) throw new Error(v.errors.map(e => `${e.field}: ${e.message}`).join("\n"));
await fetch("https://api.waiaconnect.com/v1/templates", { method: "POST", headers: J, body: JSON.stringify(def) });

// 3) Wait for template.status_changed (APPROVED) on your webhook, then:
// 4) Send it: one card per card of the template, in order
await fetch("https://api.waiaconnect.com/v1/messages", {
  method: "POST",
  headers: { ...J, "Idempotency-Key": randomUUID() },
  body: JSON.stringify({
    "connectionId": "conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "to": "5493511234567",
    "type": "template",
    "template": {
      "name": "novedades_semana",
      "language": "es",
      "body": [
        "Sofía"
      ],
      "carousel": {
        "cards": [
          {
            "header": {
              "type": "image",
              "link": "https://your-shop.example/img/remera-azul.jpg"
            },
            "body": [
              "azul"
            ],
            "buttons": [
              {
                "type": "quick_reply",
                "payload": "remera-azul:me-interesa"
              },
              {
                "type": "url",
                "text": "remera-azul"
              }
            ]
          },
          {
            "header": {
              "type": "image",
              "link": "https://your-shop.example/img/buzo-gris.jpg"
            },
            "body": [
              "gris"
            ],
            "buttons": [
              {
                "type": "quick_reply",
                "payload": "buzo-gris:me-interesa"
              },
              {
                "type": "url",
                "text": "buzo-gris"
              }
            ]
          }
        ]
      }
    }
  })
});

// 5) In your webhook handler: which card and which button
export const onEvent = (evt) => {
  if (evt.type !== "message.received" || evt.data.message.type !== "button") return;
  const payload = evt.data.message.button.payload; // yours, intact
  const { cardIndex, buttonIndex } = evt.data.tap ?? {}; // e.g. 1 and 0
  console.log(payload, cardIndex, buttonIndex);
};
```

### PHP

```php
<?php
$key = "wc_live_YOUR_API_KEY";
function call($url, $key, $body, $json = true, $extra = []) {
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => array_merge(["Authorization: Bearer $key"], $json ? ["Content-Type: application/json"] : [], $extra),
    CURLOPT_POSTFIELDS => $json ? json_encode($body) : $body,
  ]);
  return json_decode(curl_exec($ch), true);
}
// 1) One sample image per card → handles
$h1 = call("https://api.waiaconnect.com/v1/templates/media", $key, ["file" => new CURLFile("remera.jpg")], false)["handle"];
$h2 = call("https://api.waiaconnect.com/v1/templates/media", $key, ["file" => new CURLFile("buzo.jpg")], false)["handle"];

// 2) Create the carousel (MARKETING, 2–10 cards, same buttons in every card)
$def = ["connectionId" => "conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","name" => "novedades_semana","language" => "es","category" => "MARKETING","body" => ["text" => "Hola {{1}}, mirá lo nuevo de esta semana.","examples" => ["Martín"]],"carousel" => ["cards" => [["header" => ["format" => "IMAGE","handle" => $h1],"body" => ["text" => "Remera {{1}} de algodón.","examples" => ["azul"]],"buttons" => [["type" => "QUICK_REPLY","text" => "Me interesa"],["type" => "URL","text" => "Ver","url" => "https://your-shop.example/p/{{1}}","example" => "remera-azul"]]],["header" => ["format" => "IMAGE","handle" => $h2],"body" => ["text" => "Buzo {{1}} de algodón.","examples" => ["gris"]],"buttons" => [["type" => "QUICK_REPLY","text" => "Me interesa"],["type" => "URL","text" => "Ver","url" => "https://your-shop.example/p/{{1}}","example" => "buzo-gris"]]]]]];
call("https://api.waiaconnect.com/v1/templates", $key, $def);

// 3) Wait for template.status_changed (APPROVED) · 4) send it, one card per card, in order
$send = ["connectionId" => "conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","to" => "5493511234567","type" => "template","template" => ["name" => "novedades_semana","language" => "es","body" => ["Sofía"],"carousel" => ["cards" => [["header" => ["type" => "image","link" => "https://your-shop.example/img/remera-azul.jpg"],"body" => ["azul"],"buttons" => [["type" => "quick_reply","payload" => "remera-azul:me-interesa"],["type" => "url","text" => "remera-azul"]]],["header" => ["type" => "image","link" => "https://your-shop.example/img/buzo-gris.jpg"],"body" => ["gris"],"buttons" => [["type" => "quick_reply","payload" => "buzo-gris:me-interesa"],["type" => "url","text" => "buzo-gris"]]]]]]];
call("https://api.waiaconnect.com/v1/messages", $key, $send, true, ["Idempotency-Key: " . bin2hex(random_bytes(16))]);

// 5) In your webhook: $evt["data"]["message"]["button"]["payload"] (yours, intact)
//    and $evt["data"]["tap"]["cardIndex"], $evt["data"]["tap"]["buttonIndex"]
```

### Python

```python
import uuid, requests
H = {"Authorization": "Bearer wc_live_YOUR_API_KEY"}

# 1) One sample image per card (Meta reviews them) -> handles
h1 = requests.post("https://api.waiaconnect.com/v1/templates/media", headers=H, files={"file": open("remera.jpg", "rb")}).json()["handle"]
h2 = requests.post("https://api.waiaconnect.com/v1/templates/media", headers=H, files={"file": open("buzo.jpg", "rb")}).json()["handle"]

# 2) Create the carousel (MARKETING, 2-10 cards, same buttons in every card)
definition = {"connectionId":"conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","name":"novedades_semana","language":"es","category":"MARKETING","body":{"text":"Hola {{1}}, mirá lo nuevo de esta semana.","examples":["Martín"]},"carousel":{"cards":[{"header":{"format":"IMAGE","handle":h1},"body":{"text":"Remera {{1}} de algodón.","examples":["azul"]},"buttons":[{"type":"QUICK_REPLY","text":"Me interesa"},{"type":"URL","text":"Ver","url":"https://your-shop.example/p/{{1}}","example":"remera-azul"}]},{"header":{"format":"IMAGE","handle":h2},"body":{"text":"Buzo {{1}} de algodón.","examples":["gris"]},"buttons":[{"type":"QUICK_REPLY","text":"Me interesa"},{"type":"URL","text":"Ver","url":"https://your-shop.example/p/{{1}}","example":"buzo-gris"}]}]}}
requests.post("https://api.waiaconnect.com/v1/templates", headers=H, json=definition).raise_for_status()

# 3) Wait for template.status_changed (APPROVED) on your webhook, then
# 4) send it: one card per card of the template, in order
requests.post("https://api.waiaconnect.com/v1/messages", headers={**H, "Idempotency-Key": str(uuid.uuid4())}, json={"connectionId":"conn_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","to":"5493511234567","type":"template","template":{"name":"novedades_semana","language":"es","body":["Sofía"],"carousel":{"cards":[{"header":{"type":"image","link":"https://your-shop.example/img/remera-azul.jpg"},"body":["azul"],"buttons":[{"type":"quick_reply","payload":"remera-azul:me-interesa"},{"type":"url","text":"remera-azul"}]},{"header":{"type":"image","link":"https://your-shop.example/img/buzo-gris.jpg"},"body":["gris"],"buttons":[{"type":"quick_reply","payload":"buzo-gris:me-interesa"},{"type":"url","text":"buzo-gris"}]}]}}})

# 5) In your webhook handler: which card and which button
def on_event(evt):
    if evt["type"] == "message.received" and evt["data"]["message"]["type"] == "button":
        payload = evt["data"]["message"]["button"]["payload"]  # yours, intact
        tap = evt["data"].get("tap")  # {"cardIndex": 1, "buttonIndex": 0}
        print(payload, tap)
```

## Notes

- Give every quick reply its own `payload` (e.g. `buzo-gris:interested`): with two identical ones in the same message we can't tell which was tapped, and `tap` is left out. An "Unsubscribe" quick reply arrives the same way: your system records the opt-out.
- Link and call buttons produce no event: Meta opens the browser or the phone.
- A carousel created in WhatsApp Manager syncs with its cards (`GET /v1/templates`, `carousel.cards`) and sends the same way.
- The **catalog product** carousel ("View" button, buying inside WhatsApp) is not available yet.
