BeeZapdocs
EntrarPainel →

Webhooks

Quando algo acontece num número WhatsApp do cliente (mensagem chega, ack de leitura, sessão muda de estado), o BeeZap dispara um POST assinado pra URL configurada em /app/config.

Formato do payload

Todo webhook tem o mesmo envelope:

json
{
  "event": "MESSAGE_RECEIVED",
  "deliveryId": "cmod123abc...",
  "tenantId": "ckxx...",
  "attempt": 1,
  "payload": { /* dados do evento — varia por tipo */ }
}
  • event: tipo do evento (lista abaixo).
  • deliveryId: ID único dessa entrega. Use pra deduplicar caso a gente reenvie por timeout.
  • tenantId: ID interno do cliente.
  • attempt: número da tentativa (1 = primeira, até 6).
  • payload: dados crus da engine WhatsApp, varia por evento.

Headers

HeaderDescrição
content-typeapplication/json
x-beezap-eventMesmo valor de event no body — útil pra rotear sem parsear JSON.
x-beezap-delivery-idIgual ao deliveryId no body.
x-beezap-signaturet=<unix_ts>, v1=<hex> — assinatura HMAC SHA-256.

Verificação HMAC

Pra confirmar que o webhook veio mesmo do BeeZap (e não de um atacante), recompute o HMAC no seu lado e compare. Algoritmo:

  • Leia o body cru (string, sem reparsear).
  • Extraia t e v1 do header x-beezap-signature.
  • Confira que |now - t| < 300s (proteção contra replay).
  • Compute HMAC_SHA256(secret, "<t>.<rawBody>") em hex.
  • Compare com v1 usando comparação tempo-constante.
O secret é diferente da API key
O secret de webhook (whs_...) é separado da API key (bz_...). Pegue ele em /app/config ao regenerar o webhook secret. Ele aparece uma vez — guarde.

Verifier em Node.js

js
import crypto from "node:crypto";

export function verifyBeeZap(secret, rawBody, signatureHeader) {
  if (!signatureHeader) return false;
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=").map((s) => s.trim())),
  );
  const ts = Number(parts.t);
  const received = parts.v1;
  if (!ts || !received) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(received, "hex");
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

// Express handler exemplo
app.post("/webhooks/beezap", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyBeeZap(
    process.env.BEEZAP_WEBHOOK_SECRET,
    req.body.toString("utf8"),
    req.headers["x-beezap-signature"],
  );
  if (!ok) return res.status(401).end();
  const evt = JSON.parse(req.body.toString("utf8"));
  // ... processar evt.event ...
  res.json({ ok: true });
});

Verifier em PHP

php
<?php
function verifyBeeZap(string $secret, string $rawBody, ?string $sigHeader): bool {
    if (!$sigHeader) return false;
    $parts = [];
    foreach (explode(',', $sigHeader) as $kv) {
        [$k, $v] = array_map('trim', explode('=', $kv, 2));
        $parts[$k] = $v;
    }
    if (!isset($parts['t'], $parts['v1'])) return false;
    $ts = (int) $parts['t'];
    if (abs(time() - $ts) > 300) return false;
    $expected = hash_hmac('sha256', "$ts.$rawBody", $secret);
    return hash_equals($expected, $parts['v1']);
}

// Handler
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_BEEZAP_SIGNATURE'] ?? null;
if (!verifyBeeZap(getenv('BEEZAP_WEBHOOK_SECRET'), $raw, $sig)) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true);
// ... processar $event['event'] ...
echo json_encode(['ok' => true]);

Eventos disponíveis

EventoQuando dispara
MESSAGE_RECEIVEDMensagem chegou de um contato pra você (inbound).
MESSAGE_SENTWhatsApp confirmou envio do servidor (ack=1).
MESSAGE_DELIVEREDMensagem foi entregue ao celular do destinatário (ack=3).
MESSAGE_READDestinatário leu (ack=4, só com confirmação de leitura ativada).
MESSAGE_FAILEDEnvio falhou definitivamente (ack=-1).
SESSION_CONNECTEDNúmero WhatsApp ficou online (após scan de QR).
SESSION_DISCONNECTEDNúmero desconectou (perda de conexão, logout).

Exemplo MESSAGE_RECEIVED

json
{
  "event": "MESSAGE_RECEIVED",
  "deliveryId": "cmod123abc",
  "tenantId": "ckxx...",
  "attempt": 1,
  "payload": {
    "id": { "_serialized": "false_5511999999999@c.us_3EB0..." },
    "from": "5511999999999@c.us",
    "fromMe": false,
    "body": "Oi, tudo bem?",
    "timestamp": 1714233721,
    "notifyName": "Maria",
    "hasMedia": false
  }
}

Exemplo MESSAGE_DELIVERED

json
{
  "event": "MESSAGE_DELIVERED",
  "deliveryId": "cmod456def",
  "tenantId": "ckxx...",
  "attempt": 1,
  "payload": {
    "id": "true_5511999999999@c.us_3EB0XYZ",
    "ack": 3
  }
}

Retry e idempotência

O BeeZap considera entregue qualquer resposta 2xx. Outras respostas disparam retry com backoff exponencial:

TentativaDelay aproximado
1imediato
230s
32min
410min
530min
62h
após 6 falhas, descartado

Como a 1ª tentativa é fire-and-forget e o retry usa um cron, sua URL pode receber o mesmo evento mais de uma vez. Use deliveryId pra deduplicar:

js
if (await db.processedDeliveries.exists(evt.deliveryId)) {
  return res.json({ ok: true, dedup: true });
}
await db.processedDeliveries.insert(evt.deliveryId);
// ... processar ...

Boas práticas

  • Responda 200 rápido (<5s) — empurre o trabalho pra fila se for pesado.
  • Sempre verifique HMAC antes de fazer qualquer coisa.
  • Loga o deliveryId e o attempt pra debug.
  • HTTPS obrigatório. URLs http:// são aceitas mas o tráfego vai vazado.
  • Acompanhe falhas em /app/webhooks — entregas com 6 tentativas estão visíveis lá.