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
| Evento | Quando dispara |
|---|---|
invoice.authorized | Qualquer nota (NFC-e / NF-e / NFS-e) autorizada pela SEFAZ |
invoice.denied | Nota denegada (cStat 110/301/302/etc) |
invoice.cancelled | Cancelamento aprovado |
invoice.failed | Falha técnica (sem retorno SEFAZ ou erro de rede após retries) |
issuer.certificate.expiring | Cert vence em 30/14/7/1 dia |
issuer.certificate.expired | Cert venceu (emissões falham!) |
Os eventos são genéricos por tipo de nota — o campo
data.modelono payload (65= NFC-e,55= NF-e,nfse= NFS-e) diferencia se você precisa.
Cadastrando
Via dashboard
portal-zfiscoo.zek.app.br/webhooks → Novo webhook:
- URL (HTTPS obrigatório)
- Eventos assinados (checkboxes)
- Secret (gerado automaticamente, copia 1x)
Validando a signature
Cada delivery vem com header X-Signature: sha256=<hex>. Computado como:
HMAC_SHA256(secret, body) → hexNode.js
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:
| Tentativa | Atraso (a partir do erro) |
|---|---|
| 1 | imediato |
| 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