Mensagens (HTTP)
Endpoints /v1/channels/:ref/messages — envio único, lote e rastreio.
O envio é por um número connected. O :ref na rota é o id do número (ch_…) ou o telefone (E.164 sem +) — o broker resolve os dois para o número vivo do tenant.
| método | path | o que faz |
|---|---|---|
POST | /v1/channels/:ref/messages | envia uma mensagem |
POST | /v1/channels/:ref/messages/batch | envia em lote (lista de envios independentes) |
GET | /v1/messages/:id | rastreia uma mensagem |
GET | /v1/messages/metrics | contagens do pipeline de disparo |
POST | /v1/messages/:id/renotify | re-notifica o status atual de uma mensagem (unitário) |
POST | /v1/messages/renotify | re-notifica em lote, por filtro |
Enviar uma mensagem
O corpo carrega to (destinatário, E.164 sem +), um content discriminado por kind, uma idempotencyKey obrigatória e, opcionalmente, replyToProviderId, ttlSeconds e metadata. A resposta é 202 Accepted (aceite, não confirmação de entrega) e ecoa o idempotencyKey ao lado do messageId — mesmo gancho de correlação do resultado de lote, para o unitário e o item de lote terem a mesma forma de resposta.
curl -X POST https://api.grwthy.com/v1/channels/5511999999999/messages \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "5511988887777",
"content": { "kind": "text", "text": "Seu pedido saiu para entrega." },
"idempotencyKey": "pedido-1234-saiu",
"metadata": { "contrato": "CT-88213", "nome": "Maria Silva" }
}'Resposta 202
{
"idempotencyKey": "pedido-1234-saiu",
"idempotent": false,
"messageId": "msg_7b3e1d9a2c4f6081",
"state": "queued",
"traceId": "tr_2f8b6d0a4c1e3597"
}metadata: opaco, capado, ecoado no message.sent
metadata é um mapa opcional, string→string, de chaves livres (qualquer propriedade sua — nº de contrato, nome, documento). O broker nunca interpreta, indexa ou loga o conteúdo: só valida a forma (capado em ≤20 chaves, chave ≤64 caracteres, valor ≤512 caracteres, e o total serializado ≤4096 bytes — passar do teto é 422) e devolve exatamente o que você mandou no evento message.sent (data.metadata). Omitido no corpo → omitido no evento (nunca null/{}). Como pode carregar dado pessoal (LGPD), o pedido de apagamento (compliance.erase/ POST /v1/erasure) também remove o metadata já entregue/pendente do contato apagado.Responder uma mensagem (replyToProviderId)
replyToProviderId aceita preferencialmente o msg_… do broker — internamente o broker o resolve para o provider_message_id (wamid) da citada. Para responder uma mensagem recebida, passe o mesmo messageId (msg_…) que veio no evento message.received — você não precisa de wamid. Um id de provider cru também passa (compat legada). Se a citada é desconhecida ou ainda não despachada, o envio degrada gracioso (vai sem a citação, não falha).curl -X POST https://api.grwthy.com/v1/channels/5511999999999/messages \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "5511988887777",
"content": { "kind": "text", "text": "Pode ser às 10h." },
"idempotencyKey": "pedido-1234-resposta",
"replyToProviderId": "msg_9f2c8a1b0d3e4f50"
}'msg_9f2c8a1b0d3e4f50 acima é o messageId que chegou no message.received da mensagem que você está respondendo.
Idempotência é obrigatória
UNIQUE (tenant, idempotencyKey): reenviar a mesma key devolve o resultado do primeiro envio (com idempotent: true) em vez de duplicar. Derive a key da sua operação de negócio (ex.: pedido-1234-confirmacao).Tipos de content
O content.kind discrimina a forma:
// texto
{ "kind": "text", "text": "Olá!" }
// mídia — media: image | video | audio | document
{ "kind": "media", "media": "document",
"url": "https://cdn.exemplo.com/nf-1234.pdf",
"filename": "nf-1234.pdf", "caption": "Sua nota fiscal" }
// template — oficial: nativo/aprovado; não-oficial: renderizado como texto
{ "kind": "template", "name": "confirmacao_pedido", "language": "pt_BR",
"bodyParams": ["1234", "amanhã"] }
// interativo — botões
{ "kind": "interactive", "interactive": { "type": "buttons", "body": "...", "buttons": [] } }
// interativo — lista
{ "kind": "interactive", "interactive": {
"type": "list", "body": "Escolha um horário disponível", "buttonLabel": "Ver horários",
"sections": [
{ "title": "Manhã", "rows": [
{ "id": "h9", "title": "09:00" },
{ "id": "h10", "title": "10:00", "description": "Último horário da manhã" }
] }
] } }
// reação — targetMessageId é o msg_… que veio em message.received
{ "kind": "reaction", "targetMessageId": "msg_9f2c8a1b0d3e4f50", "emoji": "👍" }Janela de 24h e templates
outside_24h_window. No oficial, o template é nativo e aprovado; no não-oficial, o broker o renderiza como texto (basta existir no workspace). O campo template aceita ainda headerText, headerMediaUrl e buttonParams.Reação: agnóstica e simétrica ao reply — só no oficial
reaction segue o mesmo contrato de replyToProviderId: targetMessageId é o msg_… que veio no evento message.received da mensagem que você quer reagir — o broker resolve o formato do provider internamente, sem que você lide com wamid. Diferente do reply (que degrada gracioso sem a citação quando o alvo é desconhecido), a reação não tem "reagir a nada": sem um alvo resolvível o envio falha com o erro terminal invalid_recipient (não é retentado). Só funciona no canal oficial — no não-oficial (WAHA) responde unsupported_capability. O nome antigo targetProviderMessageId ainda é aceito como alias deprecated (o broker o normaliza para targetMessageId); código novo deve usar só targetMessageId.Enviar em lote
O corpo do lote é { "messages": [...] } — uma lista de envios, cada item idêntico ao corpo do envio unitário acima (to, content, idempotencyKey obrigatória, opcionalmente replyToProviderId, ttlSeconds e metadata). Não existe content "comum ao lote": cada item traz o seu, então um lote pode misturar kind diferentes — texto, template e mídia no mesmo envio. O :ref aceita id ou telefone, igual ao envio unitário. Máximo de 2000 itens por requisição, num corpo de até 512KB (o teto apertado de 64KB do resto de /v1/* não se aplica a esta rota).
curl -X POST https://api.grwthy.com/v1/channels/5511999999999/messages/batch \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "to": "5511900000001", "idempotencyKey": "promo-a",
"content": { "kind": "text", "text": "Promoção só hoje!" },
"metadata": { "contrato": "CT-001" } },
{ "to": "5511900000002", "idempotencyKey": "promo-b",
"content": { "kind": "text", "text": "Promoção só hoje!" },
"metadata": { "contrato": "CT-002" } }
]
}'metadata, idempotencyKey, replyToProviderId: sempre por item
metadata mora dentro de cada item: contrato/nome/documento é dado por pessoa, nunca compartilhado entre os N envios do lote.A resposta é 202 Accepted com um resultado por item, todos com status: "accepted" — o 202 confirma só que o item entrou na fila de admissão, nunca o desfecho final do envio. Cada resultado carrega index (a posição 0-based no lote que você enviou), o idempotencyKey daquele item e o messageId (msg_…) já atribuído — os dois primeiros são os ganchos de correlação: case a resposta com a sua mensagem pela posição ou pela chave que você mesmo escolheu, sem depender só da ordem da lista.
Resposta 202
{
"results": [
{
"idempotencyKey": "promo-a",
"index": 0,
"messageId": "msg_7b3e1d9a2c4f6081",
"status": "accepted"
},
{
"idempotencyKey": "promo-b",
"index": 1,
"messageId": "msg_7b3e1d9a2c4f6081",
"status": "accepted"
}
]
}Desfecho de cada item chega pelo webhook, não nesta resposta
messageId pelo evento message.status no seu webhook (inclusive uma eventual falha de admissão, que chega como failed com o motivo em lastError) ou consultando GET /v1/messages/:id.Lote heterogêneo: text + template + media no mesmo envio
Como cada item carrega o seu próprio content, um lote não precisa ser uniforme — é só uma lista de envios independentes disparados numa única requisição. Nada impede misturar um lembrete de texto, uma confirmação de pedido por template e o envio de um comprovante em imagem no mesmo POST:
curl -X POST https://api.grwthy.com/v1/channels/5511999999999/messages/batch \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "to": "5511900000001", "idempotencyKey": "het-text",
"content": { "kind": "text", "text": "Olá!" } },
{ "to": "5511900000002", "idempotencyKey": "het-tpl",
"content": { "kind": "template", "name": "confirmacao_pedido", "language": "pt_BR",
"bodyParams": ["Ana", "1234"] } },
{ "to": "5511900000003", "idempotencyKey": "het-media",
"content": { "kind": "media", "media": "image", "url": "https://cdn.exemplo.com/nf.png" } }
]
}'A resposta alinha por índice: o resultado da posição 0 é sempre o do primeiro item do array que você mandou, e assim por diante — results[i] corresponde a messages[i].
Template com params dentro do content
Quando um item tem content.kind === "template", os parâmetros de renderização — bodyParams, headerText, headerMediaUrl e buttonParams — vão dentro do próprio content, exatamente como no envio unitário: não existe mais personalização "por fora" do content nem herança entre itens. Cada item de um lote de template é tão independente quanto um lote heterogêneo — só que, tipicamente, todos apontam para o mesmo name/language, com bodyParams diferentes (ex.: "Ana", "Bruno", "Carla" no primeiro parâmetro de cada um).
curl -X POST https://api.grwthy.com/v1/channels/5511999999999/messages/batch \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "to": "5511900000001", "idempotencyKey": "pedido-a",
"content": { "kind": "template", "name": "confirmacao_pedido", "language": "pt_BR",
"bodyParams": ["Ana", "1234"] } },
{ "to": "5511900000002", "idempotencyKey": "pedido-b",
"content": { "kind": "template", "name": "confirmacao_pedido", "language": "pt_BR",
"bodyParams": ["Bruno", "5678"] } }
]
}'Corpo estrito: campo desconhecido é rejeitado
messages) é validado estrito, com a mesma regra do envio unitário: uma propriedade fora do contrato — um bodyParams deixado fora do content, por exemplo — responde 422 em vez de ser silenciosamente ignorada. É a rede de segurança contra um parâmetro de template mal posicionado que só apareceria como mensagem entregue errada, não como erro.Validação de forma é atômica: 1 item inválido reprova o lote inteiro
content contra o canal — é checada antes de aceitar qualquer um. O primeiro item com forma inválida (ex.: content ausente, bodyParams vazio, headerText e headerMediaUrl juntos) reprova a requisição inteira com 422 e nada é enviado — nem os itens "bons" do lote. Corrija o item indicado na mensagem de erro (a posição é 1-based, a mesma do seu array) e reenvie o lote completo.Rastrear uma mensagem
GET /v1/messages/:id devolve estado e metadado — nunca o conteúdo. O telefone vem mascarado por padrão. Os estados e o último erro chegam também pelo evento message.status no seu webhook.
curl https://api.grwthy.com/v1/messages/msg_abc123 \
-H "Authorization: Bearer $WABROKER_API_KEY"Resposta 200
{
"attempts": 0,
"channelId": "ch_9f2c8a1b0d3e4f50",
"createdAt": "2026-08-04T12:00:00.000Z",
"events": [
{
"brokerTimestamp": "2026-08-04T12:00:00.000Z",
"detail": {
"code": "accepted"
},
"outcome": "applied",
"providerTimestamp": null,
"state": "sent"
}
],
"id": "msg_7b3e1d9a2c4f6081",
"lastError": null,
"state": "sent",
"toAddress": "+5511•••7777",
"traceId": "tr_2f8b6d0a4c1e3597",
"updatedAt": "2026-08-04T12:00:00.000Z"
}- Mensagem de outro tenant (ou inexistente):
404 resource_not_found, nunca403. messageIdé sempremsg_…: o broker nunca entrega formato de provider (wamid…) nos eventos nem no rastreio. Na entrada,replyToProviderIde a reaçãotargetMessageIdaceitam preferencialmente omsg_…do broker (um wamid cru também passa, só por compat legada) — nenhum dos dois exige mais formato de provider.- URLs de mídia devem ser públicas e válidas até o momento do envio.
Re-notificar (recuperar um webhook perdido)
Quando o seu endpoint de webhook fica fora do ar e perde um message.status, os dois endpoints abaixo reenfileiram o mesmo evento — nunca reprocessam a mensagem nem reenviam ao WhatsApp. O status re-notificado é sempre o estado atual da mensagem no banco, marcado redelivery: true( veja Webhooks → message.status e redelivery). Exigem a permissão messages:send — é uma ação que produz efeito, não uma leitura.
Unitário
POST /v1/messages/:id/renotify re-notifica uma mensagem já conhecida, sem corpo. Estados sem status de entrega ainda a reportar (queued, dispatching, retrying, indeterminate) respondem 422 — não há o que re-notificar. Id inexistente, ou de outro tenant, responde 404, nunca 403 (o mesmo padrão de GET /v1/messages/:id).
curl -X POST https://api.grwthy.com/v1/messages/msg_abc123/renotify \
-H "Authorization: Bearer $WABROKER_API_KEY"{ "enqueued": true, "status": "delivered" }status é o estado de entrega (sent | delivered | read | failed) que acabou de ser re-notificado.
Em lote, por filtro
POST /v1/messages/renotify re-notifica todas as mensagens que casam um filtro na querystring — os mesmos parâmetros de GET /v1/messages (from, to, state, recipient, channelId), sem corpo. Diferente da listagem, from e to são obrigatórios e limitados a uma janela de até 7 dias — o endpoint existe para reparar uma lacuna recente do seu webhook, não para varrer o histórico do tenant. Fora dessas regras (faltando from/to, ou janela acima de 7 dias), a resposta é 422.
curl -X POST "https://api.grwthy.com/v1/messages/renotify?from=2026-08-20&to=2026-08-24&state=delivered" \
-H "Authorization: Bearer $WABROKER_API_KEY"{ "enqueued": 128, "skipped": 4 }enqueuedconta as re-notificações emitidas;skippedas mensagens que casaram o filtro mas estão num estado sem status de entrega ainda (mesma regra do unitário) — o lote nunca falha por causa delas.- Backstop de 5000 mensagens: se o filtro resolver a mais do que isso, a resposta é
422("estreite o filtro") e nada é processado — nunca uma primeira leva parcial e silenciosa.
Via @wabroker/sdk: messages.renotify(messageId) e messages.renotifyBatch(query); via Wabroker.Sdk: Messages.RenotifyAsync(messageId) e Messages.RenotifyBatchAsync(query) — veja a referência de Mensagens (SDK) (/mensagens).
Baixar mídia recebida
GET /v1/media/:channelId/:mediaId devolve os bytes crus de uma mídia recebida. Autentica com a mesma apiKey de tenant (Authorization: Bearer) e é escopada por tenant — mídia de um canal de outro tenant, ou inexistente, responde 404 channel_not_found, nunca 403. Precisa dos dois parâmetros: o channelId (do envelope do evento) e o mediaId (de data.message no message.received).
curl https://api.grwthy.com/v1/media/ch_9f2c.../media-9f2c8a1b.pdf \
-H "Authorization: Bearer $WABROKER_API_KEY" \
--output nota-fiscal.pdf- A resposta são os bytes crus (binário) com o
Content-Typeoriginal eCache-Control: private, max-age=86400— não é URL assinada nem base64. Teto de 100 MB. - O
mediaIdé opaco (o nome de arquivo do provider), sem TTL gerido pelo broker: se expirou na instância,404 resource_not_found. - Só serve mídia de canais não-oficiais. Um canal oficial responde
404— para um oficial, busque a mídia diretamente na plataforma oficial.
Métricas do pipeline
GET /v1/messages/metrics devolve as contagens do pipeline de disparo do tenant — série temporal (por granularity) e totals na janela pedida (?window=). É metadado agregado, nunca conteúdo.
curl "https://api.grwthy.com/v1/messages/metrics?window=30d" \
-H "Authorization: Bearer $WABROKER_API_KEY"Resposta 200
{
"granularity": "day",
"series": [
{
"day": "2026-07-22",
"delivered": 1,
"dispatched": 2,
"enqueued": 2,
"t": "2026-08-04T12:00:00.000Z"
},
{
"day": "2026-07-23",
"delivered": 0,
"dispatched": 0,
"enqueued": 1,
"t": "2026-08-04T12:00:00.000Z"
}
],
"totals": {
"delivered": 1,
"dispatched": 2,
"enqueued": 3,
"failed": 1
},
"window": "30d"
}