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.
Sumário
Política de entrega
- Entrega: at-least-once — seu handler deve ser idempotente.
- Method:
POSTcomContent-Type: application/json; charset=utf-8. - Timeout: 30 segundos para sua resposta.
- Retry: até 24 horas com backoff exponencial.
- Códigos:
2xxconfirma entrega;4xx(exceto408/429) marca falha definitiva sem retry;408/429/5xxou timeout disparam retry. - Assinatura: header
x-hub-signaturecomHMAC-SHA1do body bruto, formatosha1=<HEX>— ver Validação HMAC.
Após esgotar as tentativas, o documento permanece consultável via API. Reenvie manualmente com 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 body.action identifica a ação específica.
X-Hook-Event | body.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
O corpo é sempre embrulhado num envelope {"body": {...}}. Exemplo de NF-e recebida (X-Hook-Event: product_invoice_inbound, body.action: issued_successfully):
{
"body": {
"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.
Para CT-e, os campos issuer (transportadora) e taker (tomador do serviço) substituem issuer/buyer. Para eventos (body.action = event_raised_successfully ou input_event_raised_successfully), o body carrega também o eventCode e o tipo de evento associado.
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.
| 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["body"] — veja X-Hook-Event / body["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.