Building with AI? Connect it to your account over MCP: it checks your setup, creates webhooks and templates with your confirmation and writes the code for you. See how →
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.
# 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 }
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)
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.