Webhooks entregam a você tudo que chega das WABAs dos seus clientes: mensagens recebidas, atualizações de status, saúde da WABA e outros eventos. Você registra uma URL, a BotoZap faz POST nela e você responde 2xx para confirmar. A lista completa de eventos, com categoria, chave de idempotência e payload de cada um, está no Catálogo de eventos.
Intenção manual e saúde automática
Um endpoint tem duas dimensões independentes. active é a sua intenção manual: true significa que você quer o endpoint habilitado e false desativa novas entregas. health_status é o veredito automático da plataforma e pode ser unverified, healthy, degraded ou paused; ele não substitui nem altera active.
Todo endpoint novo nasce com health_status=unverified. Mesmo que o cadastro use active=true, a BotoZap não admite nem enfileira eventos reais antes de POST /api/v1/webhooks/{id}/testretornar 2xx. Um teste 2xx verifica a configuração e ativa o endpoint; uma falha de teste retorna 502, preserva o estado anterior e não incrementa failure_streak nem os alertas.
active | health_status | Eventos reais | Entregas |
|---|---|---|---|
false | qualquer valor | não são admitidos | as já aceitas ficam retidas |
true | unverified | não são admitidos | aguardam um teste 2xx |
true | healthy ou degraded | são admitidos | tentadas conforme o retry |
true | paused | são retidos por até 14 dias | não há POST até novo teste 2xx |
Como funciona
Quando um evento acontece, a BotoZap faz um POST no seu endpoint HTTPS com o corpo em JSON. A conexão só sai para destinos públicos (URLs de loopback, IP privado ou não-resolvível são recusadas no cadastro). Cada requisição leva três headers úteis:
X-Webhook-Event: o tipo do evento, ex.whatsapp.message.received.X-Idempotency-Key: chave estável do evento, owamidpara mensagens,wamid:statuspara atualizações de status e uma chave própria por transição nos eventos de CRM; nos eventos de conta, ela é derivada do payload normalizado. O formato de cada uma está no catálogo. Use para deduplicar reentregas.X-Webhook-Signature: a assinatura HMAC do corpo (veja abaixo).
Responda rápido com 2xx: é o único ACK que a BotoZap aceita. Fora disso (qualquer outro status, timeout ou erro de rede) a entrega é tratada como falha. A única exceção é o Web App do Google Apps Script, que responde todo POST com 302 para script.googleusercontent.com depois de já ter executado o doPost: a BotoZap aceita esse 302 como ACK, sem seguir o redirecionamento, e o registra como 200. Trate erros dentro do próprio script, porque o Google responde 302 mesmo quando ele falha. No caminho health-aware, o próximo POST segue a curva de 1 / 2 / 4 / 8 / 16 / 32 / 60 minutos (a última duração é repetida): não há teto fixo de seis tentativas. Por isso o mesmo evento pode chegar mais de uma vez: use o X-Idempotency-Key para tratar duplicatas, nunca um Set em memória, que não sobrevive a um restart ou a múltiplas réplicas (veja o receptor de exemplo abaixo). Detalhes completos em Confiabilidade e entrega.
Corpo de uma mensagem recebida
No evento whatsapp.message.received, o corpo do POST tem cinco campos:
event: semprewhatsapp.message.received.customer_id: o Cliente dono do número que recebeu o evento;nullquando a cadeia não pôde ser resolvida.phone_number_id: ophone_number_idda Meta que recebeu a mensagem.message: o objeto de mensagem cru da Cloud API (id, tipo, conteúdo econtextquando é resposta ou botão). Quando o contato chega por um anúncio Click-to-WhatsApp, a mensagem traz oreferralda Meta (source_iddo anúncio,source_url,source_type,headline,ctwa_clidetc.; octwa_clidnão vem em anúncios exibidos no Status).contact: a identidade de quem enviou, comwa_id,user_id,username,phone,profile_nameemetadata(os campos personalizados do contato).
Os eventos de status (enviado, entregue, lido, falhou) não trazem o objeto contact. Mensagens que a equipe envia pelo app WhatsApp Business num número em coexistência não chegam em messages: elas saem como whatsapp.message.echo (no mesmo formato deste corpo e com source app), e edições e remoções feitas no app como whatsapp.message.edited e whatsapp.message.revoked, só para Endpoints que assinam a categoria opt-in app_messages (veja coexistência no catálogo).
Os eventos de status trazem pricing e, quando a Meta manda, conversation. Desde a versão 24 da Graph API, conversation só aparece para mensagens enviadas dentro da janela grátis do Free Entry Point (anúncio Click-to-WhatsApp): pricing.type vem como free_entry_point, conversation.id identifica a janela e conversation.expiration_timestamp (só no sent) diz quando ela termina. O BotoZap grava esse fim em fep_expires_at da conversa (GET /api/v1/conversations).
Consentimento (categoria messages)
Cada mudança de Consentimento emite crm.contact.consent_changed. O nome começa com crm., mas a categoria de inscrição é messages — Endpoints assinados só em crm não o recebem. O corpo traz canal, Finalidade, estado, origem e evidência.
Eventos de CRM (categoria crm)
A categoria crm entrega todo evento crm.* exceto o de Consentimento: mudança de etapa do contato, crm.opportunity.created/updated, crm.demand.created/updated, crm.appointment_created e crm.appointment_updated. O payload de cada um está no catálogo; o mais usado é detalhado abaixo.
Quando um contato muda de etapa no funil — arrastado no kanban, via PATCH /api/v1/contacts/{id}, promovido automaticamente por uma resposta de transmissão ou porque a etapa foi excluída — a BotoZap emite crm.contact.stage_changed. O corpo traz:
event: semprecrm.contact.stage_changed.customer_id: o Cliente dono da conta de canal do contato.phone_number_id: ophone_number_idda Meta do número dono do contato. Só vem quando o canal do contato é o WhatsApp; nos demais canais o campo fica ausente e a origem é ochannel_account.contact: a identidade do contato (id,wa_id,phone,profile_name…).previous_stage/new_stage: a etapa anterior e a nova, cada uma como{ id, key, label }. Ausentes quando o contato entra no funil (sem anterior) ou sai dele (sem nova).changed_by:manual(kanban/API),auto(transmissão/resposta) ousystem(etapa limpa ou excluída).
Use este evento para reagir ao momento certo da negociação — ex.: contato entrou em "Fechou" → crie o agendamento, dispare a cobrança, acione seu agente. O mesmo evento sai no stream durável de GET /api/v1/events e na subscription MCP.
Saúde da WABA e conexão (categoria account)
A categoria account encaminha as mudanças de saúde que a Meta envia para a WABA e avisa quando um número termina de conectar. Ela é opt-in: endpoints que assinam apenas messages, statuses ou crm não recebem esses eventos. O payload é normalizado a partir dos campos que a BotoZap persiste para acompanhar a saúde; valores ausentes permanecem como null.
Atualização da conta
O evento whatsapp.account.updated corresponde ao field da Meta account_update:
{
"event": "whatsapp.account.updated",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"waba_id": "4207187232868022",
"account_event": "ACCOUNT_RESTRICTION",
"ban_state": null,
"restrictions": [
{
"restriction_type": "RESTRICTED_BIZ_INITIATED_MESSAGING",
"expiration": "2026-08-31T00:00:00.000Z"
}
],
"occurred_at": "2026-08-27T12:00:00.000Z"
}account_eventeban_staterefletem, respectivamente, o evento e o estado de banimento conhecidos.customer_ididentifica o Cliente dono da WABA; em eventos sem número, a resolução usa owaba_idda conexão.restrictionsé a lista persistida de restrições, comrestriction_typeeexpiration. Uma lista vazia significa que a Meta informou não haver restrições.occurred_até o horário do evento na Meta; pode sernullquando o payload não trouxer um horário legível.violation_typesó aparece quando a Meta reporta uma violação de política (account_eventACCOUNT_VIOLATION), por exemploSPAM. Em geral o banimento (DISABLED_UPDATE) chega logo em seguida como um evento separado.
Exemplo de violação de política:
{
"event": "whatsapp.account.updated",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"waba_id": "4207187232868022",
"account_event": "ACCOUNT_VIOLATION",
"ban_state": null,
"restrictions": null,
"violation_type": "SPAM",
"occurred_at": "2026-09-03T16:21:30.000Z"
}Atualização de limite do número
O evento whatsapp.phone_number.quality_updated corresponde ao field phone_number_quality_update. Apesar do nome legado do field, o payload atual informa o evento de limite e o tier de conversas; ele não é uma leitura de quality_rating. Ao receber esse field, o BotoZap relê os números da conexão na Graph e atualiza a qualidade e o status no painel na hora, sem esperar a sincronização diária.
{
"event": "whatsapp.phone_number.quality_updated",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"waba_id": "4207187232868022",
"phone_number_id": "1279498075235551",
"display_phone_number": "+1 555-078-3881",
"messaging_limit_tier": "TIER_2K",
"occurred_at": "2026-08-27T12:00:00.000Z"
}phone_number_idedisplay_phone_numberidentificam o número persistido na conexão da WABA.customer_ididentifica o Cliente dono do número.messaging_limit_tieré o tier persistido para o número.- Sem número ou tier utilizável, a BotoZap não cria esse evento;
occurred_atpode sernullquando ausente ou ilegível.
Número conectado
O evento whatsapp.phone_number.connected sai quando o Embedded Signup termina e o número fica conectado, pelo Link de conexão ou pelo painel, em conexão dedicada ou coexistência. É o jeito de saber que o cliente concluiu a conexão sem consultar GET /api/v1/customers/{id}/setup_links em loop.
{
"event": "whatsapp.phone_number.connected",
"customer_id": "4f5f0b1e-3ca6-4fd2-9c2f-3f3a5f90d2a1",
"environment": "live",
"waba_id": "4207187232868022",
"phone_number_id": "1279498075235551",
"display_phone_number": "+55 11 3333-4444",
"connection_mode": "coexistence",
"setup_link_id": "5b2f7c1e-8a4d-4e3f-9b6a-1c2d3e4f5a60",
"occurred_at": "2026-09-30T13:05:00.000Z"
}connection_modeédedicated(número só na API) oucoexistence(o número segue no app WhatsApp Business).setup_link_idé o Link de conexão usado. A chave de idempotência éwhatsapp.phone_number.connected:<setup_link_id>:<phone_number_id>: uma nova conexão do mesmo número por outro link gera outro evento; repetir o mesmo link, não.- Vale o filtro por Cliente do Endpoint e o evento também entra em
GET /api/v1/events. - Conexão concluída sem número (a Meta ainda não informou nenhum) não gera o evento.
- Não há evento de desconexão. Quando a Meta informa que o acesso acabou, chega
whatsapp.account.updatedcom oaccount_eventda Meta (por exemploPARTNER_APP_UNINSTALLED).
Registrar um webhook
Crie um endpoint com POST /api/v1/webhooks informando a url (obrigatoriamente https://) e a lista de events: aceita as categorias messages (mensagens recebidas), statuses (status de entrega/ leitura das que você enviou e término de Régua), crm (etapa do contato, oportunidades, demandas e agendamentos), account (saúde da WABA) e app_messages (o que a equipe envia, edita ou apaga pelo app em coexistência), não nomes finos de evento. Valores fora dessa lista são descartados; sem nenhum válido, a API responde 422 invalid_events. Endpoints existentes continuam recebendo só o que assinaram — crm, account e app_messages são opt-in; app_messages não chega nem a um Endpoint com events vazio. O campo opcional customer_id limita o endpoint aos eventos de um Cliente; omitido ou null, preserva o recebimento da Conta inteira.
Registrar um endpoint de webhook
Cria o endpoint com `health_status=unverified`: mesmo com `active=true`, eventos reais só são admitidos depois de um teste assinado com resposta 2xx.
curl https://botozap.com.br/api/v1/webhooks \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://exemplo-integracao.com.br/webhooks/botozap",
"events": [
"messages",
"statuses"
],
"customer_id": null
}'Resposta 201
{
"data": {
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"customer_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"url": "https://exemplo-integracao.com.br/webhooks/botozap",
"events": [
"messages",
"statuses"
],
"active": true,
"has_authorization": false,
"health_status": "unverified",
"failure_streak": 0,
"verified_at": null,
"last_success_at": null,
"last_failure_at": null,
"last_response_code": null,
"next_attempt_at": null,
"paused_at": null,
"secret": "whsec_...",
"created_at": "2026-07-14T12:00:00.000Z",
"updated_at": "2026-07-14T12:00:00.000Z"
}
}O `secret` (`whsec_...`) só aparece nesta resposta de criação — guarde-o agora, ele assina cada entrega (`X-Webhook-Signature`).
`active` expressa sua intenção manual; `health_status` começa como `unverified`. Execute `POST /api/v1/webhooks/{id}/test` e receba 2xx para verificar e ativar o endpoint.
`customer_id` é opcional: omitido ou `null`, o endpoint recebe eventos de todos os Clientes da Conta; informado, recebe apenas eventos daquele Cliente. O id precisa pertencer à Conta da chave.
A URL precisa usar https:// e resolver para um destino público (o BotoZap bloqueia loopback/redes privadas por segurança).
`events` aceita `messages`, `statuses`, `crm`, `account` e `app_messages`. `app_messages` (o que a equipe envia, edita ou apaga pelo app em coexistência: `whatsapp.message.echo`, `.edited`, `.revoked`) é opt-in: só chega se estiver na lista.
O secret é opcional na criação: omitido, a BotoZap gera um whsec_…; informado, precisa ter de 16 a 256 caracteres (sem quebras de linha), senão a API responde 422 invalid_secret. Ele aparece somente na resposta 201 de criação. Guarde-o nesse momento: listagens, consultas e atualizações não devolvem essa credencial (para trocar, veja Rotacionar o secret). O endpoint também aceita GET/PATCH/DELETE /api/v1/webhooks/{id} para consultar, atualizar e remover, e POST /api/v1/webhooks/{id}/test para disparar um evento de teste.
Verificar, ativar e corrigir
Execute o teste assinado depois de criar o endpoint e novamente sempre que corrigir o destino. Qualquer resposta HTTP entre 200 e 299 verifica a configuração, zera o streak e ativa o endpoint. Resposta não-2xx, timeout ou erro de conexão retorna 502 test_delivery_failed e não penaliza a saúde automática.
Testar e ativar um endpoint
Envia `webhook.test` assinado. Qualquer resposta HTTP 2xx verifica a configuração, zera o streak e ativa o endpoint para eventos reais.
curl https://botozap.com.br/api/v1/webhooks/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11/test \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 200
{
"data": {
"success": true,
"response_code": 204,
"health_status": "healthy",
"active": true
}
}Se o destino responder fora de 2xx, ou houver timeout/erro de rede, a API retorna 502 `test_delivery_failed`; a falha manual não incrementa `failure_streak` nem cruza alertas.
Depois de corrigir URL ou Authorization, use este mesmo endpoint para verificar e reativar; `PATCH { "active": true }` sozinho retorna `verification_required`.
Alterar url ou headers.Authorization invalida a verificação e coloca health_status=unverified; execute o teste de novo. Alterar somente events não reinicia a saúde. PATCH com active=false é a desativação manual. Para reativar, PATCH { "active": true } sozinho retorna verification_required: a reativação passa pelo teste 2xx.
Campos de leitura
GET /api/v1/webhooks, GET /api/v1/webhooks/{id} e a resposta de PATCH retornam somente o estado sanitizado abaixo. O valor de Authorization nunca é devolvido; a resposta de criação 201 é a única que inclui secret — nem um PATCH que troca o secret o devolve.
| Campo | Significado |
|---|---|
customer_id | Cliente filtrado pelo endpoint; null significa todos os Clientes da Conta. |
health_status | Veredito automático: unverified, healthy, degraded ou paused. |
failure_streak | Falhas consecutivas de deliveries reais penalizadas. |
verified_at | Momento do último teste assinado 2xx da configuração atual. |
last_success_at | Momento do último POST 2xx observado. |
last_failure_at | Momento da última falha observada. |
last_response_code | Último código HTTP observado; null quando não houve resposta. |
next_attempt_at | Próximo horário de tentativa automática, quando houver. |
paused_at | Momento em que o circuito abriu em paused; não significa active=false. |
Consultar um endpoint de webhook
Lê a configuração e o estado sanitizado de saúde; o segredo HMAC nunca volta em uma leitura.
curl https://botozap.com.br/api/v1/webhooks/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11 \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 200
{
"data": {
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"customer_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"url": "https://exemplo-integracao.com.br/webhooks/botozap",
"events": [
"messages",
"statuses"
],
"active": true,
"has_authorization": false,
"health_status": "healthy",
"failure_streak": 0,
"verified_at": "2026-07-14T12:00:00.000Z",
"last_success_at": "2026-07-14T12:00:00.000Z",
"last_failure_at": "2026-07-14T12:00:00.000Z",
"last_response_code": 204,
"next_attempt_at": "2026-07-14T12:00:00.000Z",
"paused_at": null,
"created_at": "2026-07-14T12:00:00.000Z",
"updated_at": "2026-07-14T12:00:00.000Z"
}
}As leituras expõem apenas `health_status`, `failure_streak`, `verified_at`, `last_success_at`, `last_failure_at`, `last_response_code`, `next_attempt_at` e `paused_at`; `secret` aparece somente no 201 de criação.
Degradação automática
Depois de verificado, cada falha de delivery real (resposta fora de 2xx, timeout, erro de rede ou bloqueio local de configuração) incrementa ofailure_streak e mantém o endpoint elegível para retry. A BotoZap alerta o owner ao cruzar os streaks 5 / 10 / 15 e abre a pausa automática no streak 15. A pausa não grava active=false: ela muda a saúde para paused, retém o backlog por 14 diase não faz novos POSTs até um teste 2xx. Corrija o endpoint e use Testar e ativar para retomar a fila não expirada.
Validar a assinatura e deduplicar
Antes de confiar em qualquer entrega, valide o header X-Webhook-Signature: HMAC-SHA256 em hexadecimal, calculado sobre os bytes crus do corpo da requisição com o secret do seu endpoint. Não há prefixo sha256=: o header é só o hex. Compare com crypto.timingSafeEqual (constant-time). Reserializar o JSON antes de comparar muda os bytes e quebra a assinatura.
Rotacionar o secret
Envie PATCH /api/v1/webhooks/{id} com { "secret": "<novo>" } (no SDK, boto.webhooks.update(id, { secret }); na CLI, botozap webhooks update <id> --secret <novo>). A validação é a mesma da criação — 16 a 256 caracteres, sem quebras de linha, 422 invalid_secret caso contrário — e o valor não volta na resposta nem em GET. Gere-o você mesmo, por exemplo com whsec_ + 32 bytes aleatórios em base64url; no painel de Webhooks, Regenerar segredo faz a mesma troca com um secret gerado pela BotoZap.
A troca é imediata e não há janela com duas assinaturas: cada entrega é assinada com o secret vigente no momento da tentativa, inclusive as pendentes e as que estão em retry. Uma tentativa que já estava em voo no instante da troca ainda chega com o secret anterior. Por isso, faça o receptor aceitar os dois secrets, rotacione e só então remova o antigo. A rotação não muda url nem Authorization, então não reinicia a verificação; se o receptor recusar entregas nesse meio tempo, elas seguem o retry normal e contam no failure_streak. Reenviar o mesmo PATCH é seguro.
O receptor abaixo cobre os quatro pontos onde devs mais erram: capturar o corpo cru, validar a assinatura, deduplicar de forma persistente por X-Idempotency-Key (um Set em memória não serve para produção) e só responder 2xx depois de persistir o efeito.
Receptor de webhook (Node.js)
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const WEBHOOK_SECRET = process.env.BOTOZAP_WEBHOOK_SECRET!; // whsec_... (tela Webhooks)
/**
* Verifica a assinatura HMAC-SHA256 do BotoZap (header X-Webhook-Signature).
* IMPORTANTE: calcule o HMAC sobre os BYTES CRUS do corpo — nunca sobre
* JSON.stringify(JSON.parse(rawBody)), que pode reordenar chaves ou mudar
* espaçamento e invalidar a assinatura.
*/
function verifyBotozapSignature(
rawBody: Buffer,
signatureHeader: string | undefined,
secret: string,
): boolean {
if (!signatureHeader) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signatureHeader, "utf8");
const b = Buffer.from(expected, "utf8");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
/**
* Processe o efeito E registre a X-Idempotency-Key NA MESMA TRANSAÇÃO. Um Set
* em memória não serve, e inserir só a chave antes do efeito também é
* incorreto: se o processo cair no meio, o retry parecerá uma duplicata e o
* efeito será perdido. O padrão relacional é:
*
* BEGIN;
* INSERT INTO processed_webhooks (idempotency_key) VALUES ($1)
* ON CONFLICT (idempotency_key) DO NOTHING RETURNING idempotency_key;
* -- se inseriu: aplique aqui as escritas do evento, na MESMA transação
* COMMIT;
*
* Em conflito, retorne "duplicate" sem repetir o efeito. Em crash antes do
* COMMIT, banco reverte chave+efeito juntos e o retry pode processar de novo.
*/
async function processEventOnce(
idempotencyKey: string,
event: unknown,
): Promise<"processed" | "duplicate"> {
throw new Error(
"implemente uma transação única para idempotencyKey + escritas do evento",
);
}
const server = createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", async () => {
const rawBody = Buffer.concat(chunks); // bytes CRUS — nunca reserializar
const signatureHeader = req.headers["x-webhook-signature"];
const signature = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
if (!verifyBotozapSignature(rawBody, signature, WEBHOOK_SECRET)) {
res.writeHead(401).end();
return;
}
const idempotencyHeader = req.headers["x-idempotency-key"];
const idempotencyKey = Array.isArray(idempotencyHeader) ? idempotencyHeader[0] : idempotencyHeader;
if (!idempotencyKey) {
res.writeHead(400).end();
return;
}
const event = JSON.parse(rawBody.toString("utf8"));
// Chave + efeito são commitados juntos. Só depois responda: 2xx é o único
// ACK; fora de 2xx OU timeout = nova tentativa automática.
await processEventOnce(idempotencyKey, event);
res.writeHead(200).end();
});
});
// Ajuste a porta ao seu ambiente antes de subir o processo:
// server.listen(3001);
Auditar entregas
O histórico de entregas fica em GET /api/v1/webhook_deliveries, com o tipo do evento, o status (pending / success / failed / exhausted / limited), o número de tentativas e o código de resposta HTTP do seu servidor. Filtre por endpoint_id (ou o alias webhook_id), status (valor fora da lista responde 422 invalid_status) e event_type, que recebe o nome fino do evento (ex.: whatsapp.message.failed ou webhook.test), não a categoria. No caminho health-aware, retries continuam após a sétima tentativa com intervalos de 60 minutos;exhausted é reservado a um terminal auditável (por exemplo, expiração do hold), não a um teto de seis tentativas. Uma linha em hold pode expirar após 14 dias com razão terminal auditável. Use esta listagem para depurar entregas que falharam e confirmar o que já foi processado.
Limite de repasse no plano Free
No plano Free, até 300 mensagens recebidas por mês (mês UTC) são repassadas aos seus webhooks. Só contam whatsapp.message.received e instagram.message.received de números reais; o mesmo evento entregue a vários endpoints conta uma vez. Status, CRM, eventos da conta e app_messages não contam, e o Sandbox fica fora. O recebimento no BotoZap nunca para: a mensagem aparece no Inbox e na API normalmente. Nos planos pagos e no teste de 14 dias não há esse limite. Contas Free criadas até 30 de setembro de 2026 passam a ter o limite em 1º de novembro de 2026; as criadas a partir de 1º de outubro já nascem com ele.
Acima do limite, a entrega fica com status limited: não é enviada, não é retentada e não é reenviada sozinha quando você assina. Depois de assinar, reenvie pela tela de Operação. Filtre com status=limited para ver o que ficou retido. Um aviso por e-mail chega em 80% (240) e em 100% (300), e o medidor aparece na tela Webhooks do painel.
Auditar entregas de webhook
Lista as tentativas de entrega dos seus webhooks — útil para depurar por que um evento não chegou.
curl https://botozap.com.br/api/v1/webhook_deliveries?limit=2 \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 200
{
"data": [],
"paging": {
"cursors": {
"before": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa",
"after": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa"
},
"next": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa",
"previous": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa"
}
}`events` (no `POST /api/v1/webhooks` de criação do endpoint) são CATEGORIAS de assinatura ("messages"/"statuses"/…); `event_type` aqui (na auditoria de entregas) é o nome FINO do evento entregue (ex.: "whatsapp.message.received", "whatsapp.message.echo" — mensagem que a equipe enviou pelo app em coexistência, categoria opt-in "app_messages" —, "whatsapp.message.delivered") — não confunda os dois.
Quer ligar uma Entrega diretamente ao seu runtime? Veja o exemplo executável em Endpoint para agentes, com PostgreSQL, dedupe e worker sem polling.
