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
| Header | Descrição |
|---|---|
| content-type | application/json |
| x-beezap-event | Mesmo valor de event no body — útil pra rotear sem parsear JSON. |
| x-beezap-delivery-id | Igual ao deliveryId no body. |
| x-beezap-signature | t=<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
tev1do headerx-beezap-signature. - Confira que
|now - t| < 300s(proteção contra replay). - Compute
HMAC_SHA256(secret, "<t>.<rawBody>")em hex. - Compare com
v1usando 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
| Evento | Quando dispara |
|---|---|
| MESSAGE_RECEIVED | Mensagem chegou de um contato pra você (inbound). |
| MESSAGE_SENT | WhatsApp confirmou envio do servidor (ack=1). |
| MESSAGE_DELIVERED | Mensagem foi entregue ao celular do destinatário (ack=3). |
| MESSAGE_READ | Destinatário leu (ack=4, só com confirmação de leitura ativada). |
| MESSAGE_FAILED | Envio falhou definitivamente (ack=-1). |
| SESSION_CONNECTED | Número WhatsApp ficou online (após scan de QR). |
| SESSION_DISCONNECTED | Nú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:
| Tentativa | Delay aproximado |
|---|---|
| 1 | imediato |
| 2 | 30s |
| 3 | 2min |
| 4 | 10min |
| 5 | 30min |
| 6 | 2h |
| — | 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
200rápido (<5s) — empurre o trabalho pra fila se for pesado. - Sempre verifique HMAC antes de fazer qualquer coisa.
- Loga o
deliveryIde oattemptpra 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á.