Cada partner tem uma única configuração de webhook (uma URL + uma lista de eventos),
não múltiplos webhooks cadastráveis. Configure via
PATCH /v1/webhooks/config.Configurando um Webhook
Quem configura o webhook
A mesmax-api-key com que você emite documentos (NF-e, NFC-e, NFS-e) basta para
consultar, configurar e testar o webhook: GET/PATCH /v1/webhooks/config e
POST /v1/webhooks/test aceitam x-api-key de integração, além do login do dashboard
(JWT). Não é preciso um token separado nem um convite específico para webhooks.
O secret HMAC (whsec_...) é gerado pela engineAPI no momento em que você configura a
webhookUrl pela primeira vez. Ele aparece completo uma única vez (padrão
Stripe/GitHub) — guarde-o assim que a resposta chegar; veja
Onde pego o secret abaixo.
Eventos disponíveis
Qualquer valor fora desta lista emevents[] é rejeitado com 400:
dfe.received também existe no enum, mas o módulo DF-e está fora do contrato público
hoje: não há endpoint público para configurar a distribuição de DF-e que dispararia
este evento. Não assine até a funcionalidade entrar na superfície pública.Estrutura do Payload
O campo étype, não event:
id é um UUID cru, sem prefixo evt_.
No invoice.rejected, o data traz status: "REJECTED" e o campo erros[]
com o retorno verbatim da SEFAZ/SEFIN (mesmo shape do 400 da emissão
síncrona); ramifique pelo codigo. Na NFS-e, o identificador em data é
nfseId (não invoiceId) e model: "NFSe":
O campo
id do evento é único e pode ser usado para garantir idempotência. Se o mesmo evento chegar duas vezes, ignore a duplicata.sefaz.status.changed: status real da SEFAZ por UF
A engineAPI monitora as 27 UFs com certificado digital próprio (não depende do seu
certificado) a cada 5 minutos. Quando uma UF muda de estado de verdade (a SEFAZ
respondeu que ficou indisponível, ou voltou a operar), o evento dispara para quem
assinou. Nunca dispara numa consulta isolada, só na transição.
Opt-in: assine
sefaz.status.changed em events[] (PATCH /v1/webhooks/config)
como qualquer outro evento, não chega automaticamente.Verificando a Autenticidade (HMAC)
A engineAPI assina cada webhook com HMAC SHA-256 usando o secret gerado pela API (prefixowhsec_). Sempre valide a assinatura antes de processar o evento.
Os headers enviados são:
Onde pego o secret
O secret HMAC (whsec_...) é gerado pela engineAPI, não escolhido por você. Ele
aparece completo uma única vez, exatamente como Stripe e GitHub fazem com chaves e
secrets: depois disso a API só devolve a versão mascarada, e o valor completo não é
recuperável — só regenerável (o que invalida o anterior e gera um novo).
Isso acontece em dois momentos:
1. No PATCH /v1/webhooks/config que cria a webhookUrl pela primeira vez
(partner ainda sem secret). A resposta traz revealedOnce como true:
A resposta de configuração sempre tem webhookUrl, webhookSecret, events e
revealedOnce (boolean). O secret só vem completo quando revealedOnce é true;
nas demais respostas ele vem mascarado.
PATCH /v1/webhooks/config seguinte (o secret já existe) devolve mascarado,
com revealedOnce como false — mesmo trocando a webhookUrl ou a lista de events:
POST /v1/webhooks/secret/regenerate, sempre que você chamar — é o único
jeito de ver o secret de novo depois da primeira vez:
webhookSecret e revealedOnce; webhookUrl e events
continuam na resposta de configuração.
Regenerar invalida o secret anterior imediatamente: atualize sua variável de ambiente
WEBHOOK_SECRET no mesmo momento em que regenerar.
GET /v1/webhooks/config nunca devolve o secret completo, só mascarado
(whsec_ab...xy).
Recebendo e Processando Eventos
Política de Retentativas
Se seu endpoint retornar qualquer status diferente de2xx ou não responder, a engineAPI
tenta novamente com o cronograma abaixo, 5 tentativas no total:
Após a 5ª tentativa falhar, o evento é movido para a Dead Letter Queue. Como o
cronograma escala até a DLQ, e como sair dela:
Dead Letter Queue (DLQ)
/admin/dead-letter-queue: é escopada por partner em /v1/webhooks/dlq.
GET /v1/webhooks/dlq só aceita limit (1-100, default 50): não pagina por page.
A resposta é { total, items[] }, não um array puro. Ver
Paginação.Histórico de Entregas
GET /v1/webhooks/logs também só aceita limit (1-100, default 50): a resposta aqui
é um array puro dos deliveries (shape diferente do /dlq acima). Ver
Paginação.Testando o webhook configurado
Debugging Local
Para testar webhooks em desenvolvimento, use o webhook.site ou o ngrok:Entrega garantida
Cada entrega e o próximo retry são persistidos. Falhas usam backoff de 5 minutos, 30 minutos, 2 horas e 24 horas; deploys ou reinícios não perdem entregas agendadas. Após cinco tentativas, a entrega vai paraGET /v1/webhooks/dlq; reagende-a por POST /v1/webhooks/dlq/{deliveryId}/retry.
Quando a reconciliação fiscal encontra o desfecho, ela emite o evento terminal normal (invoice.authorized, invoice.rejected ou invoice.canceled), sem um evento duplicado de reconciliação.
Documentos com chave e sem desfecho terminal podem ser consultados paginadamente em GET /v1/nfe?situacao=indeterminada ou GET /v1/nfce?situacao=indeterminada. situacao não pode ser combinado com status; cada item inclui situacao, accessKey, o status bruto, updatedAt, proximaVerificacaoEm e motivo.
Boas Práticas
Responda rápido, processe depois
Responda rápido, processe depois
Retorne
200 OK imediatamente e processe o evento em background (filas, workers). Nunca faça operações lentas dentro da requisição do webhook.Implemente idempotência
Implemente idempotência
Use o campo
id do evento como chave única. Se o mesmo evento chegar duas vezes (retentativa), não processe novamente.Valide a assinatura HMAC sempre
Valide a assinatura HMAC sempre
Em produção, rejeite qualquer webhook sem assinatura válida (
X-Webhook-Signature). Use timingSafeEqual/hmac.compare_digest para evitar timing attacks.Monitore a Dead Letter Queue
Monitore a Dead Letter Queue
Eventos que esgotaram as tentativas ficam em
GET /v1/webhooks/dlq. Reenvie manualmente com POST /v1/webhooks/dlq/{deliveryId}/retry (ou retry-all).Veja também
- Erros e respostas: entenda os códigos de erro da SEFAZ e como tratá-los.
- Ambiente de testes: teste seus webhooks sem emitir notas reais.