Mensagens

7. Mensagens#

Todas as operações assíncronas desta seção (send, forward, delete, edit) são processadas em fila. A API retorna 202 Accepted com o message_id da Catcher (UUID pré-gerado na aceitação da request), idempotency_key e task_id para rastreamento e deduplicação segura de retries do cliente.

O que o canal oficial suporta#

A plataforma oficial da Meta (Cloud API) suporta os tipos abaixo. Envie sempre pelo mesmo endpoint REST — a Catcher roteia para a Cloud API.

Suportado Não suportado pela Cloud API
Texto, texto com preview de link Enquete (poll)
Imagem, vídeo, áudio (PTT), documento, sticker View-once, PTV, GIF
Reação Evento (event)
Localização Status / Stories
Contato (cartão) Encaminhar, apagar, editar mensagem
Template aprovado (HSM) — ver Templates Botões interativos "crus" (use um template com botões)

Um tipo não suportado retorna 501 com uma mensagem clara (… is not supported for the official channel) — não é falha da sua integração, é uma limitação da Cloud API. Fora da janela de 24h, um envio livre retorna 409 OUTSIDE_SERVICE_WINDOW; use um template aprovado para reabrir a conversa.

Os toques em botão/lista de um template chegam de volta como o evento message.interactive_reply_received com o selected_id e o texto selecionado — pronto para automação.

Header obrigatório: Idempotency-Key#

Todos os endpoints assincronos de mensagens exigem o header HTTP Idempotency-Key.

  • Use um valor único por operação lógica.
  • Repetir a mesma combinação instanceId + tipo de operação + Idempotency-Key não cria uma nova task.
  • O retry recebe o estado atual do mesmo request (queued, sent ou failed) com o mesmo message_id e o mesmo task_id.
  • Clientes browser podem enviar esse header diretamente: ele faz parte dos headers permitidos no preflight CORS junto com Authorization, Content-Type, X-API-Key e X-CSRF-Token.

Formato de destinatário#

O campo to aceita:

  • Número de telefone: 554137984905 (será convertido para JID automaticamente)
  • JID individual: 554137984905@s.whatsapp.net

Resposta padrão (202)#

Todas as operações que criam uma nova mensagem retornam:

json
{
  "status": "queued",
  "message_id": "550e8400-e29b-41d4-a716-446655440000",
  "task_id": "asynq:task:xxxx-xxxx",
  "idempotency_key": "msg-2026-03-21-0001"
}
  • message_id — UUID do BiaZap, pre-gerado no momento da aceitacao. E o mesmo valor que aparece como elemento de message_ids nos webhooks message.sent, message.delivered, message.read, message.played. Use este campo para correlacionar a resposta sincrona com os eventos assincronos (no consumer, message_ids[0] = message_id da resposta 202).
  • idempotency_key — eco do header enviado pelo cliente. Também aparece no webhook message.sent (campo idempotency_key) quando a mensagem foi originada via API.
  • task_id — identificador interno do Asynq. Util para debug/NOC; não deve ser usado como chave de correlação primaria (use message_id ou idempotency_key).

Operações que não criam um novo Message (delete, edit) retornam apenas status, task_id e idempotency_key; o cliente já possui o message_id alvo da operação.

Repetir a mesma chave idempotente devolve o mesmo message_id, task_id e o estado atual (queued, sent ou failed) sem enfileirar duplicidade. Este e o caminho oficial para um cliente recuperar o message_id caso tenha perdido a resposta original ou precise verificar o estado antes do primeiro webhook.

Confirmando entrega de verdade (o 202 não é entrega)#

O 202 significa apenas "aceito na fila". NÃO significa que a mensagem saiu, chegou, ou renderizou no destinatário. Para confirmar de verdade, acompanhe a escada de eventos (webhook/SSE/WS) correlacionando por message_id:

Evento Prova NÃO prova
202 queued (resposta HTTP) a task entrou na fila que saiu / chegou
message.sent o worker despachou pro WhatsApp; a Meta aceitou o proto que chegou no aparelho
message.delivered chegou no aparelho do destinatário que renderizou / tocou
message.read o destinatário abriu e o chat renderizou (texto, imagem, documento, sticker, localização, contato, link-preview)
message.played o destinatário tocou a mídia (áudio/PTT, vídeo)

O salto crítico é delivered → read/played: mídia pode entregar e falhar em renderizar/tocar no cliente (proto incompleto, container errado). Se um envio chega em delivered mas nunca dá read/played, trate como não consumido — não como entregue. Regra prática: "sem read/played, não chegou de verdade."

Pré-condição importante: o read/played só dispara se o destinatário tiver recibos de leitura ligados E efetivamente abrir/tocar a mensagem. Se o destinatário não produz recibo, o piso de verificação é message.delivered (chegou no aparelho) + inspecionar o message.received do lado dele e conferir que o conteúdo bate (==) com o enviado: mesma caption/content, mesmo message_type, media_id presente, phone correto. Isso prova que chegou correto, mesmo sem o sinal de leitura.

Campo quoted_id (reply)#

Todos os endpoints de envio que listam quoted_id aceitam:

  • UUID BiaZap (formato xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx): e o mesmo UUID retornado em message_ids nos webhooks (message.received, message.sent, etc). A API resolve internamente para o ID hexadecimal do WhatsApp antes de enfileirar.
  • ID hexadecimal do WhatsApp (ex.: 3EB0ABC123...): passa direto, sem lookup.

Se o quoted_id for um UUID e não existir no banco (mensagem removida, pertence a outra instância, ou nunca foi recebida por está instância), a API responde 404 Not Found com error_code=MESSAGE_NOT_FOUND. Não ha enfileiramento parcial: a mensagem so e enviada com reply válido ou não e enviada.

Isto vale para SendText, SendImage, SendVideo, SendAudio, SendDocument, SendSticker, SendLocation, SendContact e para os equivalentes de envio agendado via POST /v1/instances/{instanceId}/messages/scheduled.


Modo passivo (409 PASSIVE_MODE_ENABLED)#

Quando a instância está configurada em modo passivo (passive_mode_enabled=true em GET/PATCH /v1/instances/{id}/settings), todos os endpoints de envio (text, image, video, audio, document, sticker, location, contact, reaction, template) são bloqueados antes do enfileiramento:

json
{
  "error_code": "PASSIVE_MODE_ENABLED",
  "message": "instance is in passive mode — set passive_mode_enabled=false in /v1/instances/{id}/settings or pass X-Force-Send: true to override (audit-logged)",
  "trace_id": "..."
}
  • HTTP 409 Conflict.
  • Bypass: header X-Force-Send: true (registra audit log audit_action=passive_mode.force_send).
  • Modo passivo falha fechado: um erro transitorio no banco do tenant retorna 503/500 em vez de liberar o envio (proposito: não vazar outbound de uma instância sob hold legal/monitoramento).

Guard anti-spam (409 BLOCKED_*)#

Endpoints de envio passam por um guard anti-spam antes de enfileirar. Qualquer regra que dispare devolve 409 Conflict:

error_code Regra Campos extras no payload
BLOCKED_DUPLICATE_CONTENT Conteúdo identico já enviado nas últimas 24h remote_jid, content_hash, first_sent_at
BLOCKED_NO_RECIPROCITY Envios consecutivos sem nenhum inbound do contato — limite fixo remote_jid, count, threshold
BLOCKED_TEMPERATURE_LIMIT max_consecutive da curva de temperatura do contato excedido remote_jid, score, tier, max_consecutive, outbound_consecutive
  • A regra de conteúdo duplicado é controlada por anti_spam_guard_enabled (default true).
  • O limite consecutivo sem resposta é controlado pela combinação anti_spam_guard_enabled + anti_spam_temperature_enabled. Quando ambas estão ativas (default), a curva de temperatura por contato substitui o limite fixo — apenas BLOCKED_TEMPERATURE_LIMIT pode disparar. Quando a temperatura está desativada, apenas BLOCKED_NO_RECIPROCITY pode disparar. As duas nunca rodam ao mesmo tempo.
  • delete, edit, reaction e forward são isentos da regra de conteúdo duplicado (não ha shape de conteúdo comparavel), mas continuam contando para o limite consecutivo.
  • Bypass: header X-Force-Send: true (registra audit log audit_action=anti_spam.force_send).

POST/v1/instances/{instanceId}/messages/text#

Envia mensagem de texto.

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "text": "Ola! Como posso ajudar?",
  "quoted_id": "3EB0ABC123...",
  "mentions": ["5541988888888@s.whatsapp.net"]
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário (telefone ou JID)
text string sim Conteúdo da mensagem
quoted_id string não ID da mensagem respondida (UUID BiaZap ou hex WhatsApp; ver nota geral de quoted_id acima)
mentions []string não JIDs de usuários mencionados

Regras do humanize:

  • Controlado por instância via GET/PATCH /v1/instances/{instanceId}/settings (humanize_enabled, default true).
  • Quando ativo, o processor envia composing imediatamente antes de SendText e espera numero_de_caracteres(texto) * multiplier segundos, com multiplier aleatorio entre 0.1 e 0.5.
  • A aplicação ocorre apenas em mensagens de texto; mensagens de mídia seguem o fluxo legado.
  • Se a leitura da configuração falhar, o envio segue imediatamente para não bloquear a fila.

Envio de mídia: media_url, media_id ou arquivo direto#

Os endpoints POST /v1/instances/{instanceId}/messages/{image|video|audio|document|sticker} aceitam as tres formas de origem no mesmo endpoint:

  1. application/json com media_url: o servidor baixa a URL sincronamente durante o request (~500ms-2s para arquivos pequenos), aplica validação SSRF/MIME/64 MB, armazena no R2/S3, gera um media_id interno e enfileira o envio usando esse ID.
  2. application/json com media_id: reutiliza uma mídia já armazenada pelo endpoint POST /v1/instances/{instanceId}/media ou por mídia recebida.
  3. multipart/form-data com campo file: envia o arquivo binario direto no próprio endpoint; a API armazena em R2, gera um media_id e enfileira o envio usando esse ID.

Arquitetura de mídia: API resolve, worker só envia#

Independente da forma de origem (URL, media_id existente ou multipart), a API sempre garante que os bytes estejam armazenados no R2 antes de retornar 202. O worker só recebe media_id na fila Asynq — nunca uma URL externa para baixar. Isso elimina classe inteira de falha silenciosa em que a CDN externa bloqueia download depois do 202 e a mensagem nunca chega.

Prático para você (consumidor da API):

  • Falha de download é sincrona: se a URL retorna 4xx, 5xx, timeout, ou MIME bloqueado, você recebe 502 MEDIA_FETCH_FAILED no momento da chamada. NÃO vai pra fila, não tem retry silencioso. Retry o request com URL válida ou faca upload prévio.
  • Custo de latência: requests com media_url agora levam ~500ms-2s a mais que com media_id (tempo do download + upload R2). Para arquivos grandes (>10MB), considere fazer upload prévio via POST /v1/instances/{id}/media e reutilizar o media_id retornado.
  • Cache automático: cada media_url baixado vira uma linha no R2 com source="url-cache" e TTL de 7 dias. Reaproveitamento da mesma URL durante esse período é livre.

Campos textuais do envio (to, caption, file_name, ptt, quoted_id, mentions) podem ir no multipart como campos de formulario. mentions aceita valores repetidos, CSV ou JSON array.

Exemplo com arquivo direto:

bash
curl -X POST "https://api.catcher.one/v1/instances/$INSTANCE/messages/image" \
  -H "X-API-Key: $TOKEN" \
  -H "Idempotency-Key: req_01HX9Y..." \
  -F "to=554137984905" \
  -F "caption=Imagem enviada pela API" \
  -F "file=@/caminho/foto.png"

Exemplo com URL externa:

bash
curl -X POST "https://api.catcher.one/v1/instances/$INSTANCE/messages/video" \
  -H "X-API-Key: $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: req_01HX9Y..." \
  -d '{
    "to": "554137984905",
    "media_url": "https://meucdn.com/video.mp4",
    "caption": "Olha esse vídeo"
  }'
# → 202 só sai depois que os bytes estão no R2.
# → 502 MEDIA_FETCH_FAILED se o CDN externo recusar.

Exemplo com upload prévio (recomendado para arquivos grandes ou reutilizados):

bash
# 1. Upload uma vez:
MEDIA_ID=$(curl -sf -X POST "https://api.catcher.one/v1/instances/$INSTANCE/media" \
  -H "X-API-Key: $TOKEN" \
  -H "Idempotency-Key: upload-$(date +%s)" \
  -F "file=@/caminho/video-grande.mp4" | jq -r .media_id)

# 2. Reutilize em N envios sem rebaixar:
for PHONE in 554137984905 554196332719 554163475715; do
  curl -X POST "https://api.catcher.one/v1/instances/$INSTANCE/messages/video" \
    -H "X-API-Key: $TOKEN" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: bulk-$PHONE-$(date +%s)" \
    -d "{\"to\":\"$PHONE\",\"media_id\":\"$MEDIA_ID\",\"caption\":\"Veja como ficou\"}"
done

Em JSON, envie media_url OU media_id. Em multipart, envie file. Um desses tres e obrigatório.


POST/v1/instances/{instanceId}/messages/image#

Envia imagem.

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "media_url": "https://exemplo.com/foto.png",
  "caption": "Confira esta imagem!",
  "quoted_id": "",
  "mentions": []
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário
media_url string sim* URL pública da imagem
media_id string sim* ID de média pre-enviado via upload
file file sim* Arquivo direto via multipart/form-data
caption string não Legenda da imagem
quoted_id string não ID da mensagem respondida (UUID BiaZap ou hex WhatsApp; ver nota geral de quoted_id acima)
mentions []string não JIDs mencionados

*Enviar media_url, media_id OU file (um dos tres e obrigatório).


POST/v1/instances/{instanceId}/messages/video#

Envia video.

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "media_url": "https://exemplo.com/video.mp4",
  "caption": "Assista ao video"
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário
media_url string sim* URL pública do video
media_id string sim* ID de média pre-enviado
file file sim* Arquivo direto via multipart/form-data
caption string não Legenda
quoted_id string não ID da mensagem respondida (UUID BiaZap ou hex WhatsApp; ver nota geral de quoted_id acima)
mentions []string não JIDs mencionados

*Enviar media_url, media_id OU file (um dos tres e obrigatório).


Áudio: dois endpoints, dois jeitos#

O envio de áudio tem dois endpoints separados — escolha pelo tipo de mensagem, sem depender de flags:

Endpoint Renderiza como Waveform Recibo played Use quando
POST .../messages/voice mensagem de voz (PTT) ✅ ondinha quer o áudio "de voz", tipo gravar segurando o microfone
POST .../messages/audio arquivo de áudio ❌ player comum quer mandar um arquivo de áudio (música, gravação, anexo)

Ambos aceitam o mesmo corpo (to + media_url ou media_id ou file).


POST/v1/instances/{instanceId}/messages/voice#

Envia uma mensagem de voz (PTT) — renderizada com waveform (ondinha) + ícone de play, e o toque do destinatário gera o recibo message.played. O áudio é sempre convertido para OGG Opus (mono, 16 kHz, 32 kbps) — formato exigido pelo WhatsApp para voz; uma waveform de 64 bytes é gerada automaticamente. É o jeito de mandar "um áudio de voz".

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "media_url": "https://exemplo.com/audio.mp3"
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário
media_url string sim* URL pública do áudio (qualquer formato — convertido para OGG Opus)
media_id string sim* ID de mídia pré-enviado
file file sim* Arquivo direto via multipart/form-data
quoted_id string não ID da mensagem respondida (UUID Catcher ou hex WhatsApp)
mentions []string não JIDs mencionados

*Enviar media_url, media_id OU file (um dos três é obrigatório).

Ciclo de vida: sent → delivered → read → played. O played chega quando o destinatário toca o áudio (requer confirmações de leitura habilitadas no aparelho dele).


POST/v1/instances/{instanceId}/messages/audio#

Envia um arquivo de áudio — um player de áudio comum, sem waveform e sem recibo played. Para uma mensagem de voz (ondinha + played), use /messages/voice. Os bytes vão como estão, sem conversão.

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "media_url": "https://exemplo.com/audio.mp3"
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário
media_url string sim* URL pública do audio (qualquer formato: MP3, OGG, M4A, WAV, etc)
media_id string sim* ID de média pre-enviado
file file sim* Arquivo direto via multipart/form-data
ptt bool não Compatibilidade retroativa. Padrão false (arquivo de áudio). true força uma mensagem de voz (equivalente a /messages/voice). Prefira o endpoint /messages/voice.
quoted_id string não ID da mensagem respondida (UUID Catcher ou hex WhatsApp; ver nota geral de quoted_id acima)
mentions []string não JIDs mencionados

*Enviar media_url, media_id OU file (um dos tres e obrigatório).


POST/v1/instances/{instanceId}/messages/document#

Envia documento (PDF, DOCX, etc).

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "media_url": "https://exemplo.com/relatorio.pdf",
  "file_name": "relatorio-marco.pdf"
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário
media_url string sim* URL pública do documento
media_id string sim* ID de média pre-enviado
file file sim* Arquivo direto via multipart/form-data
file_name string não Nome do arquivo exibido no WhatsApp
quoted_id string não ID da mensagem respondida (UUID BiaZap ou hex WhatsApp; ver nota geral de quoted_id acima)
mentions []string não JIDs mencionados

*Enviar media_url, media_id OU file (um dos tres e obrigatório).


POST/v1/instances/{instanceId}/messages/sticker#

Envia sticker (figurinha). A imagem deve estar no formato WebP.

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "media_url": "https://exemplo.com/sticker.webp"
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário
media_url string sim* URL pública do sticker (WebP)
media_id string sim* ID de média pre-enviado
file file sim* Arquivo direto via multipart/form-data
quoted_id string não ID da mensagem respondida (UUID BiaZap ou hex WhatsApp; ver nota geral de quoted_id acima)
mentions []string não JIDs para mencionar

*Enviar media_url, media_id OU file (um dos tres e obrigatório).


POST/v1/instances/{instanceId}/messages/location#

Envia localização.

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "latitude": -25.4284,
  "longitude": -49.2733,
  "name": "Praca Tiradentes",
  "address": "Centro, Curitiba - PR"
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário
latitude float sim Latitude
longitude float sim Longitude
name string não Nome do local
address string não Endereço
quoted_id string não ID da mensagem respondida (UUID BiaZap ou hex WhatsApp; ver nota geral de quoted_id acima)
mentions []string não JIDs para mencionar

POST/v1/instances/{instanceId}/messages/contact#

Envia cartao de contato (vCard).

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "contact_name": "Suporte BiaZap",
  "contact_phone": "5541988887777"
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário
contact_name string sim Nome do contato no cartao
contact_phone string sim Telefone do contato
quoted_id string não ID da mensagem respondida (UUID BiaZap ou hex WhatsApp; ver nota geral de quoted_id acima)
mentions []string não JIDs para mencionar

POST/v1/instances/{instanceId}/messages/reaction#

Envia reacao (emoji) a uma mensagem.

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "message_id": "3EB0ABC123DEF456",
  "emoji": "👍"
}
Campo Tipo Obrigatório Descricao
to string sim JID do chat onde está a mensagem
message_id string sim ID da mensagem no WhatsApp
emoji string sim Emoji da reacao (string vazia para remover)

POST/v1/instances/{instanceId}/messages/template#

Envia mensagem com botoes (template). Requer conta WhatsApp Business.

Auth: Todos autenticados

Request:

json
{
  "to": "554137984905",
  "body_text": "Escolha uma opcao abaixo:",
  "footer_text": "Powered by BiaZap",
  "buttons": [
    {"text": "Comprar", "id": "btn_buy"},
    {"text": "Cancelar", "id": "btn_cancel"}
  ]
}
Campo Tipo Obrigatório Descricao
to string sim Destinatário
body_text string sim Texto principal
footer_text string não Texto do rodape
buttons []object sim 1 a 3 botoes
buttons[].text string sim Texto exibido no botao
buttons[].id string sim ID do botao (retornado no callback)

Nota: Se a instância não for uma conta Business, a mensagem será rejeitada sem retry.


POST/v1/instances/{instanceId}/messages/link-preview#

Envia uma mensagem de texto com card de preview de link controlado (você define título e descrição). Renderiza no destinatário (tipo nativo ExtendedTextMessage).

Auth: Todos autenticados · Header: Idempotency-Key obrigatório

Preview NÃO é o default. Um link num POST /messages/text comum vai como texto puro — o app do destinatário PODE gerar um preview básico do lado dele (fetch client-side), mas isso não é garantido nem controlado por você. Para um card com título/descrição garantidos, use este endpoint.

Request:

json
{
  "to": "554137984905",
  "text": "Conhece a Catcher? https://catcher.one",
  "url": "https://catcher.one",
  "title": "Catcher",
  "description": "WhatsApp API multi-tenant"
}
Campo Tipo Obrigatório Descrição
to string sim Destinatário
text string sim Corpo da mensagem (deve conter a URL)
url string sim URL do preview
title string não Título do card
description string não Descrição do card
quoted_id string não UUID Catcher OU hex WhatsApp da mensagem citada

Retorna 202 com message_id (UUID Catcher) + task_id + idempotency_key. Emite message.sent (message_type: text).


POST/v1/instances/{instanceId}/official/send-interactive#

Envia uma mensagem interativa nativa da Cloud API: botões de resposta (reply buttons), lista, botão CTA URL, pedido de localização, endereço, ou um Flow. Só disponível no canal oficial (whatsapp_official).

Auth: Todos autenticados · Header: Idempotency-Key obrigatório

Request:

json
{
  "to": "5541999998888",
  "kind": "cta_url",
  "interactive": {
    "type": "cta_url",
    "body": { "text": "Confira nosso catálogo completo" },
    "action": {
      "name": "cta_url",
      "parameters": { "display_text": "Ver catálogo", "url": "https://loja.exemplo.com" }
    }
  }
}
Campo Tipo Obrigatório Descrição
to string sim Destinatário (telefone ou JID)
kind string não Dica do tipo: button, list, cta_url, location_request, address, flow
interactive object sim O objeto interactive da Cloud API (type + body + action), montado por você

Mais exemplos (o objeto interactive segue o schema da Cloud API):

json
// Reply buttons (até 3) — o tap volta como interactive_reply_received
{ "to": "5541999998888", "kind": "button", "interactive": {
  "type": "button",
  "body": { "text": "Confirma o agendamento?" },
  "action": { "buttons": [
    { "type": "reply", "reply": { "id": "opt_sim", "title": "Sim" } },
    { "type": "reply", "reply": { "id": "opt_nao", "title": "Não" } } ] } } }

// Lista/menu (rows agrupadas em seções)
{ "to": "5541999998888", "kind": "list", "interactive": {
  "type": "list",
  "header": { "type": "text", "text": "Menu" },
  "body": { "text": "Escolha uma opção" },
  "action": { "button": "Ver opções", "sections": [
    { "title": "Planos", "rows": [
      { "id": "row_start", "title": "Plano Start", "description": "Para começar" } ] } ] } } }

// Pedido de localização — o toque abre o "compartilhar localização" do WhatsApp
{ "to": "5541999998888", "kind": "location_request", "interactive": {
  "type": "location_request_message",
  "body": { "text": "Compartilhe sua localização 📍" },
  "action": { "name": "send_location" } } }

Retorna 202 com message_id (UUID Catcher) + task_id + idempotency_key.

O que volta quando o contato interage:

Interação do contato Evento(s) que você recebe
Tap em reply button / item de lista message.received (type interactive, content = label tocado) + message.interactive_reply_received (reply_type button_reply/list_reply, selected_id, display_text/title/description)
Submit de um Flow message.interactive_reply_received com reply_type: "nfm" + o JSON da resposta do formulário
Resposta a location_request message.received type location com latitude/longitude, location_address (quando o lugar é nomeado), nome em content, e quoted_msg_id = UUID do pedido — correlacione pedido→resposta por ele
Clique no botão CTA URL Nenhum evento (o link abre no navegador do contato — não gera reply)

Carrossel: um carrossel é enviável pelo endpoint de template (POST .../official/send-template) usando componentes carousel no template aprovado. Flow: requer um Flow publicado no Flow Builder da sua WABA (o interactive referencia flow_id/flow_cta). Address: disponibilidade restrita por região pela Meta.