Webhook events + validação HMAC
A NFE.io envia HTTP POST ao endpoint configurado em webhookUrl da empresa sempre que um documento NF-e ou CT-e novo (ou evento associado) é capturado do Ambiente Nacional da SEFAZ. Este documento cataloga os eventos, o shape do payload, a política de entrega e os mecanismos de segurança.
Os eventos de NF-e e CT-e recebidos acompanham o calendário oficial da Reforma Tributária e as atualizações do Ambiente Nacional da SEFAZ. Esta página reflete o que está em vigor na data em last_update — novos eventos são habilitados de forma alinhada aos marcos regulatórios.
Sumário
Política de entrega
- Entrega: at-least-once — seu handler deve ser idempotente (deduplique por
X-Hook-Id). - Method:
POSTcomContent-Type: application/json; charset=utf-8. - Retry e timeout: seguem a política única de entrega da plataforma — veja Catálogo de eventos — Política de entrega. Valores atuais: até 16 tentativas ao longo de cerca de 45 horas; timeout de 10 s nas três primeiras tentativas e de 100 s a partir da quarta. São parâmetros de operação e podem ser ajustados — responda
2xxrápido e dimensione com margem. - Códigos:
2xx(e410) confirma a entrega;400,401,403,404,405e422são recusa definitiva, sem reentrega; qualquer outro código, erro de rede ou timeout gera reentrega. - Assinatura: header
x-hub-signaturecomHMAC-SHA1do body bruto, formatosha1=<HEX>— ver Validação HMAC.
Esgotadas as tentativas, o evento deixa de ser reenviado automaticamente; o documento continua consultável via API. Para reenviar uma NF-e: POST /v2/companies/{companyId}/inbound/productinvoices/{access_key}/processwebhook.
Eventos
Quando um documento é recebido, fazemos um POST para sua URL. O tipo de evento chega em dois lugares: o header X-Hook-Event identifica a área (NF-e ou CT-e) e o campo action, na raiz do corpo, identifica a ação específica.
X-Hook-Event | action | Quando ocorre |
|---|---|---|
product_invoice_inbound | issued_successfully | outbound_successfully | NF-e recebida (ou emitida pela própria conta, se você optou por recebê-las) |
product_invoice_inbound | input_event_raised_successfully | Evento de manifestação do destinatário (Ciência, Confirmação, Desconhecimento, Operação não Realizada) |
product_invoice_inbound | event_raised_successfully | Outro evento de NF-e (cancelamento, CC-e, EPEC, eventos NT 2025.002) |
product_invoice_inbound_summary | mesmos valores acima | Variante resumida (sem XML completo) do mesmo evento |
transportation_invoice_inbound | issued_successfully | outbound_successfully | CT-e recebido (ou emitido pela própria conta) |
transportation_invoice_inbound | event_raised_successfully | Evento de CT-e |
Formato do Payload
Os campos vêm achatados na raiz do corpo — sem envelope {"payload": {...}} nem {"body": {...}}. Exemplo de NF-e recebida (X-Hook-Event: product_invoice_inbound, action: issued_successfully):
{
"action": "issued_successfully",
"accountId": "5f9a1b2c3d4e5f6a7b8c9d0e",
"accessKey": "35240112345678000195550010000012341234567890",
"createdOn": "2024-03-15T14:22:10Z",
"parentAccessKey": "",
"company": {
"id": "comp_123",
"federalTaxNumber": "98765432000100"
},
"issuer": {
"federalTaxNumber": "12345678000195",
"name": "Fornecedor LTDA"
},
"buyer": {
"federalTaxNumber": "98765432000100",
"name": "Minha Empresa S.A."
},
"links": {
"xml": "https://api.nfe.io/v2/companies/comp_123/inbound/nfe/35240112345678000195550010000012341234567890/xml",
"pdf": "https://api.nfe.io/v2/companies/comp_123/inbound/nfe/35240112345678000195550010000012341234567890/pdf"
},
"blobUrl": "comp_123/2024/03/35240112345678000195550010000012341234567890.xml",
"type": "productInvoice",
"nsu": "21825",
"nsuParent": "",
"nfeNumber": "1234",
"nfeSerialNumber": "1",
"issuedOn": "2024-03-15T10:00:00Z",
"description": "Autorizado o uso da NF-e",
"totalInvoiceAmount": "1500.00",
"operationType": "Incoming",
"environmentType": 1,
"direction": "Received"
}
Note que totalInvoiceAmount é string, e o identificador da empresa vem em company (objeto), não em um campo solto companyId.
Outros pontos do payload de NF-e:
nsuensuParentsão strings, não números.direction:Received(nota recebida de terceiro) ouIssued(nota emitida pela própria empresa que retornou na distribuição).blobUrlé uma referência interna de armazenamento. Não dependa dele; para baixar arquivos uselinks.xml/links.pdfou as rotas/xmle/pdf.issuedOn: na NF-e completa (productInvoice) é a data de emissão (dhEmi). Nos resumos (product_invoice_inbound_summary,typeproductInvoiceSummaryouproductInvoiceEventSummary) é a data de recebimento na SEFAZ (dhRecbto) ou do evento (dhEvento) — por desenho, porque o resumo não trazdhEmi. ComwebhookVersion3, o valor sai no offset do XML (ex.:-03:00); um defeito que adiantava oissuedOndos resumos em 3h foi corrigido. ComwebhookVersion2 ou anterior, o formato segue+00:00, sem mudança.
O resultado de uma manifestação enviada por você (POST .../manifestation-events ou o legado POST /manifest) não é notificado por webhook: consulte o evento por polling em GET .../productinvoices/manifestation-events/{id}. Veja Manifestar NF-e.
Para CT-e, os campos issuer (transportadora) e taker (tomador do serviço) substituem issuer/buyer. Para eventos (action = event_raised_successfully ou input_event_raised_successfully), o corpo carrega também o eventCode e o tipo de evento associado.
Se você cadastrou propriedades personalizadas na assinatura do webhook, elas chegam num objeto properties na raiz do corpo. Sem propriedades cadastradas, o campo não aparece.
Fluxo end-to-end
Validação HMAC
A NFE.io assina cada POST com HMAC-SHA1 sobre o body bruto. Validar a assinatura é obrigatório — sem isso, qualquer um que descubra a URL do seu endpoint pode forjar requisições.
Todos os webhooks entregues pelo serviço central de webhooks da NFE.io são assinados pelo mesmo mecanismo, independente do produto de origem — emissão de NF-e, NFC-e e NFS-e, e captura de documentos recebidos. As especificações desta seção valem para qualquer webhook não legado.
| Item | Valor |
|---|---|
| Header | x-hub-signature (lowercase) |
| Algoritmo | HMAC-SHA1 |
| Encoding | hex MAIÚSCULO, sem separadores |
| Formato | sha1=<HEX> (prefixo obrigatório, 45 chars total) |
| Conteúdo assinado | Body bruto UTF-8, exatamente como recebido |
| Secret | String ASCII de 32 a 64 caracteres, configurada na subscrição |
Vetor de teste
Secret: SuperSecretWebhookKey12345678901
Body: {"event":"test","id":"abc123","amount":42.00}
→ Header: sha1=502BC91DE70F6802FC16CD2E599A9AF752064FE5
Se sua implementação não gera exatamente esse hash com esses inputs, ela tem um bug — corrija antes de testar contra webhook real.
Exemplo (Python/Flask)
import hmac, hashlib
from flask import request, abort
WEBHOOK_SECRET = b"<seu-segredo-32-a-64-chars>"
@app.post("/webhook/nfeio")
def receive():
sig = request.headers.get("x-hub-signature", "")
if not sig.startswith("sha1="):
abort(401)
expected = "sha1=" + hmac.new(
WEBHOOK_SECRET, request.get_data(cache=True), hashlib.sha1
).hexdigest().upper()
if not hmac.compare_digest(expected, sig):
abort(401)
# processar request.json — veja X-Hook-Event / request.json["action"]
return "", 200
Use comparacão timing-safe (hmac.compare_digest em Python, crypto.timingSafeEqual em Node, CryptographicOperations.FixedTimeEquals em C#) — comparação byte-a-byte vaza informação por análise de tempo.
Documentação completa
Catálogo canônico (PT/EN/ES, 13 seções, 5 implementações de referência, troubleshooting, rotação de secret): https://github.com/nfe/shared-webhook-api/blob/main/docs/webhook-signature-validation.md
x-hub-signature (HMAC) autentica — prova que o webhook veio da NFE.io e não foi adulterado.
Content-MD5 apenas detecta corrupção em trânsito — qualquer atacante pode calcular o MD5 do payload forjado dele. MD5 ≠ autenticação.
Idempotência
Webhooks podem chegar mais de uma vez após retry. Seu handler deve deduplicar usando (companyId, accessKey) ou (companyId, nsu) como chave única.
Boas práticas adicionais:
- Responda rápido: retorne
2xxem até 5 segundos e processe assincronamente (fila interna). - Persista o payload antes de processar — não perca dados em crash do consumer.
- Baixe XML/PDF cedo — URLs assinadas em
links.xmlexpiram em 1 hora. - Monitore HTTP 5xx no seu endpoint — detecte problemas antes que o orçamento de retry esgote.