Carrossel e Flows
Envie um carrossel de produtos e um formulário dentro do WhatsApp numa conexão oficial, e leia os toques e as respostas que voltam.
Dois formatos que só existem em conexão oficial, porque ambos são construídos sobre a Cloud API: um carrossel de cards de produto, e um Flow — um formulário que abre dentro do WhatsApp, sem mandar a pessoa para o navegador.
Tecnicamente não compartilham nada, mas chegam no mesmo lugar: os toques e o formulário preenchido voltam como webhook da conexão.
Nenhum dos dois funciona em conexão por QR Code. Enviado ali, um carrossel vira texto puro e um Flow é recusado.
Carrossel de produtos
Um carrossel é um template com um componente CAROUSEL: de 2 a 10 cards, cada um com header de mídia, corpo próprio e até dois botões. Todos os cards precisam ter o mesmo formato — mesmo tipo de header, mesma quantidade e tipo de botões. A Meta recusa um conjunto heterogêneo.
Dois uploads parecem a mesma coisa e não são intercambiáveis:
| Endpoint | O que devolve | Validade | |
|---|---|---|---|
| Definição | POST /v3/connections/{id}/templates/header-media | handle | Permanente, descreve o template |
| Envio | POST /v3/connections/{id}/templates/send-media | mediaId | ~30 dias, preso ao número |
Nunca persista o mediaId. Suba de novo a cada envio.
Suba as imagens de amostra
Um handle por card, em templates/header-media. É o que a Meta analisa.
Crie o template
POST /v3/connections/{id}/templates com categoria MARKETING, um BODY e um CAROUSEL cujos cards[].components tragam HEADER (com example.header_handle), BODY e BUTTONS.
Aguarde a aprovação
A resposta volta PENDING. Consulte GET /templates/{templateId} até virar APPROVED; um rejected_reason diz o que corrigir.
Suba as imagens de novo, para envio
Agora em templates/send-media, que devolve o mediaId que cada header de card precisa.
Envie
POST /v3/connections/{id}/chats/messages/send-template com um único componente carousel.
{
"to": "5511999998888",
"name": "catalogo_demo",
"language": "pt_BR",
"components": [
{ "type": "carousel", "cards": [
{ "card_index": 0, "components": [
{ "type": "header", "parameters": [{ "type": "image", "image": { "id": "28801092679526768" } }] },
{ "type": "button", "sub_type": "quick_reply", "index": 0,
"parameters": [{ "type": "payload", "payload": "add:SKU-FONE-ANC" }] },
{ "type": "button", "sub_type": "quick_reply", "index": 1,
"parameters": [{ "type": "payload", "payload": "det:SKU-FONE-ANC" }] }
]}
]}
]
}
O casing muda entre definir e enviar
Definir um template usa HEADER, BUTTONS, QUICK_REPLY. Enviar usa header, button, quick_reply — e o index do botão é inteiro, não string. É assimetria da própria Meta; a nossa validação aponta todos os campos errados de uma vez.
Dê um payload próprio a cada botão. O webhook não traz índice de card, então add:SKU-FONE-ANC nomeando produto e ação é o único jeito de saber qual card foi tocado.
Flows
Um Flow é um formulário com telas próprias. Existem dois tipos, e a diferença decide todo o resto:
navigate | data_exchange | |
|---|---|---|
| Telas | fixas no Flow JSON | seu backend responde cada passo |
| Opções de dropdown | fixas no JSON | vêm do seu banco, na hora |
| Validação | só formato | sua (o cupom vale? o horário ainda está livre?) |
| Precisa da chave de criptografia | não | sim |
Agendamento é o caso canônico de data_exchange: o cliente escolhe o dia, e os horários livres daquele dia precisam sair do seu sistema naquele instante.
Crie e publique
POST /v3/connections/{id}/flows com flowJson (objeto ou string) e publish: true. Confira validation_errors na resposta antes de considerar publicado.
Envie
POST /v3/connections/{id}/chats/messages/send-flow. Dentro da janela de 24h — fora dela, use template com botão FLOW.
{
"to": "5511999998888",
"header": "Fale com a gente",
"body": "Preencha o formulário e a gente te responde hoje.",
"cta": "Abrir formulário",
"flowId": "1626797462403345",
"flowAction": "navigate",
"screen": "FORMULARIO",
"flowToken": "lead-8812",
"mode": "published"
}
flowToken é a sua correlação e o único fio de volta — o webhook que traz o formulário preenchido não inclui o id do Flow. Mande um id de pedido, de ticket, de lead. Se omitir, a gente gera pingo_<uuid> e devolve na resposta do envio.
Flows dinâmicos
Para data_exchange, a Meta criptografa cada requisição com RSA-2048 e AES-128-GCM. O Pingo é o endpoint registrado: ele decripta, repassa o JSON em claro para você, assinado, e encripta a sua resposta de volta.
Provisione a chave
PUT /v3/connections/{id}/flows-encryption. O Pingo gera o par RSA e registra a pública na Meta. Exige o app secret da Meta na conexão — é ele que autentica cada requisição que a Meta envia.
Inscreva um webhook em `flow.data_exchange`
Não existe URL separada para Flows: quem responde as telas é um webhook normal da conexão inscrito nesse evento. O HMAC precisa estar ligado.
Aponte o Flow para o Pingo
Coloque a dataExchangeUrl do GET /flows-encryption no endpointUri do Flow. O Flow JSON também precisa de data_api_version: "3.0" e routing_model.
O seu endpoint então recebe, no meio do preenchimento:
{ "action": "data_exchange", "screen": "ESCOLHER_DIA",
"data": { "dia": "2026-10-05" }, "flow_token": "agendamento-8812" }
e responde com a próxima tela:
{ "screen": "ESCOLHER_HORA",
"data": { "horarios": [{ "id": "09:00", "title": "09:00" }, { "id": "14:30", "title": "14:30" }] } }
Este evento é uma pergunta, não uma notificação
Todos os outros webhooks ignoram a sua resposta. Aqui o corpo que você devolve é a tela que o cliente vê, e o WhatsApp está esperando: responda em cerca de cinco segundos, com um objeto JSON. Um 200 vazio quebra o formulário. A chave é do número, não da conexão — ela sobrevive quando você apaga a conexão.
O que volta
Toques e respostas chegam como messages.upsert no webhook da conexão, nos nós documentados na referência:
| O que a pessoa fez | messageType | Onde está o id |
|---|---|---|
| Tocou um botão do carrossel | templateButtonReplyMessage | selectedId — o payload que você mandou |
| Enviou um Flow | interactiveResponseMessage | nativeFlowResponseMessage.paramsJson, uma string — faça o parse |
O formulário preenchido chega sempre aqui, nos dois tipos de Flow. O endpoint de data-exchange só cuida da conversa durante o preenchimento — muita gente configura ele e depois se pergunta por que as respostas nunca apareceram.
Um 201 no envio é a Meta aceitando a chamada, não entregando a mensagem. O desfecho chega depois, assíncrono, em messages.update — leia statuses[].errors. O erro 131047 significa janela de 24h fechada.