Webhooks
Verificação HMAC, retry com backoff exponencial, DLQ, replay manual e boas práticas.
Eventos são entregues via POST ao endpoint que você cadastrou em Integrações → Webhooks, assinados com HMAC-SHA256 e o whsec_* do endpoint.
Headers
x-zhex-signature: a3f2d1e8b...
x-zhex-event: payment_intent.succeeded
x-zhex-attempt: 1
content-type: application/jsonx-zhex-signature— HMAC-SHA256 dorawBodyusandowhsec_*como chave.x-zhex-event— tipo do evento (mesmo valor debody.type).x-zhex-attempt— qual tentativa de entrega é essa (1 = primeira).
Verificar assinatura (Node)
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(payload: Buffer, header: string | undefined, secret: string) {
if (!header) throw new Error('Missing x-zhex-signature');
const expected = createHmac('sha256', secret)
.update(payload.toString('utf8'))
.digest('hex');
const a = Buffer.from(header, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('Signature mismatch');
}
return JSON.parse(payload.toString('utf8'));
}
app.post('/webhooks/zhex', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = verify(
req.body,
req.header('x-zhex-signature'),
process.env.ZHEX_WEBHOOK_SECRET!,
);
} catch (err) {
return res.status(400).send(`Invalid signature: ${(err as Error).message}`);
}
switch (event.type) {
case 'payment_intent.succeeded':
handleSuccess(event.data.object);
break;
case 'charge.dispute.created':
handleDispute(event.data.object);
break;
}
res.json({ received: true });
});Usar timingSafeEqual é crítico — === em string vaza tempo e permite ataque de oráculo. Raw body (express.raw) também é obrigatório — express.json() muda os bytes e quebra a assinatura.
Retry e DLQ
A Zhex faz retry automático até 7 tentativas com backoff exponencial:
| Tentativa | Atraso | Acumulado |
|---|---|---|
| 1 | imediata | 0s |
| 2 | 5s | 5s |
| 3 | 30s | 35s |
| 4 | 5min | ~6min |
| 5 | 1h | ~1h |
| 6 | 6h | ~7h |
| 7 | 24h | ~31h |
Após a 7ª, vai para DLQ. Replay manual no dashboard: Integrações → Webhooks → endpoint → Events → Reenviar.
O que conta como sucesso
- HTTP 2xx em < 5s.
- Resposta vazia ou JSON é OK.
O que dispara retry
- HTTP 4xx (exceto 410 Gone) ou 5xx
- Timeout (> 5s)
- Connection error / DNS fail
- TLS error
410 Gone (parar retry)
Retorne 410 Gone quando o evento é de recurso deletado ou fora de escopo:
case 'customer.deleted':
if (!await db.customers.findById(event.data.object.id)) {
return res.status(410).send('Customer no longer tracked');
}A Zhex marca o evento como aborted e não tenta de novo.
Idempotência de evento
Mesmo event.id pode chegar duas vezes (race em retry com sucesso parcial). Use event.id como dedup key:
async function processEvent(event) {
const exists = await db.processed_events.findById(event.id);
if (exists) return; // já tratado, skip
await db.processed_events.create({ id: event.id, type: event.type });
switch (event.type) { /* ... */ }
}Tabela processed_events deve ter (id PRIMARY KEY, processed_at, type). Garbage collect a cada 90 dias.
Endpoints separados por ambiente
Cadastre um endpoint para test, outro para live com URLs diferentes:
https://meusite.com/webhooks/zhex/test ← whsec_test_*
https://meusite.com/webhooks/zhex/live ← whsec_live_*Cada um com seu whsec_* próprio. Misturar é vetor de bug — um evento de test mode disparando lógica de produção.
Local development
Pra dev local, use ngrok ou cloudflared pra expor o localhost e cadastre um endpoint apontando pro tunnel:
# 1. Tunnel
ngrok http 3000
# → https://abc123.ngrok-free.app- Cadastra no dashboard em test mode: Integrações → Webhooks → Adicionar → URL =
https://abc123.ngrok-free.app/webhooks/zhex→ secret é mostrado uma vez, exporte comoZHEX_WEBHOOK_SECRET.
Quando terminar a sessão, desative ou delete o webhook pra não ficar recebendo eventos órfãos.
Boas práticas
- Responda 2xx em < 5s. Trabalho pesado vai para fila (BullMQ, SQS). Senão a Zhex considera timeout.
- Verifique a assinatura ANTES de qualquer outra coisa. Não logue body, não parseie JSON, não toque banco.
- Use
event.idcomo dedup key numa tabelaprocessed_events. - Endpoint dedicado. Não compartilhe path com webhook de outras integrações.
- Monitore taxa de retry. Spike em retry = seu endpoint está lento ou caindo.
Atualizado em