Webhooks entregam a você tudo que chega das WABAs dos seus clientes: mensagens recebidas, atualizações de status e outros eventos. Você registra uma URL, a BotoZap faz POST nela e você responde 2xx para confirmar.
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. 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 e reagendada com backoff exponencial: a primeira retentativa depois de 1 minuto, dobrando até um teto de 1 hora entre tentativas, por até 6 tentativas no total. 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 quatro campos:
event: semprewhatsapp.message.received.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).contact: a identidade de quem enviou, comwa_id,user_id,username,phone,profile_nameemetadata(os campos personalizados do contato).
Os eventos de status (entregue, lido, falhou) e os ecos das mensagens que você enviou não trazem o objeto contact.
Registrar um webhook
Crie um endpoint com POST /api/v1/webhooks informando a url (obrigatoriamente https://) e a lista de events: só aceita as categorias messages (mensagens recebidas) e statuses (status de entrega/ leitura das que você enviou), não nomes finos de evento.
Registrar um endpoint de webhook
`events` só aceita as categorias `messages` (mensagens recebidas) e `statuses` (status de entrega/leitura das que você enviou) — não nomes finos de evento.
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"
]
}'Resposta 201
{
"data": {
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"url": "https://exemplo-integracao.com.br/webhooks/botozap",
"events": [
"messages",
"statuses"
],
"active": true,
"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`).
A URL precisa usar https:// e resolver para um destino público (o BotoZap bloqueia loopback/redes privadas por segurança).
O secret aparece somente na resposta 201 de criação. Guarde-o nesse momento: listagens, consultas e atualizações não devolvem essa credencial. 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.
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.
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 (success / failed / exhausted), o número de tentativas e o código de resposta HTTP do seu servidor. Use-o para depurar entregas que falharam e confirmar o que já foi processado.
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.delivered") — não confunda os dois.
