Skip to Content
GuiasWebhooks + HMAC

Webhooks

Em produção, webhooks são o jeito recomendado de receber resultados (autorização, cancelamento, cert expiring, etc). Polling funciona, mas custa rate-limit e latência.

Eventos disponíveis

EventoQuando dispara
invoice.authorizedQualquer nota (NFC-e / NF-e / NFS-e) autorizada pela SEFAZ
invoice.deniedNota denegada (cStat 110/301/302/etc)
invoice.cancelledCancelamento aprovado
invoice.failedFalha técnica (sem retorno SEFAZ ou erro de rede após retries)
issuer.certificate.expiringCert vence em 30/14/7/1 dia
issuer.certificate.expiredCert venceu (emissões falham!)

Os eventos são genéricos por tipo de nota — o campo data.modelo no payload (65 = NFC-e, 55 = NF-e, nfse = NFS-e) diferencia se você precisa.

Cadastrando

portal-zfiscoo.zek.app.br/webhooks Novo webhook:

  1. URL (HTTPS obrigatório)
  2. Eventos assinados (checkboxes)
  3. Secret (gerado automaticamente, copia 1x)

Validando a signature

Cada delivery vem com header X-Signature: sha256=<hex>. Computado como:

HMAC_SHA256(secret, body) → hex
import crypto from 'node:crypto'; function verifyZfiscooSignature(rawBody: string, signature: string, secret: string): boolean { const expected = crypto .createHmac('sha256', secret) .update(rawBody, 'utf8') .digest('hex'); const received = signature.replace(/^sha256=/, ''); // timingSafeEqual previne timing attack return crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'), ); } // Express app.post('/zfiscoo/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.header('X-Signature') ?? ''; if (!verifyZfiscooSignature(req.body.toString('utf8'), sig, process.env.ZFISCOO_WEBHOOK_SECRET!)) { return res.status(401).send('invalid signature'); } const event = JSON.parse(req.body.toString('utf8')); // handle event.type res.status(200).send('ok'); });

Payload exemplo

{ "id": "evt_01HXYZ...", "type": "invoice.authorized", "created_at": "2026-05-11T10:25:00.000Z", "account_id": "acc_...", "issuer_id": "iss_...", "data": { "id": "nfce_01HXYZ...", "external_ref": "pedido-001", "access_key": "35260111222333000181650010000000011000000017", "protocolo": "135260000000001", "danfe_url": "https://...", "xml_url": "https://...", "qrcode_url": "https://..." } }

Retry exponencial

Se sua URL devolver não-2xx (ou timeout > 10s), retry com backoff:

TentativaAtraso (a partir do erro)
1imediato
2+30s
3+2min
4+10min
5+1h
6+6h
7+24h

Após 7 falhas consecutivas, o webhook é marcado failing e cria notificação in-app no sino do dashboard. Após status 410 Gone, o webhook é desativado automaticamente.

Replay 1-clique

Em portal-zfiscoo.zek.app.br/webhooks/:id/playground (no dashboard ), aba Deliveries: toda entrega tem botão “Replay” — reenviado com o mesmo payload e signature que originalmente foi enviado.

Útil pra:

  • Debugar fix novo da sua URL
  • Testar after deploy em staging
  • Reprocessar entregas perdidas após maintenance

Idempotência no seu lado

O gateway pode reenviar a mesma entrega (retry, replay manual). Sempre use o id do evento (evt_...) pra de-duplicar no seu handler:

const seen = await redis.set(`zfiscoo:${event.id}`, '1', 'NX', 'EX', 86400); if (seen !== 'OK') return res.status(200).send('already processed'); // processa o evento