Pular para o conteúdo principal

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 no payload.type), message.sent, message.delivered, message.read, message.failed
  • instance.connected / warming / disconnected / logged_out / banned
  • qr_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.

Requer a CLI/pacote @bzapper/sdk

A 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.