Pular para o conteúdo principal

Documentação Técnica — Webhook NF-e/CT-e Inbound (Devs de Clientes)

Público-alvo: Desenvolvedores de sistemas que integram com a NFE.io para receber automaticamente NF-e e CT-e capturadas via DistribuicaoDFe (SEFAZ Ambiente Nacional). Objetivo: Referência técnica completa para integrar via webhook: payload, campos, tipos, eventos, validação de assinatura, boas práticas. Atualizado em: 2026-05-22

📑 Este documento cobre apenas o webhook do NF-e/CT-e Recebidas. Para a referência REST (consulta, download, manifestação) consulte 02-doc-tecnica-clientes-nfe-cte-dev-pt.md. Para o webhook do NFS-e Recebidas, consulte 02-doc-tecnica-clientes-dev-nfse-inbound-webhook.md. Para a visão geral, abra README.md.

Índice


1. Visão geral

O serviço Inbound da NFE.io captura automaticamente NF-e e CT-e da SEFAZ via DFe (Distribuição de DF-e) e entrega ao seu sistema via webhook HTTP POST.

Fluxo resumido:

SEFAZ (DFe) → NFE.io Inbound Worker → Webhook POST → Seu sistema

Você não precisa consultar a SEFAZ — o NFE.io faz isso automaticamente em intervalos regulares por empresa ativa.

Famílias de evento entregues por este serviço:

FamíliaeventTypeeventActionQuando ocorre
NF-e completaproduct_invoice_inboundissued_successfullyNova NF-e autorizada recebida via DistribuicaoDFe, XML completo
Evento de NF-eproduct_invoice_inboundevent_raised_successfullyEvento emitido pela própria origem da NF-e (cancelamento, EPEC, CC-e, registro de passagem, etc.)
Manifestação do destinatárioproduct_invoice_inboundinput_event_raised_successfullyManifestação registrada pelo destinatário (confirmação, ciência, desconhecimento, operação não realizada)
Resumo de NF-eproduct_invoice_inbound_summaryissued_successfullyResumo de NF-e (metadados apenas — XML completo não disponível até manifestação ou via consulta on-demand)
Resumo de evento de NF-eproduct_invoice_inbound_summaryevent_raised_successfullyResumo de evento — metadados do evento sem XML completo
CT-e completotransportation_invoice_inboundissued_successfullyNovo CT-e autorizado recebido via DistribuicaoDFe
Evento de CT-etransportation_invoice_inboundevent_raised_successfullyEvento de CT-e (cancelamento, prestação em desacordo, etc.)

Resumo vs documento completo (NF-e apenas):

A SEFAZ entrega via DFe resumos (NFeSummary...) com poucos metadados (empresa, chave, NSU, descrição) ou documentos completos (NFeMetadata, NFeEventMetadata) com participantes, valores e links detalhados. O serviço NFE.io repassa cada um como webhook distinto (*_inbound vs *_inbound_summary). Quando você recebe um resumo e quer o documento completo, chame GET .../inbound/{accessKey}/xml.


2. Webhook — Contrato técnico

Método e headers

POST <sua-url-configurada>
Content-Type: application/json; charset=utf-8

Resposta esperada do seu endpoint

HTTP StatusComportamento NFE.io
2xx (200, 201, 204)Entrega confirmada — não reenvia
4xx (exceto 408, 429)Marca falha definitiva — não reenvia
408 (timeout)Falha temporária — reenvia com backoff
429 (rate limit)Falha temporária — reenvia com backoff
5xxFalha temporária — reenvia com backoff
Timeout (> 30s)Falha temporária — reenvia

Política de retry

  • Até 50 tentativas em uma janela de 24 horas.
  • Backoff exponencial: 30s → 60s → 120s → ... → max 7200s (2h).
  • Jitter de ±20% em cada delay.
  • Após exaurir: documento permanece disponível via API (GET /v2/companies/{companyId}/inbound/productinvoices/{accessKey} ou .../transportationinvoices/{accessKey}) e pode ser reprocessado manualmente (ver §9).

3. Envelope de entrega

O payload entregue ao seu endpoint vem dentro de um envelope { "body": { ... } } adicionado pela camada de entrega de webhooks da NFE.io (events-api). É o mesmo wrapper usado pelo webhook NFS-e Inbound (ver 02-doc-tecnica-clientes-dev-nfse-inbound-webhook.md §3) — os campos do objeto enviado ao transporte são espalhados dentro de body, e body.action reflete o eventAction da rota.

Estrutura completa do envelope final entregue:

{
"body": {
"action": "issued_successfully",
"accountId": "acc-123",
"type": "productInvoice",
"accessKey": "...",
"company": { /* ... */ },
"issuer": { /* ... */ },
"buyer": { /* ... */ }
/* demais campos do payload — ver §4 e §5 */
}
}
  • body.action — corresponde ao eventAction da rota interna (issued_successfully, event_raised_successfully, input_event_raised_successfully). Use-o para diferenciar ação.
  • body.type — campo do próprio payload (ver Type em §4/§5). Valores: productInvoice, productInvoiceEvent, productInvoiceSummary, productInvoiceEventSummary, transportationInvoice, transportationInvoiceEvent. Use-o para diferenciar variante.
  • Demais campos (accessKey, company, issuer, etc.) — espalhados diretamente em body (não há sub-objeto data ou document aninhado para NF-e/CT-e — diferente do NFS-e, cujo handler embrulha em body.document).

Roteamento por eventType/eventAction: o filtro de webhook registrado no painel nfe.io usa o par (eventType, eventAction) para decidir entregar a notificação. Os 3 eventType (product_invoice_inbound, product_invoice_inbound_summary, transportation_invoice_inbound) aparecem na URL interna da events-api (v2/events/{eventType}/{eventAction}) e definem qual webhook do painel será disparado, mas não são necessariamente propagados dentro de body. Para discriminar o payload recebido, use (body.action, body.type).

Discriminação no consumer: use o par (body.action, body.type) — a matriz completa está na §12 ("Troubleshooting").

Nota de validação: o wrapper acima reflete o padrão da events-api documentado no webhook NFS-e Inbound (mesma camada de entrega). Recomenda-se validar o shape contra uma entrega real de homologação antes de codar contratos rígidos de parser.

Os exemplos JSON nas seções 4 e 5 abaixo mostram apenas o conteúdo top-level do payload (sem o wrapper { "body": { ... } }) para clareza. O wrapper é sempre o mesmo.

Serialização: campos null são omitidos por padrão

⚠️ Mudança em relação a versões anteriores deste documento. O serializador JSON da API omite campos com valor null — está configurado globalmente com JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull (Program.cs). Consequências práticas:

  1. Não confie em chave presente como indicador. Todo campo opcional documentado nas tabelas abaixo pode estar ausente quando o valor real seria null. Acesse com checagem defensiva (obj?.field, obj.get("field"), etc.), nunca assuma que a chave existe.
  2. Objetos com todas as propriedades internas null aparecem como {}. Por exemplo, em um evento NF-e em que a NF-e pai ainda não foi indexada, transportation pode chegar como {} (objeto vazio) em vez de { "federalTaxNumber": null, "name": null }. O container existe porque foi instanciado em código; as chaves internas foram omitidas pelo serializador.
  3. Containers que nem foram instanciados ficam totalmente ausentes. Em resumos (§4.4.1, §4.4.2) e em CT-es que não declaram um participante opcional, o objeto pai não aparece na resposta — não vem como null nem como {}.
  4. Os exemplos das seções 4 e 5 refletem payloads reais observados em produção (anonimizados). Use-os como contrato de referência; as tabelas listam quais campos podem aparecer, não garantem presença.

⚠️ Diferença para a doc do webhook NFS-e: o handler do NFS-e usa um JsonSerializerOptions próprio que serializa null explicitamente. Não extrapole o comportamento de um webhook para o outro.

Estabilidade contra novos campos

A API pode adicionar novos campos opcionais ao payload sem aviso prévio (mudança não-breaking). Seu parser deve ignorar chaves desconhecidas em vez de falhar. Configurações recomendadas:

  • TypeScript: evite exactOptionalPropertyTypes para o shape do webhook; trate todos os campos como string | null | undefined.
  • C#: configure JsonSerializer com JsonSerializerOptions { UnknownTypeHandling = ... } permissivo, ou use JsonElement para campos não documentados.
  • Go: use json.RawMessage ou structs com tags json:",omitempty" e não falhe em campos extras (comportamento default de encoding/json).

4. Payloads de NF-e

Todas as 5 variantes NF-e compartilham o mesmo shape de payload. O que diferencia uma da outra é:

  • o campo type dentro do data (productInvoice, productInvoiceEvent, productInvoiceSummary, productInvoiceEventSummary);
  • e quais campos opcionais vêm populados (resumos têm muitos campos null; eventos têm campos de enrichment da NF-e referenciada que podem vir null durante in-flight).

Valores possíveis de type:

typeQuando ocorre
productInvoiceNF-e completa recebida via DFe
productInvoiceEventEvento de NF-e (cancelamento, EPEC, etc.)
productInvoiceSummaryResumo de NF-e (metadados parciais)
productInvoiceEventSummaryResumo de evento de NF-e

4.1. product_invoice_inbound / issued_successfully

Enviado quando uma NF-e nova completa é recebida via DistribuicaoDFe e o XML é desempacotado com sucesso. Carrega os participantes, valores e links de download.

Campos populados vs sempre null

Igual ao que descrevemos em §4.2 para eventos, o shape de NFeMetadataResource é compartilhado com outros endpoints e contém campos que este webhook não popula.

Sempre populados:

CampoOrigem
accessKeyinfNFe/@Id (sem prefixo NFe) — chave de 44 dígitos.
parentAccessKey"" (string vazia — NF-e raiz não tem documento pai).
createdOnQuando o documento entrou no nosso sistema.
nsuNSU atribuído pela SEFAZ ao envelope DistribuicaoDFe (string).
nsuParent"" (string vazia para NF-e raiz).
typeSempre "productInvoice".
descriptionSempre o literal "Autorizado o uso da NF-e".
nfeNumber, nfeSerialNumberinfNFe/ide/nNF e infNFe/ide/serie.
issuedOninfNFe/ide/dhEmi.
totalInvoiceAmountinfNFe/total/ICMSTot/vNF formatado como "0.00".
operationTypeinfNFe/ide/tpNF"Incoming" (tpNF=0) ou "Outgoing" (tpNF=1).
company.id, company.federalTaxNumberSua empresa nfe.io.
issuer.federalTaxNumber, issuer.nameinfNFe/emit/{CNPJ|CPF} e xNome.
buyer.federalTaxNumber, buyer.nameinfNFe/dest/{CNPJ|CPF} e xNome.
transportation.federalTaxNumber, transportation.nameinfNFe/transp/transporta/{CNPJ|CPF, xNome}. Se a NF-e não declarou transportadora, vêm string vazia (""), não null.
links.xmlURL para baixar o XML completo via API.
links.pdfURL para baixar o DANFE (PDF) via API.

Containers issuer, buyer, transportation, links: o código sempre instancia esses objetos. Quando o XML da NF-e declara o participante, todos os campos internos vêm preenchidos. Quando algum campo interno é null, ele é omitido pela serialização global (§3). Se a NF-e não declarou uma transportadora, por exemplo, transportation pode chegar como { "federalTaxNumber": "", "name": "" } (strings vazias preenchidas pelo parser) ou apenas {} (objeto vazio após filtro de null).

Campos do shape que NÃO são populados (omitidos do JSON):

CampoComportamento real
xmlUrl (top-level)Ausente do JSON. Use links.xml. Há [JsonIgnore(WhenWritingNull)] explícito no shape e o valor é null para NF-e completa. Só é populada em webhooks de resumo de evento (§4.4.2).
federalTaxNumberSenderAusente do JSON (null no servidor, omitido pelo serializador global). Use issuer.federalTaxNumber.
nameSenderAusente do JSON. Use issuer.name.
environmentType0 (default de int; presente no JSON porque tipos de valor não-nullable não são afetados pelo filtro de null). Não é confiável — para saber o ambiente SEFAZ, use a configuração do inbound (EnvironmentSEFAZ).

Exemplo (payload real anonimizado)

Conteúdo top-level de body (sem o wrapper do §3):

{
"accessKey": "35260557622466000146550010011320981999999993",
"createdOn": "2026-05-19T11:41:44.695Z",
"parentAccessKey": "",
"company": {
"id": "9999999999999999999999999999997",
"federalTaxNumber": "99999999999999"
},
"issuer": {
"federalTaxNumber": "57622466000146",
"name": "DISTRIBUIDORA DE CARNES VALE DO MOGI IMP EXP LTDA"
},
"buyer": {
"federalTaxNumber": "99999999999999",
"name": "CHURRASCARIA CACADOR LTDA"
},
"transportation": {
"federalTaxNumber": "57622466000146",
"name": "DISTRIBUIDORA VALE DO MOGI IMP E EXP LTDA"
},
"links": {
"xml": "https://api.nfse.io/v2/companies/9999999999999999999999999999997/inbound/35260557622466000146550010011320981999999993/xml",
"pdf": "https://api.nfse.io/v2/companies/9999999999999999999999999999997/inbound/35260557622466000146550010011320981999999993/pdf"
},
"type": "productInvoice",
"nsu": "34691",
"nsuParent": "",
"nfeNumber": "1132098",
"nfeSerialNumber": "1",
"issuedOn": "2026-05-18T04:08:27Z",
"description": "Autorizado o uso da NF-e",
"totalInvoiceAmount": "980.72",
"operationType": "Incoming",
"environmentType": 0
}

Notas sobre o exemplo:

  • createdOn traz milissegundos (.695Z). Não assuma resolução de segundos no parsing.
  • federalTaxNumberSender/nameSender não aparecem — foram omitidos por serem null no servidor (ver §3 e tabela acima).
  • A accessKey da NF-e tem 44 dígitos (sem prefixo NFe). Ver §13 para a anatomia completa.

4.2. product_invoice_inbound / event_raised_successfully

Enviado quando um evento da SEFAZ é recebido para uma NF-e (cancelamento, Carta de Correção, EPEC, registro de passagem, etc.). Para a lista completa de tpEvento e mapeamento para eventAction, ver §6.1.

Quais campos vêm populados

O payload reaproveita o shape de NFeMetadataResource da §4.1, mas para eventos o servidor preenche um subconjunto específico:

Sempre populados (vêm do próprio evento):

CampoOrigem
accessKey51 dígitos: concatenação {tpEvento}{chaveNFe}{sequência}sem prefixo ID. Ex.: 110111412605...41 (cancelamento). Diferente do CT-e (§5.2), que mantém o prefixo ID por usar o Id original do XML sem reescrita.
parentAccessKeyChave da NF-e referenciada (44 dígitos).
createdOnQuando o evento entrou no nosso sistema (com milissegundos).
nsuNSU do evento (string).
typeSempre "productInvoiceEvent".
descriptionTexto literal vindo do XML do evento. Cancelamento → "Cancelamento". CC-e → "Carta de Correcao" (sem cedilha/acento, vindo do <descEvento> da SEFAZ). EPEC → "EPEC".
company.id, company.federalTaxNumberSua empresa.
links.xmlURL do XML do evento (não da NF-e).
links.pdfSempre string vazia (""). Eventos não geram PDF.

Condicionais — só populados quando a NF-e pai está indexada (caso típico após captura prévia via SEFAZ ADN; vêm null se o evento chegou antes do documento — janela transiente):

CampoTipoQuando vem null
nsuParentstringNF-e pai não indexada.
nfeNumber, nfeSerialNumberstringNF-e pai não indexada.
issuedOnDateTimeNF-e pai não indexada.
totalInvoiceAmountstringNF-e pai não indexada.
issuer.federalTaxNumber, issuer.namestringNF-e pai não indexada.
buyer.federalTaxNumber, buyer.namestringNF-e pai não indexada.
transportation.federalTaxNumber, transportation.namestringNF-e pai não indexada ou NF-e sem transportadora.

⚠️ operationType é sempre serializado, mas se a NF-e pai não estiver indexada, o servidor não consegue determinar a operação real e emite "Outgoing" por default. Trate esse campo como confiável apenas quando os demais campos condicionais (nfeNumber, issuer, etc.) também vierem populados.

Containers issuer, buyer, transportation e links: o código instancia esses objetos. Os campos internos podem vir omitidos quando forem null (regra global de serialização, §3). É comum, por exemplo, ver "transportation": {} (objeto vazio) num evento de uma NF-e que não declarou transportadora.

Campos do shape que NÃO são populados em eventos (omitidos do JSON):

CampoComportamento real
xmlUrl (top-level)Ausente do JSON. Use links.xml.
federalTaxNumberSenderAusente do JSON. Use issuer.federalTaxNumber.
nameSenderAusente do JSON. Use issuer.name.
environmentType0 (presente no JSON; valor de tipo int não é afetado pelo filtro de null). Não é confiável.

Exemplo — Cancelamento (tpEvento=110111)

Conteúdo top-level de body (sem o wrapper do §3) — payload real anonimizado:

{
"accessKey": "110111412605999999999999995500200059985119999999941",
"createdOn": "2026-05-18T19:21:54.06Z",
"parentAccessKey": "41260599999999999999550020005998511999999994",
"company": {
"id": "99999999999999999999",
"federalTaxNumber": "99999999999999"
},
"issuer": {
"federalTaxNumber": "99999999999999",
"name": "TESTE DISTRIBUIDORA DE FERRO E ACO LTDA"
},
"buyer": {
"federalTaxNumber": "99999999999999",
"name": "TESTE E TESTE LTDA"
},
"transportation": {},
"links": {
"xml": "https://api.nfse.io/v2/companies/99999999999999999999/inbound/41260599999999999999550020005998511999999994/events/110111412605999999999999995500200059985119999999941/xml",
"pdf": ""
},
"type": "productInvoiceEvent",
"nsu": "3272",
"nsuParent": "3271",
"nfeNumber": "599851",
"nfeSerialNumber": "2",
"issuedOn": "2026-05-15T15:22:30Z",
"description": "Cancelamento",
"totalInvoiceAmount": "7723.76",
"operationType": "Incoming",
"environmentType": 0
}

Notas sobre o exemplo:

  • accessKey começa com 110111 (tpEvento de cancelamento), sem o prefixo ID. Veja §13.1 para a anatomia.
  • transportation: {} — a NF-e original não declarou transportadora, então os campos internos foram omitidos pela serialização global e o container ficou vazio.
  • Campos como issuer, buyer, nfeNumber, totalInvoiceAmount vieram preenchidos porque a NF-e pai já estava indexada no momento da entrega do evento. Em janelas transientes (evento chega antes da NF-e), os containers de enrichment podem vir vazios e os campos escalares podem estar omitidos.

Exemplo — Carta de Correção (tpEvento=110110)

Mesmo shape; apenas accessKey (começa com 110110…) e description mudam. O texto da correção (xCorrecao, item alterado, etc.) não vem no payload — baixe o XML do evento via links.xml e leia procEventoNFe/evento/infEvento/detEvento/xCorrecao.

Payload real anonimizado:

{
"accessKey": "110110332605685839540001085500100010826019999999931",
"createdOn": "2026-05-18T20:29:35.898Z",
"parentAccessKey": "33260568583954000108550010001082601999999993",
"company": {
"id": "999999999999999999999999",
"federalTaxNumber": "99999999999999"
},
"issuer": {
"federalTaxNumber": "99999999999999",
"name": "TESTE LTDA"
},
"buyer": {
"federalTaxNumber": "99999999999999",
"name": "CASA DE SAUDE E TESTE SA"
},
"transportation": {},
"links": {
"xml": "https://api.nfse.io/v2/companies/999999999999999999999999/inbound/33260568583954000108550010001082601999999993/events/110110332605685839540001085500100010826019999999931/xml",
"pdf": ""
},
"type": "productInvoiceEvent",
"nsu": "62313",
"nsuParent": "62305",
"nfeNumber": "108260",
"nfeSerialNumber": "1",
"issuedOn": "2026-05-18T00:00:00Z",
"description": "Carta de Correcao",
"totalInvoiceAmount": "1967.55",
"operationType": "Incoming",
"environmentType": 0
}

⚠️ Observe que description chega como "Carta de Correcao" (sem cedilha/acento) — o valor é repassado verbatim do <descEvento> da SEFAZ. Não normalize/traduza no consumer.

4.3. product_invoice_inbound / input_event_raised_successfully (manifestação)

Enviado quando o destinatário registra uma manifestação sobre a NF-e. A SEFAZ define 4 tpEvento para manifestações; este serviço usa o eventAction = input_event_raised_successfully exclusivamente para esses 4 códigos. Eventos do emissor (cancelamento, CC-e, EPEC, etc.) saem como event_raised_successfully (§4.2).

Mapeamento dos 4 tpEvento:

tpEventoManifestaçãoSignificado
210200Confirmação da OperaçãoO destinatário recebeu a mercadoria como descrito.
210210Ciência da OperaçãoO destinatário está ciente, mas não confirma recebimento.
210220Operação DesconhecidaO destinatário não reconhece a operação.
210240Operação Não RealizadaA operação não foi concluída (devolução etc.).

Por que esse split existe? A manifestação é uma ação proativa do tomador (não do emissor). Muitos clientes querem filtrar manifestações distintas de eventos do emissor (cancelamento, CC-e). O split por eventAction permite filtros simples no seu consumer:

if body['action'] == 'input_event_raised_successfully':
# Manifestação do destinatário — processar fluxo de aceitação/rejeição
...
elif body['action'] == 'event_raised_successfully':
# Evento do emissor — atualizar status da NF-e (cancelado, CC-e aplicada)
...

Shape do payload é idêntico ao §4.2 — mesma estrutura, com type: "productInvoiceEvent". O que muda é apenas body.action no envelope.

4.4. Resumos (product_invoice_inbound_summary)

A SEFAZ pode entregar via DFe apenas metadados resumidos (não o documento completo). Isso acontece quando:

  1. A empresa ainda não está manifestada sobre a operação — a SEFAZ restringe acesso ao XML completo até que o destinatário confirme ciência ou confirmação.
  2. O documento tem mais de 3 meses desde a emissão (limite de retenção do DFe para XMLs completos).

Nesses casos, você recebe um webhook de resumo ao invés do webhook completo (§4.1/§4.2). Os campos vêm parcialmente populados (vide tabelas abaixo).

4.4.1. product_invoice_inbound_summary / issued_successfully

Compartilha o shape de NFeMetadataResource com §4.1, mas com type: "productInvoiceSummary" e um subconjunto bem menor de campos populados. Helper: NFePersistenceService.GetNFeMetadataResource(NFeSummaryNFeMetadata).

Sempre populados:

  • accessKey, createdOn, nsu.
  • type = "productInvoiceSummary".
  • nfeNumber — extraído da accessKey.
  • issuedOn — vem do receiptOn do resumo SEFAZ (não da emissão original — esse dado não está no resumo).
  • totalInvoiceAmount — formato "0.00".
  • company.id, company.federalTaxNumber.
  • issuer.federalTaxNumber, issuer.name — vêm do envelope DistribuicaoDFe.
  • links.xml — URL para baixar o XML quando estiver disponível.

Conditional: description = nfeObject.Reason — o xMotivo do SEFAZ retornado junto com o resumo. No fluxo de sucesso típico vem null (não setado pelo gateway); pode trazer texto da SEFAZ em casos de erro/aviso.

Não populados (vêm null/0):

CampoValor real
parentAccessKeynull.
nsuParentnull.
nfeSerialNumbernull (não está no resumo SEFAZ).
operationType"Outgoing" (default do enum int 0 — não é confiável aqui, ignorar).
environmentType0.
federalTaxNumberSender, nameSendernull.
xmlUrl (top-level)Ausente do JSON ([JsonIgnore(WhenWritingNull)] aplicado). Use links.xml.
buyer, transportationobjetos não instanciados, vêm null no JSON (diferente do §4.1, onde sempre vêm como objeto).
links.pdfnull (resumos não geram PDF).

Exemplo JSON (conteúdo top-level de body, sem o wrapper — ver §3):

{
"accessKey": "35240112345678000195550010000012341234567890",
"createdOn": "2026-05-13T14:42:15Z",
"parentAccessKey": null,
"company": {
"id": "54244e0ee340420fdc94ad10",
"federalTaxNumber": "98765432000100"
},
"issuer": {
"federalTaxNumber": "12345678000195",
"name": "FORNECEDOR LTDA"
},
"buyer": null,
"transportation": null,
"links": {
"xml": "https://api.nfse.io/v2/companies/54244e0ee340420fdc94ad10/inbound/35240112345678000195550010000012341234567890/xml",
"pdf": null
},
"federalTaxNumberSender": null,
"nameSender": null,
"type": "productInvoiceSummary",
"nsu": "21825",
"nsuParent": null,
"nfeNumber": "1234",
"nfeSerialNumber": null,
"issuedOn": "2026-05-13T10:00:00Z",
"description": null,
"totalInvoiceAmount": "1500.00",
"operationType": "Outgoing",
"environmentType": 0
}

Quando o cliente recebe summary e quer dados completos: veja §6.4 — a SEFAZ libera o XML completo após o destinatário registrar a manifestação. O webhook subsequente virá como §4.1 (type: "productInvoice").

4.4.2. product_invoice_inbound_summary / event_raised_successfully

Mesmo shape de NFeMetadataResource, com type: "productInvoiceEventSummary". ⚠️ Diferença importante: este é o único webhook que usa xmlUrl (top-level) ao invés de links.xml — o helper GetMetadataResource(NFeSummaryEventMetadata) emite o link do XML do evento em xmlUrl, e o objeto links vem null.

Sempre populados:

  • accessKey (chave do evento), parentAccessKey (chave NF-e referenciada).
  • createdOn, nsu.
  • type = "productInvoiceEventSummary".
  • descriptionxEvento literal do SEFAZ (ex.: "Cancelamento de NF-e homologado", "Carta de Correção registrada").
  • nfeNumber — extraído da parentAccessKey.
  • issuedOn — vem do receiptOn do evento.
  • company.id, company.federalTaxNumber.
  • xmlUrl — URL para baixar o XML do evento.

Não populados (vêm null/0):

CampoValor real
nsuParentnull.
nfeSerialNumbernull.
totalInvoiceAmountnull.
operationType"Outgoing" (default — ignorar).
environmentType0.
federalTaxNumberSender, nameSendernull.
issuer, buyer, transportation, linksobjetos não instanciados, vêm null no JSON.

Exemplo JSON — Cancelamento (resumo):

{
"accessKey": "ID110111352401123456780001955500100000123412345678901",
"createdOn": "2026-05-13T15:10:00Z",
"parentAccessKey": "35240112345678000195550010000012341234567890",
"company": {
"id": "54244e0ee340420fdc94ad10",
"federalTaxNumber": "98765432000100"
},
"issuer": null,
"buyer": null,
"transportation": null,
"links": null,
"xmlUrl": "https://api.nfse.io/v2/companies/54244e0ee340420fdc94ad10/inbound/35240112345678000195550010000012341234567890/events/ID110111352401123456780001955500100000123412345678901/xml",
"federalTaxNumberSender": null,
"nameSender": null,
"type": "productInvoiceEventSummary",
"nsu": "21850",
"nsuParent": null,
"nfeNumber": "1234",
"nfeSerialNumber": null,
"issuedOn": "2026-05-13T15:00:00Z",
"description": "Cancelamento de NF-e homologado",
"totalInvoiceAmount": null,
"operationType": "Outgoing",
"environmentType": 0
}

5. Payloads de CT-e

CT-e usa um shape distinto do payload de NF-e — campos típicos do transporte (recipient, sender, taker, dispatcher, productInvoices) substituem issuer/buyer/transportation. O CT-e também carrega um id próprio (GUID gerado pela API) e nsu como número (em NF-e é string).

5.1. transportation_invoice_inbound / issued_successfully

Enviado quando um CT-e novo autorizado é recebido via DistribuicaoDFe. Helper: CTeService.SendCTeWebhook, que emite um CTeMetadataWebhookResource.

Sempre populados (no servidor):

CampoTipoFonteDescrição
idstring (GUID)gerado APIIdentificador interno NFE.io.
createdOnDateTime?gerado APIQuando o NFE.io recebeu o documento (com milissegundos).
accessKeystring (44 dígitos)infCte/@Id (sem prefixo CTe)Chave de acesso do CT-e.
parentAccessKeystringgerado APISempre "" (string vazia) para CT-e raiz.
nsulong (number)Envelope DistribuicaoDFeNSU do registro. ⚠️ É long, não string (em NF-e é string).
company.idstringgerado APIID da empresa NFE.io.
typestringgerado APISempre "transportationInvoice".
descriptionstringgerado APISempre o literal "Autorizado o uso do CT-e".
xmlUrlstringgerado APIURL absoluta do XML do CT-e.

Campos opcionais (podem estar ausentes quando todos os internos forem null — comportamento da serialização global, §3):

CampoFonte XMLQuando some
company.federalTaxNumbergerado APISe o servidor não populou (raro). Sempre presente em fluxo normal.
recipient.federalTaxNumber, recipient.nameinfCte/dest/{CNPJ|CPF, xNome}Se o CT-e não declarou destinatário ou só um dos campos veio nulo.
sender.federalTaxNumber, sender.nameinfCte/rem/{CNPJ|CPF, xNome}Idem para remetente.
taker.federalTaxNumber, taker.nameinfCte/ide/toma3 ou toma4Idem para tomador.
dispatcher.federalTaxNumber, dispatcher.nameinfCte/exped/{CNPJ|CPF, xNome}Expedidor é opcional na SEFAZ; ausente quando não declarado.
issuedOninfCte/ide/dhEmiApenas se anomalia no XML.
productInvoicesNF-es referenciadasArray com objetos { accessKey }. Pode vir vazio ([]) ou ausente se o CT-e não declara NF-es.
totalAmountinfCte/vPrest/vTPrest (formato "0.00")Se o XML não tem o campo vTPrest.

Campos do shape NÃO populados (sempre ausentes):

  • issuer e buyer — herdados do MetadataResource, mas o helper de CT-e não os instancia. CT-e usa sender/recipient em vez disso.

⚠️ Em produção, os payloads de CT-e podem chegar bastante enxutos. Quando o XML não declara participantes opcionais e o NSU foi emitido sem vPrest, é normal o payload conter só os campos do primeiro bloco da tabela acima (Sempre populados) — sem recipient, sender, productInvoices ou totalAmount. Veja o exemplo abaixo.

Exemplo JSON — payload real enxuto (anonimizado), conteúdo top-level de body (sem o wrapper do §3):

{
"issuedOn": "2026-05-19T07:48:34Z",
"id": "94152fd4-07ec-4145-88c7-01c2595da531",
"createdOn": "2026-05-19T11:51:53.896Z",
"accessKey": "35260542584754000267570023199390221999999996",
"parentAccessKey": "",
"nsu": 53642910,
"company": {
"id": "99999999999999999999999"
},
"type": "transportationInvoice",
"description": "Autorizado o uso do CT-e",
"xmlUrl": "https://api.nfse.io/v2/companies/99999999999999999999999/inbound/35260542584754000267570023199390221999999996/xml"
}

Notas sobre o exemplo:

  • company.federalTaxNumber está omitido neste payload — o servidor não o populou. Use o id para casar com a sua empresa.
  • Nenhum dos participantes (recipient, sender, taker, dispatcher) aparece — todos foram omitidos pelo filtro de null da serialização. Isso não significa que a CT-e não os teve no XML — pode ser que o parser ainda não popule esses campos do XML completo. Para os dados fiscais detalhados, baixe o XML via xmlUrl.
  • productInvoices e totalAmount também estão ausentes — caso típico.

Exemplo JSON — payload completo (quando o XML declara todos os participantes):

{
"id": "c0b3a1f2-e340-420f-dc94-ad10c0b3a1f2",
"createdOn": "2026-05-13T16:20:00.000Z",
"accessKey": "35240187654321000111570010000045671234567890",
"parentAccessKey": "",
"nsu": 21900,
"company": {
"id": "54244e0ee340420fdc94ad10",
"federalTaxNumber": "98765432000100"
},
"type": "transportationInvoice",
"description": "Autorizado o uso do CT-e",
"xmlUrl": "https://api.nfse.io/v2/companies/54244e0ee340420fdc94ad10/inbound/35240187654321000111570010000045671234567890/xml",
"recipient": {
"federalTaxNumber": "12345678000195",
"name": "DESTINATARIO LTDA"
},
"sender": {
"federalTaxNumber": "98765432000100",
"name": "REMETENTE S.A."
},
"taker": {
"federalTaxNumber": "55566677000188",
"name": "TOMADOR DO SERVICO LTDA"
},
"dispatcher": {
"federalTaxNumber": "11122233000144",
"name": "EXPEDIDOR LOGISTICA LTDA"
},
"issuedOn": "2026-05-13T15:00:00Z",
"productInvoices": [
{ "accessKey": "35240112345678000195550010000012341234567890" },
{ "accessKey": "35240112345678000195550010000012351234567891" }
],
"totalAmount": "850.00"
}

⚠️ Diferenças de tipos vs NF-e: o CT-e tem id próprio (GUID) — a NF-e usa apenas accessKey. O CT-e tem nsu como long; a NF-e tem nsu como string.

5.2. transportation_invoice_inbound / event_raised_successfully

Enviado quando um evento de CT-e é recebido (cancelamento, prestação em desacordo, etc.). Helper: CTeService.SendCTeEventWebhook, que emite um CTeEventMetadataResource — payload bem mais enxuto que CT-e completo: sem participantes, sem productInvoices, sem totalAmount.

Campos do shape (10 ao todo):

CampoTipoFonteDescrição
idstring (GUID)gerado APIIdentificador interno NFE.io.
createdOnDateTime?gerado APIQuando o NFE.io recebeu o evento (com milissegundos).
accessKeystringevento/infEvento/@Idmantém o prefixo ID vindo do XMLChave do evento (não do CT-e pai). Ex.: ID110111352605...0001.
parentAccessKeystringevento/infEvento/chCTeChave do CT-e referenciado (44 dígitos).
nsulongEnvelope DistribuicaoDFeNSU do registro do evento (numérico, não string).
company.idstringgerado APIID da empresa NFE.io.
company.federalTaxNumberstring (opcional)gerado APIPode estar ausente.
typestringgerado APISempre "transportationInvoiceEvent".
descriptionstringprocEventoCTe/retEventoCTe/infEvento/xEventoTexto curto do evento (lido apenas de xEvento, sem fallback para descEvento — ver CTeGateway.cs L342). Cancelamento chega como "Cancelamento" (não "Cancelamento de CT-e homologado" como versões anteriores deste documento afirmavam).
xmlUrlstringgerado APIURL para baixar o XML do evento.
receiptOnDateTime?procEventoCTe/retEventoCTe/infEvento/dhRegEventoData/hora em que a SEFAZ registrou o evento.

⚠️ Diferença vs NF-e (§4.2): o accessKey do evento de CT-e preserva o prefixo ID (resultado da leitura direta do Id do XML pelo gateway), enquanto o evento de NF-e tem o ID removido pelo serviço ({tpEvento}{chave}{seq}).

Não há campos de enrichment para CT-e evento — o shape CTeEventMetadataResource não declara recipient, sender, etc. Se você precisa dos dados do CT-e pai, consulte-o via GET /v2/companies/{companyId}/inbound/{parentAccessKey} (mas note a §8.2 do doc combinado: aquele endpoint REST também não retorna participantes — só o payload do webhook §5.1 carrega isso).

Exemplo JSON — payload real anonimizado (conteúdo top-level de body, sem o wrapper do §3):

{
"receiptOn": "2026-05-16T00:03:27Z",
"id": "493d2525-d9e8-4c3f-b30f-c70510147e06",
"createdOn": "2026-05-16T03:21:04.943Z",
"accessKey": "ID11011135260555442952000157570010168676041999999990001",
"parentAccessKey": "35260555442952000157570010168676041999999990",
"nsu": 55125,
"company": {
"id": "999999999999999999999999999999",
"federalTaxNumber": "55271322000167"
},
"type": "transportationInvoiceEvent",
"description": "Cancelamento",
"xmlUrl": "https://api.nfse.io/v2/companies/999999999999999999999999999999/inbound/35260555442952000157570010168676041999999990/events/ID11011135260555442952000157570010168676041999999990001/xml"
}

Notas sobre o exemplo:

  • accessKey começa com ID110111 — prefixo ID + tpEvento (110111 = cancelamento) + chave do CT-e (44 dígitos) + sequência (001, 3 dígitos padded). Total: 55 caracteres (2 letras + 53 dígitos).
  • description é apenas "Cancelamento" — texto curto vindo do XML do evento, não "Cancelamento de CT-e homologado".
  • A ordem dos campos no JSON pode variar — não dependa dela.

Se você precisa de detalhes do CT-e parent (participantes, valores, NF-es transportadas), busque-o via GET /v2/companies/{companyId}/inbound/transportationinvoices/{parentAccessKey}.


6. Mapeamento tpEvento SEFAZ → eventAction

6.1. NF-e

A SEFAZ usa códigos tpEvento de 6 dígitos para identificar cada tipo de evento. O serviço NFE.io mapeia os 4 códigos de manifestação do destinatário (21020x/21024x) para input_event_raised_successfully e demais para event_raised_successfully:

tpEventoDescriçãoeventAction
110110Carta de Correção (CC-e)event_raised_successfully
110111Cancelamento de NF-eevent_raised_successfully
110140EPEC (Evento Prévio de Emissão em Contingência)event_raised_successfully
110150Registro de Passagem Eletrônicoevent_raised_successfully
110160Manifestação do Fiscoevent_raised_successfully
210200Confirmação da Operação (destinatário)input_event_raised_successfully
210210Ciência da Operação (destinatário)input_event_raised_successfully
210220Operação Desconhecida (destinatário)input_event_raised_successfully
210240Operação Não Realizada (destinatário)input_event_raised_successfully

Outros tpEvento da SEFAZ (não listados acima) caem por default em event_raised_successfully.

6.2. CT-e

tpEventoDescriçãoeventAction
110110Carta de Correção CT-eevent_raised_successfully
110111Cancelamento de CT-eevent_raised_successfully
110140EPEC para CT-eevent_raised_successfully
610110Prestação de Serviço em Desacordoevent_raised_successfully

CT-e não tem split de manifestação como NF-e — todos os eventos saem com event_raised_successfully.


7. Validação HMAC

Para garantir que o webhook realmente veio da nfe.io (e não de terceiros mal-intencionados), valide a assinatura HMAC-SHA256 no header da requisição:

Python (Flask):

import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = b"seu_secret_aqui" # configurado no painel nfe.io

def validate_webhook(payload_bytes: bytes, received_signature: str) -> bool:
expected = hmac.new(WEBHOOK_SECRET, payload_bytes, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_signature)

@app.route('/webhooks/nfeio', methods=['POST'])
def receive():
signature = request.headers.get('X-NFe-Signature', '')
if not validate_webhook(request.data, signature):
abort(401, 'Assinatura inválida')

body = request.json['body']
event_action = body['action'] # ex.: issued_successfully
document_type = body['type'] # ex.: productInvoice / transportationInvoiceEvent
access_key = body.get('accessKey')

# Despache por (event_action, document_type) — ver matriz no §12.
# Os campos do payload (accessKey, company, issuer, ...) estão
# espalhados em `body`, não sob `body.data`.
return '', 200

Node.js (Express):

const crypto = require('crypto');
const express = require('express');
const app = express();

const WEBHOOK_SECRET = 'seu_secret_aqui';

app.post('/webhooks/nfeio', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-nfe-signature'] || '';
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');

if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).send('Assinatura inválida');
}

const body = JSON.parse(req.body).body;
const { action, type, accessKey } = body;
// `action` é o eventAction (ex.: issued_successfully).
// `type` é o tipo do documento (ex.: productInvoice, transportationInvoiceEvent).
// Demais campos (`company`, `issuer`, ...) estão diretamente em `body`.
// Despache por (action, type) — ver matriz no §12.
res.status(200).end();
});

Configure o Webhook Secret no painel nfe.io → Empresas → {sua empresa} → Webhooks. Sem ele, qualquer pessoa que descobrir sua URL pode simular notificações.


8. URLs de download do XML

O payload do webhook não embute o XML — em vez disso, carrega uma URL para um endpoint público da API NFE.io. Onde essa URL fica depende do tipo de payload, porque o shape compartilhado expõe dois campos diferentes para essa finalidade:

WebhookCampo com a URL do XML
§4.1 NF-e completalinks.xml
§4.2 evento NF-e (cancelamento, CC-e, EPEC, etc.)links.xml
§4.3 manifestaçãolinks.xml
§4.4.1 resumo NF-elinks.xml
§4.4.2 resumo de evento NF-exmlUrl (top-level)
§5.1 CT-e completoxmlUrl (top-level)
§5.2 evento CT-exmlUrl (top-level)

Para NF-e existe também o campo links.pdf com a URL do DANFE (PDF) — só em webhooks de NF-e completa (§4.1). Em todos os webhooks de evento NF-e, links.pdf vem como string vazia (""); em resumos, vem null.

Os campos xmlUrl em NF-e completa/evento/manifestação/resumo-NF-e e links em CT-e/resumo-de-evento-NF-e nunca vêm populados. Esta é uma vestígio de polimorfismo no shape NFeMetadataResource, compartilhado entre vários consumidores REST e webhook — não há duplicação real no payload.

Comportamento do endpoint apontado pela URL

Independentemente de qual campo carregue a URL (links.xml ou xmlUrl), ela aponta para um destes endpoints públicos:

  • NF-e (completa): GET /v2/companies/{companyId}/inbound/{accessKey}/xml
  • NF-e (evento): GET /v2/companies/{companyId}/inbound/{accessKey}/events/{eventId}/xml
  • CT-e (completo): GET /v2/companies/{companyId}/inbound/{accessKey}/xml
  • CT-e (evento): GET /v2/companies/{companyId}/inbound/{accessKey}/events/{eventId}/xml

Roteamento por modelo é feito pelos dígitos 21–22 da chave (modelo do documento), via accessKey.slice(20, 22): 55 ⇒ NF-e, demais ⇒ CT-e. Autenticação via header Authorization: <API_KEY>.

Comportamento da resposta

A resposta NÃO é um redirect HTTP. O endpoint retorna:

HTTP/1.1 200 OK
Content-Type: application/json

{
"publicTemporaryUri": "https://<storage-host>/<path>?<query-de-acesso-temporário>"
}

A publicTemporaryUri é uma URL assinada de acesso direto ao blob (armazenamento de objetos da NFE.io), com TTL de ~10 minutos por default. Cliente faz um segundo GET nessa URL para baixar o XML diretamente — sem precisar do header Authorization (a assinatura na query string autoriza o acesso).

Fluxo cliente típico:

import requests

# Passo 1 — pedir o link temporário (autenticado)
r1 = requests.get(
f"https://api.nfse.io/v2/companies/{company_id}/inbound/{access_key}/xml",
headers={"Authorization": API_KEY},
)
temp_uri = r1.json()["publicTemporaryUri"]

# Passo 2 — baixar o XML pelo link temporário (sem autenticação)
r2 = requests.get(temp_uri)
xml_bytes = r2.content

PDF

CT-e não tem PDF — o endpoint GET .../inbound/{accessKey}/pdf retorna 204 No Content para chaves CT-e (modelo 57).

NF-e tem PDF (DANFE) gerado pela API: mesmo padrão de resposta (200 OK com publicTemporaryUri). Acesse via GET /v2/companies/{companyId}/inbound/{accessKey}/pdf.

Após expiração da URL temporária

Quando a publicTemporaryUri expira (~10 min), o blob storage retorna erro HTTP no segundo GET. O endpoint público da API permanece válido — basta requisitar /xml novamente para obter nova publicTemporaryUri fresca.

Recomendação: baixe e armazene XML logo após receber o webhook. Para acesso posterior, sempre chame /xml para obter URL temporária nova — não cacheie a publicTemporaryUri.


9. Reprocessamento de webhook

Se você precisar receber novamente um webhook (porque seu endpoint estava fora do ar, ou houve bug no consumer), use os endpoints:

NF-e:

POST /v2/companies/{companyId}/inbound/productinvoices/{accessKey}/processwebhook
Authorization: {api_key}

CT-e:

POST /v2/companies/{companyId}/inbound/transportationinvoices/{accessKey}/processwebhook
Authorization: {api_key}

Ambos retornam 200 OK e disparam novamente o webhook para a URL configurada da empresa.


10. Idempotência

Webhooks podem ser entregues mais de uma vez em caso de retry após falha temporária. Seu consumer deve ser idempotente:

  • Use accessKey como chave única (NF-e e CT-e completos). Se já processou, ignore.
  • Use (parentAccessKey, accessKey) como chave única para eventos — o accessKey do evento inclui o tpEvento no prefixo, garantindo unicidade.
  • Não use nsu como chave — o NSU pertence ao registro DFe, não ao documento; reprocessamento pode gerar novo NSU.

11. Boas práticas de integração

  1. Responda rápido: retorne HTTP 200 em menos de 5 segundos e processe assincronamente. O timeout da NFE.io é 30 segundos.
  2. Persista primeiro: salve o payload no seu banco antes de processar, para não perder dados em crash do consumer.
  3. Baixe os arquivos cedo: chame /xml (e /pdf quando aplicável) logo após receber o webhook e persista o conteúdo localmente — publicTemporaryUri expira em ~10 minutos.
  4. Implemente idempotência por accessKey — webhooks podem ser reentregues após retry. Ver §10.
  5. Roteie por (body.type, body.action) — a combinação dos 2 é o discriminador estável. Não filtre apenas por um deles.
  6. Trate productInvoices: [] em CT-e como normal — alguns CT-es declaram só a operação de transporte sem listar NF-es individuais.
  7. Não cacheie URLs temporáriaspublicTemporaryUri expira; sempre re-chame /xml quando precisar de acesso novo.
  8. Trate o split event_raised_successfully vs input_event_raised_successfully no consumer — manifestações do destinatário (§4.3) têm semântica distinta de eventos do emissor (§4.2).
  9. Configure alertas para HTTP 5xx no seu endpoint de webhook — detecte problemas antes de esgotar o orçamento de retries (50x em 24h).

12. Troubleshooting

Recebi um payload com vários campos ausentes ou objetos vazios. É bug?

Não. Veja §3 "Serialização: campos null são omitidos por padrão". O serializador da API está configurado para omitir campos null no JSON. Resultado prático:

  • Em product_invoice_inbound_summary, muitos campos do shape vêm ausentes (porque o resumo da SEFAZ não os preenche).
  • Em product_invoice_inbound / event_raised_successfully, campos de enrichment da NF-e pai podem estar ausentes quando a NF-e ainda não foi indexada.
  • Em transportation_invoice_inbound / issued_successfully, containers como recipient ou sender podem estar ausentes ou chegar como {} quando o CT-e não declara o participante.

Codifique seu parser para tolerar tanto ausência da chave quanto valor null.

productInvoices veio vazio no CT-e — está certo?

Sim, é possível. Alguns CT-es declaram apenas a operação de transporte sem listar NF-es individuais (raro mas válido pela SEFAZ). Trate productInvoices: [] como caso normal — não como erro.

Estou recebendo input_event_raised_successfully para uma operação minha. Por quê?

Significa que o destinatário (não você) registrou uma manifestação sobre a operação. Se você é o destinatário das NF-es recebidas via Inbound, isso reflete suas próprias manifestações sendo replicadas no webhook (ou de seu sistema, ou via painel nfe.io). Ver §4.3.

Não recebo webhooks. Checklist:

  1. URL configurada no painel nfe.io → Empresas → Webhooks? Pode levar alguns minutos para propagar após mudança.
  2. Inbound ativo para a empresa? Ver 02-doc-tecnica-clientes-nfe-cte-dev-pt.md §6 "Ativando o Serviço de Inbound".
  3. WebhookVersion: 2 configurado na ativação? V1 usa formato diferente e está deprecado.
  4. Endpoint retornando 200 em até 30s? Status 4xx (exceto 408/429) marca falha definitiva e não reenvia.
  5. NSU pendente? Existe gap de NSUs na fila? Verifique via GET .../inbound/productinvoices?nsuInicial=... (ver doc API).

Chamei /xml mas o response não tem o XML — só uma URL. É bug?

Não. Diferente do NFS-e (que faz 302 Found redirect), o endpoint /xml de NF-e/CT-e retorna 200 OK com JSON { "publicTemporaryUri": "<URL temporária do blob>" }. Faça um segundo GET na publicTemporaryUri (sem header Authorization) para baixar o XML. TTL ~10 min — não cacheie. Ver §8.

Como diferencio resumo vs documento completo?

Pelo par (body.action, body.type) no envelope. body.action é o eventAction da rota interna; body.type é o campo do payload (ver §3 e §4/§5):

body.actionbody.typeSignificado
issued_successfullyproductInvoiceNF-e completa
event_raised_successfullyproductInvoiceEventEvento NF-e completo
input_event_raised_successfullyproductInvoiceEventManifestação do destinatário
issued_successfullyproductInvoiceSummaryResumo NF-e
event_raised_successfullyproductInvoiceEventSummaryResumo evento NF-e
issued_successfullytransportationInvoiceCT-e completo
event_raised_successfullytransportationInvoiceEventEvento CT-e

Use body.action para diferenciar a ação (emissão, evento, manifestação) e body.type para diferenciar a variante do documento (NF-e/CT-e, completa/resumo, documento/evento).

O eventType da rota interna (product_invoice_inbound, product_invoice_inbound_summary, transportation_invoice_inbound) determina qual webhook do painel é disparado, mas não é necessariamente propagado dentro de body. Não dependa dele para parsing — use body.type.


13. Anatomia da chave de acesso (NF-e/NFC-e e CT-e)

A accessKey que aparece nos payloads de webhook é uma identificação única atribuída pela SEFAZ a cada documento fiscal e a cada evento. Esta seção descreve a composição posicional para você decodificar campos diretamente da chave (sem precisar parsear o XML).

📚 Fonte oficial. As regras abaixo seguem o Manual de Orientação do Contribuinte (MOC) NF-e 7.0, item 5.4, e o MOC CT-e 4.00, item 2.1.4. Para NFS-e Nacional, ver o §11 do webhook NFS-e 02-doc-tecnica-clientes-dev-nfse-inbound-webhook.md.

13.1. NF-e (modelo 55) e NFC-e (modelo 65) — 44 dígitos

35 2605 57622466000146 55 001 001132098 1 99999999 3
└─┬┘└─┬─┘└─────┬──────┘└┬┘└┬┘└────┬────┘└┬┘└───┬────┘└┬┘
│ │ │ │ │ │ │ │ │
cUF AAMM CNPJ mod sér nNF tpEmis cNF cDV
Pos.Tam.CampoDescrição
1–22cUFCódigo IBGE da UF do emitente. Ex.: 35 = SP, 33 = RJ, 41 = PR.
3–64AAMMAno (2 dígitos) + mês (2 dígitos) de emissão. Ex.: 2605 = maio/2026.
7–2014CNPJ do emitenteCNPJ com 14 dígitos (sem máscara).
21–222modModelo do documento: 55 = NF-e, 65 = NFC-e.
23–253serieSérie do documento (0–999, com zeros à esquerda).
26–349nNFNúmero sequencial da NF-e/NFC-e.
351tpEmisForma de emissão: 1 Normal, 2 Contingência FS-IA, 3 SCAN, 4 EPEC, 5 FS-DA, 6 SVC-AN, 7 SVC-RS, 9 Contingência off-line NFC-e.
36–438cNFCódigo numérico aleatório (8 dígitos).
441cDVDígito verificador (módulo 11).

Exemplo decodificado (chave 35260557622466000146550010011320981999999993, extraída do exemplo da §4.1):

CampoValorLeitura
cUF35São Paulo
AAMM2605Maio/2026
CNPJ57622466000146Emitente
mod55NF-e (modelo 55)
serie001Série 1
nNF001132098Número 1.132.098
tpEmis1Emissão normal
cNF99999999Código aleatório
cDV3DV

13.2. Chave do evento NF-e — 51 dígitos (sem prefixo ID)

Para eventos de NF-e/NFC-e (cancelamento, CC-e, EPEC, registros do fisco, manifestações do destinatário), o NFE.io entrega no campo accessKey uma chave de 51 dígitos, gerada como:

{tpEvento}{chaveNFe}{nSeqEvento}
Pos.Tam.CampoDescrição
1–66tpEventoCódigo do evento SEFAZ (6 dígitos). Tabela em §6.1. Ex.: 110110 (CC-e), 110111 (cancelamento), 210210 (ciência).
7–5044chaveNFeChave de acesso da NF-e referenciada (§13.1).
511nSeqEventoSequencial do evento para a mesma NF-e (1–99, normalmente 1).

⚠️ Sem o prefixo ID. No XML SEFAZ o atributo infEvento/@Id traz "ID" + tpEvento + chNFe + nSeqEvento (53 caracteres). O NFE.io remove o prefixo ID ao gravar o EventId na base e ao serializar o webhook (ver NFeService.cs linhas 1798–1799 — nfeEventMetadata.EventId = $"{chNfe}{nfeEventMetadata.Sequence}"). O parentAccessKey no mesmo payload contém apenas a chaveNFe (44 dígitos). Para o CT-e o prefixo ID é preservado — ver §13.4.

Exemplo decodificado (chave 110111412605999999999999995500200059985119999999941, extraída do exemplo de cancelamento da §4.2):

CampoValorLeitura
tpEvento110111Cancelamento de NF-e
chaveNFe41260599999999999999550020005998511999999994NF-e referenciada (PR/maio/2026/...)
nSeqEvento1Primeiro evento de cancelamento

13.3. CT-e (modelo 57) — 44 dígitos

A composição é idêntica à da NF-e, com mod=57:

35 2605 42584754000267 57 002 319939022 1 99999999 6
└─┬┘└─┬─┘└─────┬──────┘└┬┘└┬┘└────┬────┘└┬┘└───┬────┘└┬┘
│ │ │ │ │ │ │ │ │
cUF AAMM CNPJ mod sér nCT tpEmis cCT cDV
Pos.Tam.CampoDescrição
1–22cUFCódigo IBGE da UF do emitente.
3–64AAMMAno + mês de emissão.
7–2014CNPJ do emitenteCNPJ do prestador do serviço de transporte.
21–222modSempre 57 para CT-e. (67 = CT-e OS, 63 = CT-e GTV — não tratados por este webhook.)
23–253serieSérie do documento.
26–349nCTNúmero sequencial do CT-e.
351tpEmisForma de emissão: 1 Normal, 4 EPEC, 5 FS-DA, 7 SVC-RS, 8 SVC-SP.
36–438cCTCódigo numérico aleatório.
441cDVDígito verificador (módulo 11).

Exemplo decodificado (chave 35260542584754000267570023199390221999999996, extraída do exemplo da §5.1):

CampoValorLeitura
cUF35São Paulo
AAMM2605Maio/2026
CNPJ42584754000267Prestador do transporte
mod57CT-e
serie002Série 2
nCT319939022Número 319.939.022
tpEmis1Normal

13.4. Chave do evento CT-e — 55 caracteres (com prefixo ID)

ID{tpEvento}{chaveCTe}{nSeqEvento}
Pos.Tam.CampoDescrição
1–22Prefixo literalID (preservado do XML SEFAZ).
3–86tpEventoCódigo do evento (ver §6.2). Ex.: 110111 (cancelamento), 110110 (CC-e CT-e), 610110 (prestação em desacordo).
9–5244chaveCTeChave de acesso do CT-e referenciado (§13.3).
53–553nSeqEventoSequencial do evento (1–999), com zeros à esquerda.

⚠️ Diferença em relação ao evento NF-e: o CT-e preserva o prefixo ID e usa 3 dígitos para o sequencial; o evento NF-e remove o prefixo e usa 1 dígito (ver §13.2).

Exemplo decodificado (chave ID11011135260555442952000157570010168676041999999990001, extraída do exemplo de cancelamento da §5.2):

CampoValorLeitura
PrefixoIDLiteral
tpEvento110111Cancelamento de CT-e
chaveCTe35260555442952000157570010168676041999999990CT-e referenciado
nSeqEvento001Primeiro evento

13.5. Validação do DV (módulo 11)

O último dígito (cDV) é calculado com módulo 11 base 2-9 sobre os 43 dígitos anteriores. Algoritmo de referência (MOC NF-e 7.0 §5.4 / MOC CT-e 4.00 §7.3):

def calc_dv(chave43: str) -> str:
weights = (2, 3, 4, 5, 6, 7, 8, 9)
total = sum(int(d) * weights[i % 8] for i, d in enumerate(reversed(chave43)))
dv = 11 - (total % 11)
return "0" if dv >= 10 else str(dv)

# Uso:
chave = "35260557622466000146550010011320981999999993"
assert calc_dv(chave[:-1]) == chave[-1]

Use esse cálculo para validar integridade da chave antes de persistir no seu sistema. Se o DV não bater, o payload provavelmente sofreu corrupção no transporte — rejeite e force reprocessamento via §9.

NFE.io

A NFE.io é uma empresa de tecnologia que fornece soluções para automatizar e simplificar a emissão e gestão de notas fiscais eletrônicas. Com suas ferramentas, as empresas podem economizar tempo e reduzir erros, aumentando a eficiência e precisão do processo de emissão de notas fiscais.

Um dos principais cases de sucesso da NFE.io é a implementação da solução na empresa de transporte Rodonaves. Com a automatização da emissão e gestão de notas fiscais eletrônicas, a Rodonaves conseguiu reduzir em até 80% o tempo gasto nesse processo, o que se traduziu em uma significativa melhoria na eficiência operacional. Além disso, a empresa também conseguiu eliminar erros e atrasos na emissão de notas fiscais, o que melhorou a relação com seus clientes e aumentou a confiança dos órgãos fiscais.

Outro exemplo é a implementação da NFE.io na empresa de comércio eletrônico, a Loja Integrada. Com a automatização da emissão de notas fiscais, a Loja Integrada conseguiu aumentar a velocidade de emissão de notas em até 10 vezes, o que permitiu que a empresa atendesse a uma maior quantidade de clientes e, consequentemente, aumentar as suas vendas.

Além desses exemplos, a NFE.io também tem outros cases de sucesso com empresas de setores como indústria, construção, varejo e serviços, mostrando a versatilidade e eficácia da sua solução.

Em resumo, a NFE.io é uma empresa de tecnologia que oferece soluções para automatizar e simplificar a emissão e gestão de notas fiscais eletrônicas, ajudando as empresas a economizar tempo e reduzir erros, melhorando a eficiência e precisão do processo. Com cases de sucesso em diferentes setores, a NFE.io tem se destacado como uma empresa líder em automação fiscal.