Seções

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étodopatho que faz
POST/v1/channels/:ref/messagesenvia uma mensagem
POST/v1/channels/:ref/messages/batchenvia em lote (lista de envios independentes)
GET/v1/messages/:idrastreia uma mensagem
GET/v1/messages/metricscontagens do pipeline de disparo
POST/v1/messages/:id/renotifyre-notifica o status atual de uma mensagem (unitário)
POST/v1/messages/renotifyre-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.

bash
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

json
{
  "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)

Apesar do nome, 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).
bash
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

O broker garante 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:

json
// 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

Fora da janela de 24h desde a última mensagem do contato, um número oficial só entrega templates aprovados — texto livre falha com 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

A 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).

bash
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

Não existe mais um envelope "comum ao lote" — cada item é um envio completo e independente. 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

json
{
  "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

O 202 não distingue mais item bom de item ruim: a validação de forma (abaixo) continua reprovando o lote inteiro antes de aceitar qualquer item, mas depois de aceito o processamento é assíncrono — fila, cota, canal e compliance são resolvidos por um worker depois da resposta. Acompanhe o resultado real de cada 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:

bash
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).

bash
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

O corpo de envio (envelope do lote e cada item de 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

Diferente da admissão em si (assíncrona, resolvida depois do 202 — ver o callout acima), a forma de cada item — parse do envio e do seu 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.

bash
curl https://api.grwthy.com/v1/messages/msg_abc123 \
  -H "Authorization: Bearer $WABROKER_API_KEY"

Resposta 200

json
{
  "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, nunca 403.
  • messageId é sempre msg_…: o broker nunca entrega formato de provider (wamid…) nos eventos nem no rastreio. Na entrada, replyToProviderId e a reação targetMessageId aceitam preferencialmente o msg_… 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).

bash
curl -X POST https://api.grwthy.com/v1/messages/msg_abc123/renotify \
  -H "Authorization: Bearer $WABROKER_API_KEY"
json
{ "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.

bash
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"
json
{ "enqueued": 128, "skipped": 4 }
  • enqueued conta as re-notificações emitidas; skipped as 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).

bash
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-Type original e Cache-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.

bash
curl "https://api.grwthy.com/v1/messages/metrics?window=30d" \
  -H "Authorization: Bearer $WABROKER_API_KEY"

Resposta 200

json
{
  "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"
}