Webhooks (com carinho)
Toda atividade vira um webhook assinado. Use sempre o code na lógica; o
message é só para humanos (traduzido pelo idioma).
Envelope
{
"event_id": "evt_...",
"event_type": "message.received",
"timestamp": "2026-06-23T14:14:21Z",
"instance_id": "...",
"client_reference": "lead-42",
"group": { "jid": "[email protected]", "name": "Atendimento" },
"sender": { "jid": "[email protected]", "lid": "...@lid", "name": "Fulano" },
"mentions": ["[email protected]"],
"payload": { "type": "text", "body": "olá", "wa_message_id": "..." }
}
Catálogo de eventos
message.received(com subtipos nopayload.type),message.sent,message.delivered,message.read,message.failedinstance.connected/warming/disconnected/logged_out/bannedqr_code,pairing_code
Eventos de conta (sem instance_id — entregues a todos os webhooks da conta):
usage.threshold— atingiu 80/90/100% de um limite de uso/gasto do período (payload:percent,used,included,plan)wallet.low— carteira baixa enquanto envia em excedente (payload:balance_cents)billing.past_due— uma cobrança (recarga/auto-recarga) falhou (payload:invoice_id)billing.recovered— pagamento regularizado
Assinatura HMAC-SHA256 (sobre o corpo CRU)
O header X-Bzapper-Signature: sha256=<hex> é o HMAC-SHA256 do corpo cru com
o secret do seu webhook. Valide antes de dar parse no JSON.
import hmac, hashlib
def valid(secret: bytes, raw_body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
import crypto from 'node:crypto';
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
Idempotência
Reentregas acontecem (retry com backoff até 5x quando seu endpoint falha).
Deduplique por event_id: aplique o efeito uma única vez por id.
Regra: no máximo 1 webhook por evento
Dentro de um projeto, cada tipo de evento só pode ter um webhook. Ao criar ou
atualizar um webhook cujos event_types colidem com outro já existente, a API
responde 409 com o código event_taken (a mensagem diz qual evento
conflitou). Um webhook com event_types vazio escuta todos os eventos — e por
isso conflita com qualquer outro do projeto. Para reassinar um evento, remova
ou edite o webhook que já o escuta.
Testar local (relay estilo Stripe)
A CLI do bZapper traz um relay de webhooks para o seu localhost, igual ao
stripe listen — sem expor nenhuma URL pública:
# reenvia os eventos do projeto para o seu app local (assinados)
bzapper listen --forward-to http://localhost:3000/webhooks/bzapper \
--api-key bz_live_...
# dispara um evento de teste (ex.: message.received)
bzapper trigger message.received --api-key bz_live_...
O listen abre um stream SSE com a API, imprime um signing secret local e,
para cada evento, faz POST no seu --forward-to assinando o corpo com
X-Bzapper-Signature (HMAC-SHA256) — exatamente como um webhook real. Valide
a assinatura com esse secret local. Também recebe X-Bzapper-Event-Id e
X-Bzapper-Event-Type.
@bzapper/sdkA CLI vem no pacote @bzapper/sdk (binário bzapper). Esse pacote é hoje
privado ("private": true) — ainda não publicado no npm. Enquanto não
publicarmos, rode o binário a partir do pacote do repositório (pnpm --filter @bzapper/sdk exec bzapper listen ...); quando publicado, npx @bzapper/sdk listen ... funcionará direto. Variáveis: BZAPPER_API_KEY e BZAPPER_API_URL
(padrão http://localhost:8080).
Alternativa sem CLI: suba um receptor, registre o webhook apontando para ele,
chame POST /webhooks/{id}/test (ou POST /webhooks/trigger) e confira a
assinatura sobre o corpo cru recebido.