Webhooks

Receba eventos de mensagem e status por HTTP: a BotoZap faz POST no seu endpoint e assina cada corpo para você validar a origem.

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, o wamid para mensagens, wamid:status para 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: sempre whatsapp.message.received.
  • phone_number_id: o phone_number_id da Meta que recebeu a mensagem.
  • message: o objeto de mensagem cru da Cloud API (id, tipo, conteúdo e context quando é resposta ou botão).
  • contact: a identidade de quem enviou, com wa_id, user_id, username, phone, profile_name e metadata (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.