Skip to main content
Webhooks são chamadas HTTP que a engineAPI faz para sua URL quando um evento acontece (nota autorizada, rejeitada, certificado vencendo). Não é preciso fazer polling.
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

Não há campo secret no PATCH /webhooks/config: o secret HMAC é gerado pela API (ver Onde pego o secret abaixo), nunca fornecido por você.
Consultar a configuração atual:

Quem configura o webhook

A mesma x-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.
Configurar e rotacionar o secret exige chave ek_live_ (ou o login do dashboard). Uma chave ek_test_ recebe 403 AMBIENTE_DE_TESTE_SEM_ESCRITA_WEBHOOK em PATCH /v1/webhooks/config, POST /v1/webhooks/secret/regenerate e no retry/purge da DLQ — o secret é único por parceiro, não por ambiente, e uma integração de homologação rotacionando o secret invalidaria a assinatura da sua produção. Leitura (GET /config, GET /logs, GET /dlq) e POST /test continuam abertos pra ek_test_.
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 em events[] é rejeitado com 400:
invoice.correction e nfse.authorized não existem neste enum: CC-e de NF-e não dispara webhook próprio hoje, e NFS-e autorizada entra em invoice.authorized (com model: "NFSe" no data). Não assine eventos fora desta tabela.
Os eventos cte.* e mdfe.* existem no enum, mas CT-e e MDF-e estão fora da superfície pública da engineAPI hoje (roadmap, sem previsão de data; ver CT-e e MDF-e). Na prática, esses eventos são inalcançáveis: não há endpoint público para emitir um CT-e ou MDF-e que os dispare.
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:
amount é string decimal ("119.8"), nunca number cru: evita imprecisão de ponto flutuante em valor monetário. Não trate como número sem converter explicitamente no seu código. A resposta HTTP de POST /v1/nfe usa o mesmo formato.
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.
Falha nossa ao consultar (rede, timeout, certificado de monitoramento vencido) nunca vira status: "down": isso seria inventar uma indisponibilidade da SEFAZ que não aconteceu. Sem resposta real confirmada, nenhum evento é disparado.
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 (prefixo whsec_). Sempre valide a assinatura antes de processar o evento. Os headers enviados são:
Use exatamente o nome de header da tabela acima.
Nunca ignore a validação HMAC em produção. Qualquer endpoint sem verificação pode ser chamado por agentes maliciosos, injetando eventos falsos no seu sistema.

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.
Qualquer PATCH /v1/webhooks/config seguinte (o secret já existe) devolve mascarado, com revealedOnce como false — mesmo trocando a webhookUrl ou a lista de events:
2. No POST /v1/webhooks/secret/regenerate, sempre que você chamar — é o único jeito de ver o secret de novo depois da primeira vez:
Essa rota devolve somente 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 de 2xx 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)

A DLQ não vive em /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 para GET /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

Retorne 200 OK imediatamente e processe o evento em background (filas, workers). Nunca faça operações lentas dentro da requisição do webhook.
Use o campo id do evento como chave única. Se o mesmo evento chegar duas vezes (retentativa), não processe novamente.
Em produção, rejeite qualquer webhook sem assinatura válida (X-Webhook-Signature). Use timingSafeEqual/hmac.compare_digest para evitar timing attacks.
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