Catálogo de eventos do webhook — NFS-e Inbound
A NFE.io envia HTTP POST ao endpoint configurado em webhookUrl da empresa sempre que algo relevante acontece no subsistema NFS-e Inbound. Este documento cataloga os 6 eventos disponíveis, o shape do envelope, a política de retry e os mecanismos de segurança e idempotência.
Para desenvolvedores cliente que vão implementar o handler do webhook. Para visão sistêmica do fluxo, leia Arquitetura primeiro.
Sumário
Política de entrega
- Entrega: at-least-once — seu handler deve ser idempotente (ver seção dedicada).
- Method:
POSTcomContent-Type: application/json; charset=utf-8. - Timeout: 30 segundos para sua resposta.
- Retry: até 50 tentativas em 24 horas com backoff exponencial (30s → 60s → 120s → ... → max 7200s) e jitter de ±20%.
- Códigos:
2xxconfirma entrega;4xx(exceto408/429) marcaDefinitivelyFailedsem 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/nfse/{id}/resend-webhook.
Eventos
Todos os payloads são embrulhados num envelope { "body": { ... } }. Os exemplos abaixo mostram apenas o conteúdo de body para clareza.
Evento (body.eventName) | Quando dispara | NSU novo? |
|---|---|---|
inbound.serviceInvoice.received | NFS-e, DPS, EventRegistrationRequest, CNC ou documento desconhecido capturado do ADN | Sim |
inbound.serviceInvoice.event.received | Evento de NFS-e (cancelamento, ciência, rejeição, ato de ofício) capturado do ADN | Sim |
inbound.manifestation.submitted | Resultado da sua submissão de Ciência/Rejeição (callback assíncrono) | Não |
inbound.company.deactivated | Empresa desativada automaticamente (circuit breaker) | Não |
inbound.document.failed | Falha no processamento de um documento (XML corrompido, parse error) | Não |
inbound.company.event | Eventos operacionais (rate limit, reativação, consolidação) | Não |
Os campos body.action (legacy snake_case issued_successfully / event_raised_successfully) continuam sendo enviados para retrocompatibilidade. Novas integrações devem filtrar por body.eventName (dotted). O type emite serviceInvoice para documentos e serviceInvoiceEvent para eventos (esse vocabulário não varia por versão do payload — veja Versionamento de webhook para o que de fato muda entre v1 e v2).
inbound.serviceInvoice.received
Disparado quando um documento NFS-e, DPS, EventRegistrationRequest, CNC ou unknown é capturado do ADN. O type (na raiz do envelope) discrimina a variante.
{
"body": {
"eventName": "inbound.serviceInvoice.received",
"action": "issued_successfully",
"type": "serviceInvoice",
"companyId": "54244e0ee340420fdc94ad10",
"accountId": "acc-123",
"document": {
"id": "66b2c3d4e5f6a7890bcdef12",
"direction": "Received",
"nsu": 12345,
"accessKey": "35250612345678000190000001234567890123451234",
"generatedOn": "2026-05-13T14:42:15Z",
"issuedOn": "2026-05-13",
"provider": { "federalTaxNumber": "12345678000190", "name": "Fornecedor LTDA", "cityCode": "3550308", "state": "SP" },
"borrower": { "federalTaxNumber": "98765432000110", "name": "Sua Empresa LTDA", "cityCode": "3106705", "state": "MG" },
"servicesAmount": 1500.00,
"federalServiceCode": "010501",
"cityServiceCode": null,
"amounts": { "deductionsAmount": 0, "amountNet": 1500.00 },
"taxes": { "issqn": { "base": 1500.00, "rate": 2.00, "amount": 30.00 } },
"environment": "Production",
"xmlUrl": "https://api.nfse.io/v2/.../inbound/nfse/66b2.../xml",
"pdfUrl": "https://api.nfse.io/v2/.../inbound/nfse/66b2.../pdf"
}
}
}
Idempotência: (body.companyId, body.document.nsu) ou body.document.id.
inbound.serviceInvoice.event.received
Disparado quando um evento de NFS-e é capturado (cancelamento, ciência, rejeição, ato de ofício). Carrega type = "serviceInvoiceEvent" (na raiz do envelope) e os campos eventCode (tpEvento raw) e eventType (legível, pode vir null para códigos novos).
{
"body": {
"eventName": "inbound.serviceInvoice.event.received",
"action": "event_raised_successfully",
"type": "serviceInvoiceEvent",
"companyId": "54244e0ee340420fdc94ad10",
"document": {
"id": "69f3f9f0731bd9db773ba624",
"eventType": "Cancellation",
"eventCode": "101101",
"nsu": 12346,
"accessKey": "35250612345678000190...",
"generatedOn": "2026-05-13T15:00:00Z",
"environment": "Production",
"xmlUrl": "https://api.nfse.io/v2/.../inbound/nfse/69f3.../xml",
"pdfUrl": null
}
}
}
A tabela completa de eventCode está em Códigos de evento — atenção aos dois contextos distintos (XSD tpEvento vs CSV EventCode do bulk export).
Campos fiscais sempre null em eventos: provider, borrower, servicesAmount, amounts, taxes, pdfUrl. Para obter esses dados, consulte a NFS-e hospedeira via document.accessKey.
inbound.manifestation.submitted
Callback do fluxo assíncrono de manifestação. Disparado quando o evento de Ciência (203202) ou Rejeição (203206) que você submeteu via API atinge estado terminal no SEFIN Nacional.
{
"body": {
"eventName": "inbound.manifestation.submitted",
"companyId": "54244e0ee340420fdc94ad10",
"manifestation": {
"id": "69e193685b7241b5c4fe8e3c",
"accessKey": "35503081223301943000745000000002734526036434454892",
"eventCode": 203202,
"reasonCode": null,
"status": "Accepted",
"submittedAt": "2026-04-17T02:47:08Z",
"acceptedAt": "2026-04-17T02:47:11Z",
"errorCode": null,
"errorMessage": null
}
}
}
Status terminais: Accepted, Rejected, Failed. Quando Rejected/Failed, errorCode traz o cStat SEFIN (135 OK, 217 NFS-e não consta, 573 duplicidade etc.) e errorMessage o xMotivo.
inbound.company.deactivated
Disparado quando uma empresa é desativada automaticamente pelo circuit breaker da NFE.io. Ação imediata recomendada — sem reativação, nenhum documento novo será capturado.
{
"body": {
"eventName": "inbound.company.deactivated",
"companyId": "54244e0ee340420fdc94ad10",
"company": {
"id": "54244e0ee340420fdc94ad10",
"name": "Empresa Teste LTDA",
"isActive": false,
"deactivationReason": "CertificateExpired",
"deactivatedAt": "2026-04-05T10:00:00Z"
}
}
}
deactivationReason possíveis: CertificateNotFound, CertificateExpired, CertificateRejected, CompanyNotAuthorized. Veja Troubleshooting para causa e ação recomendada de cada um.
inbound.document.failed
Disparado quando o processamento de um documento falha de forma irrecuperável (XML corrompido na origem, root element desconhecido, GZip inválido).
{
"body": {
"eventName": "inbound.document.failed",
"companyId": "54244e0ee340420fdc94ad10",
"document": {
"id": "6621a3f2e340420fdc000002",
"type": "None",
"nsu": 43,
"accessKey": null,
"generatedOn": "2026-04-05T10:01:00Z",
"environment": "Production",
"xmlUrl": null,
"pdfUrl": null
}
}
}
O XML não fica armazenado para documentos com falha. Casos recorrentes para o mesmo companyId indicam problema sistêmico — abra chamado no suporte com o companyId e nsu.
inbound.company.event
Notificações operacionais de baixa prioridade (rate limit ativado, reativação automática, consolidação de lote). Use para monitoramento — não exige ação na maioria dos casos.
{
"body": {
"eventName": "inbound.company.event",
"companyId": "54244e0ee340420fdc94ad10",
"eventType": "RateLimited",
"occurredAt": "2026-04-17T04:00:00Z",
"description": "ADN retornou 429 com Retry-After=3600; captura pausada até 2026-04-17T05:00:00Z."
}
}
Schema do objeto document
Para type=serviceInvoice (NFS-e autorizada), os blocos amounts, taxes e taxes.ibsCbs vêm populados:
| Bloco | Campos principais | Observações |
|---|---|---|
document.amounts | discountUnconditionedAmount, discountConditionedAmount, deductionsAmount, municipalBenefit{type,amount}, reimbursement, amountNet | Componentes monetários não-tributários. Só discountUnconditionedAmount e deductionsAmount reduzem a BC do ISSQN. |
document.taxes.issqn | situationCode, retentionType, base, rate, amount | ISSQN municipal. retentionType=1 prestador recolhe, 2 retido pelo tomador, 3 retido pelo intermediário. |
document.taxes.federal | irAmountWithheld, inssAmountWithheld, pisDueAmount, cofinsDueAmount, socialContributions{retentionType,withheldAmount} | NT007/2026: pisDueAmount/cofinsDueAmount são DEVIDOS (não retidos). Retenções consolidadas em socialContributions.withheldAmount. |
document.taxes.ibsCbs | situationCode, classCode, basis, ibs.{state,municipal,totalAmount}, cbs.{rate,effectiveRate,amount} | Reforma Tributária do Consumo. IBS repartido UF+Município. Vem null em NFS-e pré-RTC. |
Garantia de shape: campos top-level do document aparecem sempre, mesmo quando null (parser strongly-typed seguro). Esta garantia não se estende a sub-campos — valide o pai antes de acessar (if (doc.amounts) { doc.amounts.amountNet }).
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/nfse")
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"]
return "", 200
Use comparaçã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
A NFE.io mantém o catálogo canônico de validação HMAC em nfe/shared-webhook-api/docs/webhook-signature-validation.md (PT/EN/ES, 13 seções). Lá você encontra:
- 5 implementações de referência completas (Node.js, Python, PHP, C#, Go)
- Passo-a-passo de validação com comparação em tempo constante
- Troubleshooting (10 causas reais de falha em produção)
- Política de rotação do secret com janela de transição
- Headers adicionais que acompanham o webhook (
Content-MD5,X-Hook-Id,X-Hook-Attempts,X-Hook-Event)
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.
Compatibilidade legada
Integrações em Version = raw-payload ou v1 recebem também o header X-NFEIO-Signature com formato sha1=<base64> (HMAC-SHA1 em base64, mantido para retrocompatibilidade). Novas integrações devem usar exclusivamente x-hub-signature.
Idempotência
Webhooks podem chegar mais de uma vez após retry. Seu handler deve deduplicar:
- Para documentos (
serviceInvoice.received,serviceInvoice.event.received,document.failed): use o par(body.companyId, body.document.nsu)oubody.document.idcomo chave única no seu banco. - Para manifestações (
manifestation.submitted): usebody.manifestation.id. - Para eventos de empresa (
company.deactivated,company.event): use(body.companyId, body.occurredAt)ou aceite duplicação benigna (apenas re-aplique o estado).
Não use nsu puro como chave — o NSU é único por empresa, não globalmente. Sempre combine com companyId.
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 no
xmlUrl/pdfUrlexpiram em 30 minutos. - Monitore HTTP 5xx no seu endpoint — detecte problemas antes que o orçamento de retry esgote.
Veja também
- Arquitetura do NFS-e Inbound — fluxo SEFIN → ADN → webhook + pontos de falha
- Integração via REST — como ativar a empresa e configurar
webhookUrl - Receita — Manifestação do tomador — submeter Ciência/Rejeição e tratar o callback
manifestation.submitted - Troubleshooting —
deactivationReason,cStatSEFIN,webhookStatus=DefinitivelyFailed - Códigos de evento — XSD
tpEventovs CSVEventCode - Mapa cross-repos —
nfe/shared-webhook-apicomo fonte canônica do HMAC