docs

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/json
  • x-zhex-signature — HMAC-SHA256 do rawBody usando whsec_* como chave.
  • x-zhex-event — tipo do evento (mesmo valor de body.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:

TentativaAtrasoAcumulado
1imediata0s
25s5s
330s35s
45min~6min
51h~1h
66h~7h
724h~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
  1. Cadastra no dashboard em test mode: Integrações → Webhooks → Adicionar → URL = https://abc123.ngrok-free.app/webhooks/zhex → secret é mostrado uma vez, exporte como ZHEX_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.id como dedup key numa tabela processed_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.
Esta página foi útil?

Atualizado em

Nesta página