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
- 2. Webhook — Contrato técnico
- 3. Envelope de entrega
- 4. Payloads de NF-e
- 5. Payloads de CT-e
- 6. Mapeamento
tpEventoSEFAZ →eventAction - 7. Validação HMAC
- 8. URLs de download do XML
- 9. Reprocessamento de webhook
- 10. Idempotência
- 11. Boas práticas de integração
- 12. Troubleshooting
- 13. Anatomia da chave de acesso (NF-e/NFC-e e CT-e)
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ília | eventType | eventAction | Quando ocorre |
|---|---|---|---|
| NF-e completa | product_invoice_inbound | issued_successfully | Nova NF-e autorizada recebida via DistribuicaoDFe, XML completo |
| Evento de NF-e | product_invoice_inbound | event_raised_successfully | Evento emitido pela própria origem da NF-e (cancelamento, EPEC, CC-e, registro de passagem, etc.) |
| Manifestação do destinatário | product_invoice_inbound | input_event_raised_successfully | Manifestação registrada pelo destinatário (confirmação, ciência, desconhecimento, operação não realizada) |
| Resumo de NF-e | product_invoice_inbound_summary | issued_successfully | Resumo de NF-e (metadados apenas — XML completo não disponível até manifestação ou via consulta on-demand) |
| Resumo de evento de NF-e | product_invoice_inbound_summary | event_raised_successfully | Resumo de evento — metadados do evento sem XML completo |
| CT-e completo | transportation_invoice_inbound | issued_successfully | Novo CT-e autorizado recebido via DistribuicaoDFe |
| Evento de CT-e | transportation_invoice_inbound | event_raised_successfully | Evento 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 Status | Comportamento 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 |
| 5xx | Falha 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 aoeventActionda rota interna (issued_successfully,event_raised_successfully,input_event_raised_successfully). Use-o para diferenciar ação.body.type— campo do próprio payload (verTypeem §4/§5). Valores:productInvoice,productInvoiceEvent,productInvoiceSummary,productInvoiceEventSummary,transportationInvoice,transportationInvoiceEvent. Use-o para diferenciar variante.- Demais campos (
accessKey,company,issuer, etc.) — espalhados diretamente embody(não há sub-objetodataoudocumentaninhado para NF-e/CT-e — diferente do NFS-e, cujo handler embrulha embody.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 3eventType(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 debody. 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:
- 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. - Objetos com todas as propriedades internas
nullaparecem como{}. Por exemplo, em um evento NF-e em que a NF-e pai ainda não foi indexada,transportationpode 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. - 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
nullnem como{}. - 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
exactOptionalPropertyTypespara o shape do webhook; trate todos os campos comostring | null | undefined. - C#: configure
JsonSerializercomJsonSerializerOptions { UnknownTypeHandling = ... }permissivo, ou useJsonElementpara campos não documentados. - Go: use
json.RawMessageou structs com tagsjson:",omitempty"e não falhe em campos extras (comportamento default deencoding/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
typedentro dodata(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 virnulldurante in-flight).
Valores possíveis de type:
type | Quando ocorre |
|---|---|
productInvoice | NF-e completa recebida via DFe |
productInvoiceEvent | Evento de NF-e (cancelamento, EPEC, etc.) |
productInvoiceSummary | Resumo de NF-e (metadados parciais) |
productInvoiceEventSummary | Resumo 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:
| Campo | Origem |
|---|---|
accessKey | infNFe/@Id (sem prefixo NFe) — chave de 44 dígitos. |
parentAccessKey | "" (string vazia — NF-e raiz não tem documento pai). |
createdOn | Quando o documento entrou no nosso sistema. |
nsu | NSU atribuído pela SEFAZ ao envelope DistribuicaoDFe (string). |
nsuParent | "" (string vazia para NF-e raiz). |
type | Sempre "productInvoice". |
description | Sempre o literal "Autorizado o uso da NF-e". |
nfeNumber, nfeSerialNumber | infNFe/ide/nNF e infNFe/ide/serie. |
issuedOn | infNFe/ide/dhEmi. |
totalInvoiceAmount | infNFe/total/ICMSTot/vNF formatado como "0.00". |
operationType | infNFe/ide/tpNF — "Incoming" (tpNF=0) ou "Outgoing" (tpNF=1). |
company.id, company.federalTaxNumber | Sua empresa nfe.io. |
issuer.federalTaxNumber, issuer.name | infNFe/emit/{CNPJ|CPF} e xNome. |
buyer.federalTaxNumber, buyer.name | infNFe/dest/{CNPJ|CPF} e xNome. |
transportation.federalTaxNumber, transportation.name | infNFe/transp/transporta/{CNPJ|CPF, xNome}. Se a NF-e não declarou transportadora, vêm string vazia (""), não null. |
links.xml | URL para baixar o XML completo via API. |
links.pdf | URL 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):
| Campo | Comportamento 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). |
federalTaxNumberSender | Ausente do JSON (null no servidor, omitido pelo serializador global). Use issuer.federalTaxNumber. |
nameSender | Ausente do JSON. Use issuer.name. |
environmentType | 0 (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:
createdOntraz milissegundos (.695Z). Não assuma resolução de segundos no parsing.federalTaxNumberSender/nameSendernão aparecem — foram omitidos por seremnullno servidor (ver §3 e tabela acima).- A
accessKeyda NF-e tem 44 dígitos (sem prefixoNFe). 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):
| Campo | Origem |
|---|---|
accessKey | 51 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. |
parentAccessKey | Chave da NF-e referenciada (44 dígitos). |
createdOn | Quando o evento entrou no nosso sistema (com milissegundos). |
nsu | NSU do evento (string). |
type | Sempre "productInvoiceEvent". |
description | Texto 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.federalTaxNumber | Sua empresa. |
links.xml | URL do XML do evento (não da NF-e). |
links.pdf | Sempre 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):
| Campo | Tipo | Quando vem null |
|---|---|---|
nsuParent | string | NF-e pai não indexada. |
nfeNumber, nfeSerialNumber | string | NF-e pai não indexada. |
issuedOn | DateTime | NF-e pai não indexada. |
totalInvoiceAmount | string | NF-e pai não indexada. |
issuer.federalTaxNumber, issuer.name | string | NF-e pai não indexada. |
buyer.federalTaxNumber, buyer.name | string | NF-e pai não indexada. |
transportation.federalTaxNumber, transportation.name | string | NF-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):
| Campo | Comportamento real |
|---|---|
xmlUrl (top-level) | Ausente do JSON. Use links.xml. |
federalTaxNumberSender | Ausente do JSON. Use issuer.federalTaxNumber. |
nameSender | Ausente do JSON. Use issuer.name. |
environmentType | 0 (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:
accessKeycomeça com110111(tpEvento de cancelamento), sem o prefixoID. 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,totalInvoiceAmountvieram 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
descriptionchega 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:
tpEvento | Manifestação | Significado |
|---|---|---|
210200 | Confirmação da Operação | O destinatário recebeu a mercadoria como descrito. |
210210 | Ciência da Operação | O destinatário está ciente, mas não confirma recebimento. |
210220 | Operação Desconhecida | O destinatário não reconhece a operação. |
210240 | Operação Não Realizada | A 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:
- 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.
- 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 daaccessKey.issuedOn— vem doreceiptOndo 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):
| Campo | Valor real |
|---|---|
parentAccessKey | null. |
nsuParent | null. |
nfeSerialNumber | null (não está no resumo SEFAZ). |
operationType | "Outgoing" (default do enum int 0 — não é confiável aqui, ignorar). |
environmentType | 0. |
federalTaxNumberSender, nameSender | null. |
xmlUrl (top-level) | Ausente do JSON ([JsonIgnore(WhenWritingNull)] aplicado). Use links.xml. |
buyer, transportation | objetos não instanciados, vêm null no JSON (diferente do §4.1, onde sempre vêm como objeto). |
links.pdf | null (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".description—xEventoliteral do SEFAZ (ex.:"Cancelamento de NF-e homologado","Carta de Correção registrada").nfeNumber— extraído daparentAccessKey.issuedOn— vem doreceiptOndo evento.company.id,company.federalTaxNumber.xmlUrl— URL para baixar o XML do evento.
Não populados (vêm null/0):
| Campo | Valor real |
|---|---|
nsuParent | null. |
nfeSerialNumber | null. |
totalInvoiceAmount | null. |
operationType | "Outgoing" (default — ignorar). |
environmentType | 0. |
federalTaxNumberSender, nameSender | null. |
issuer, buyer, transportation, links | objetos 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):
| Campo | Tipo | Fonte | Descrição |
|---|---|---|---|
id | string (GUID) | gerado API | Identificador interno NFE.io. |
createdOn | DateTime? | gerado API | Quando o NFE.io recebeu o documento (com milissegundos). |
accessKey | string (44 dígitos) | infCte/@Id (sem prefixo CTe) | Chave de acesso do CT-e. |
parentAccessKey | string | gerado API | Sempre "" (string vazia) para CT-e raiz. |
nsu | long (number) | Envelope DistribuicaoDFe | NSU do registro. ⚠️ É long, não string (em NF-e é string). |
company.id | string | gerado API | ID da empresa NFE.io. |
type | string | gerado API | Sempre "transportationInvoice". |
description | string | gerado API | Sempre o literal "Autorizado o uso do CT-e". |
xmlUrl | string | gerado API | URL absoluta do XML do CT-e. |
Campos opcionais (podem estar ausentes quando todos os internos forem
null — comportamento da serialização global, §3):
| Campo | Fonte XML | Quando some |
|---|---|---|
company.federalTaxNumber | gerado API | Se o servidor não populou (raro). Sempre presente em fluxo normal. |
recipient.federalTaxNumber, recipient.name | infCte/dest/{CNPJ|CPF, xNome} | Se o CT-e não declarou destinatário ou só um dos campos veio nulo. |
sender.federalTaxNumber, sender.name | infCte/rem/{CNPJ|CPF, xNome} | Idem para remetente. |
taker.federalTaxNumber, taker.name | infCte/ide/toma3 ou toma4 | Idem para tomador. |
dispatcher.federalTaxNumber, dispatcher.name | infCte/exped/{CNPJ|CPF, xNome} | Expedidor é opcional na SEFAZ; ausente quando não declarado. |
issuedOn | infCte/ide/dhEmi | Apenas se anomalia no XML. |
productInvoices | NF-es referenciadas | Array com objetos { accessKey }. Pode vir vazio ([]) ou ausente se o CT-e não declara NF-es. |
totalAmount | infCte/vPrest/vTPrest (formato "0.00") | Se o XML não tem o campo vTPrest. |
Campos do shape NÃO populados (sempre ausentes):
issuerebuyer— herdados doMetadataResource, mas o helper de CT-e não os instancia. CT-e usasender/recipientem 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) — semrecipient,sender,productInvoicesoutotalAmount. 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.federalTaxNumberestá omitido neste payload — o servidor não o populou. Use oidpara casar com a sua empresa.- Nenhum dos participantes (
recipient,sender,taker,dispatcher) aparece — todos foram omitidos pelo filtro denullda 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 viaxmlUrl. productInvoicesetotalAmounttambé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
idpróprio (GUID) — a NF-e usa apenasaccessKey. O CT-e temnsucomo long; a NF-e temnsucomo 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):
| Campo | Tipo | Fonte | Descrição |
|---|---|---|---|
id | string (GUID) | gerado API | Identificador interno NFE.io. |
createdOn | DateTime? | gerado API | Quando o NFE.io recebeu o evento (com milissegundos). |
accessKey | string | evento/infEvento/@Id — mantém o prefixo ID vindo do XML | Chave do evento (não do CT-e pai). Ex.: ID110111352605...0001. |
parentAccessKey | string | evento/infEvento/chCTe | Chave do CT-e referenciado (44 dígitos). |
nsu | long | Envelope DistribuicaoDFe | NSU do registro do evento (numérico, não string). |
company.id | string | gerado API | ID da empresa NFE.io. |
company.federalTaxNumber | string (opcional) | gerado API | Pode estar ausente. |
type | string | gerado API | Sempre "transportationInvoiceEvent". |
description | string | procEventoCTe/retEventoCTe/infEvento/xEvento | Texto 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). |
xmlUrl | string | gerado API | URL para baixar o XML do evento. |
receiptOn | DateTime? | procEventoCTe/retEventoCTe/infEvento/dhRegEvento | Data/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:
accessKeycomeça comID110111— prefixoID+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:
tpEvento | Descrição | eventAction |
|---|---|---|
110110 | Carta de Correção (CC-e) | event_raised_successfully |
110111 | Cancelamento de NF-e | event_raised_successfully |
110140 | EPEC (Evento Prévio de Emissão em Contingência) | event_raised_successfully |
110150 | Registro de Passagem Eletrônico | event_raised_successfully |
110160 | Manifestação do Fisco | event_raised_successfully |
210200 | Confirmação da Operação (destinatário) | input_event_raised_successfully |
210210 | Ciência da Operação (destinatário) | input_event_raised_successfully |
210220 | Operação Desconhecida (destinatário) | input_event_raised_successfully |
210240 | Operaçã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
tpEvento | Descrição | eventAction |
|---|---|---|
110110 | Carta de Correção CT-e | event_raised_successfully |
110111 | Cancelamento de CT-e | event_raised_successfully |
110140 | EPEC para CT-e | event_raised_successfully |
610110 | Prestação de Serviço em Desacordo | event_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:
| Webhook | Campo com a URL do XML |
|---|---|
| §4.1 NF-e completa | links.xml |
| §4.2 evento NF-e (cancelamento, CC-e, EPEC, etc.) | links.xml |
| §4.3 manifestação | links.xml |
| §4.4.1 resumo NF-e | links.xml |
| §4.4.2 resumo de evento NF-e | xmlUrl (top-level) |
| §5.1 CT-e completo | xmlUrl (top-level) |
| §5.2 evento CT-e | xmlUrl (top-level) |
Para NF-e existe também o campo
links.pdfcom a URL do DANFE (PDF) — só em webhooks de NF-e completa (§4.1). Em todos os webhooks de evento NF-e,links.pdfvem como string vazia (""); em resumos, vemnull.
Os campos
xmlUrlem NF-e completa/evento/manifestação/resumo-NF-e elinksem CT-e/resumo-de-evento-NF-e nunca vêm populados. Esta é uma vestígio de polimorfismo no shapeNFeMetadataResource, 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
/xmlpara obter URL temporária nova — não cacheie apublicTemporaryUri.
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
accessKeycomo chave única (NF-e e CT-e completos). Se já processou, ignore. - Use
(parentAccessKey, accessKey)como chave única para eventos — oaccessKeydo evento inclui otpEventono prefixo, garantindo unicidade. - Não use
nsucomo chave — o NSU pertence ao registro DFe, não ao documento; reprocessamento pode gerar novo NSU.
11. Boas práticas de integração
- Responda rápido: retorne HTTP 200 em menos de 5 segundos e processe assincronamente. O timeout da NFE.io é 30 segundos.
- Persista primeiro: salve o payload no seu banco antes de processar, para não perder dados em crash do consumer.
- Baixe os arquivos cedo: chame
/xml(e/pdfquando aplicável) logo após receber o webhook e persista o conteúdo localmente —publicTemporaryUriexpira em ~10 minutos. - Implemente idempotência por
accessKey— webhooks podem ser reentregues após retry. Ver §10. - Roteie por
(body.type, body.action)— a combinação dos 2 é o discriminador estável. Não filtre apenas por um deles. - Trate
productInvoices: []em CT-e como normal — alguns CT-es declaram só a operação de transporte sem listar NF-es individuais. - Não cacheie URLs temporárias —
publicTemporaryUriexpira; sempre re-chame/xmlquando precisar de acesso novo. - Trate o split
event_raised_successfullyvsinput_event_raised_successfullyno consumer — manifestações do destinatário (§4.3) têm semântica distinta de eventos do emissor (§4.2). - 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 comorecipientousenderpodem 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:
- URL configurada no painel nfe.io → Empresas → Webhooks? Pode levar alguns minutos para propagar após mudança.
- Inbound ativo para a empresa? Ver
02-doc-tecnica-clientes-nfe-cte-dev-pt.md§6 "Ativando o Serviço de Inbound". WebhookVersion: 2configurado na ativação? V1 usa formato diferente e está deprecado.- Endpoint retornando 200 em até 30s? Status 4xx (exceto 408/429) marca falha definitiva e não reenvia.
- 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.action | body.type | Significado |
|---|---|---|
issued_successfully | productInvoice | NF-e completa |
event_raised_successfully | productInvoiceEvent | Evento NF-e completo |
input_event_raised_successfully | productInvoiceEvent | Manifestação do destinatário |
issued_successfully | productInvoiceSummary | Resumo NF-e |
event_raised_successfully | productInvoiceEventSummary | Resumo evento NF-e |
issued_successfully | transportationInvoice | CT-e completo |
event_raised_successfully | transportationInvoiceEvent | Evento 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
eventTypeda rota interna (product_invoice_inbound,product_invoice_inbound_summary,transportation_invoice_inbound) determina qual webhook do painel é disparado, mas não é necessariamente propagado dentro debody. Não dependa dele para parsing — usebody.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. | Campo | Descrição |
|---|---|---|---|
| 1–2 | 2 | cUF | Código IBGE da UF do emitente. Ex.: 35 = SP, 33 = RJ, 41 = PR. |
| 3–6 | 4 | AAMM | Ano (2 dígitos) + mês (2 dígitos) de emissão. Ex.: 2605 = maio/2026. |
| 7–20 | 14 | CNPJ do emitente | CNPJ com 14 dígitos (sem máscara). |
| 21–22 | 2 | mod | Modelo do documento: 55 = NF-e, 65 = NFC-e. |
| 23–25 | 3 | serie | Série do documento (0–999, com zeros à esquerda). |
| 26–34 | 9 | nNF | Número sequencial da NF-e/NFC-e. |
| 35 | 1 | tpEmis | Forma 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–43 | 8 | cNF | Código numérico aleatório (8 dígitos). |
| 44 | 1 | cDV | Dígito verificador (módulo 11). |
Exemplo decodificado (chave 35260557622466000146550010011320981999999993,
extraída do exemplo da §4.1):
| Campo | Valor | Leitura |
|---|---|---|
cUF | 35 | São Paulo |
AAMM | 2605 | Maio/2026 |
CNPJ | 57622466000146 | Emitente |
mod | 55 | NF-e (modelo 55) |
serie | 001 | Série 1 |
nNF | 001132098 | Número 1.132.098 |
tpEmis | 1 | Emissão normal |
cNF | 99999999 | Código aleatório |
cDV | 3 | DV |
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. | Campo | Descrição |
|---|---|---|---|
| 1–6 | 6 | tpEvento | Código do evento SEFAZ (6 dígitos). Tabela em §6.1. Ex.: 110110 (CC-e), 110111 (cancelamento), 210210 (ciência). |
| 7–50 | 44 | chaveNFe | Chave de acesso da NF-e referenciada (§13.1). |
| 51 | 1 | nSeqEvento | Sequencial do evento para a mesma NF-e (1–99, normalmente 1). |
⚠️ Sem o prefixo
ID. No XML SEFAZ o atributoinfEvento/@Idtraz"ID" + tpEvento + chNFe + nSeqEvento(53 caracteres). O NFE.io remove o prefixoIDao gravar oEventIdna base e ao serializar o webhook (verNFeService.cslinhas 1798–1799 —nfeEventMetadata.EventId = $"{chNfe}{nfeEventMetadata.Sequence}"). OparentAccessKeyno mesmo payload contém apenas achaveNFe(44 dígitos). Para o CT-e o prefixoIDé preservado — ver §13.4.
Exemplo decodificado
(chave 110111412605999999999999995500200059985119999999941, extraída do
exemplo de cancelamento da §4.2):
| Campo | Valor | Leitura |
|---|---|---|
tpEvento | 110111 | Cancelamento de NF-e |
chaveNFe | 41260599999999999999550020005998511999999994 | NF-e referenciada (PR/maio/2026/...) |
nSeqEvento | 1 | Primeiro 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. | Campo | Descrição |
|---|---|---|---|
| 1–2 | 2 | cUF | Código IBGE da UF do emitente. |
| 3–6 | 4 | AAMM | Ano + mês de emissão. |
| 7–20 | 14 | CNPJ do emitente | CNPJ do prestador do serviço de transporte. |
| 21–22 | 2 | mod | Sempre 57 para CT-e. (67 = CT-e OS, 63 = CT-e GTV — não tratados por este webhook.) |
| 23–25 | 3 | serie | Série do documento. |
| 26–34 | 9 | nCT | Número sequencial do CT-e. |
| 35 | 1 | tpEmis | Forma de emissão: 1 Normal, 4 EPEC, 5 FS-DA, 7 SVC-RS, 8 SVC-SP. |
| 36–43 | 8 | cCT | Código numérico aleatório. |
| 44 | 1 | cDV | Dígito verificador (módulo 11). |
Exemplo decodificado
(chave 35260542584754000267570023199390221999999996, extraída do
exemplo da §5.1):
| Campo | Valor | Leitura |
|---|---|---|
cUF | 35 | São Paulo |
AAMM | 2605 | Maio/2026 |
CNPJ | 42584754000267 | Prestador do transporte |
mod | 57 | CT-e |
serie | 002 | Série 2 |
nCT | 319939022 | Número 319.939.022 |
tpEmis | 1 | Normal |
13.4. Chave do evento CT-e — 55 caracteres (com prefixo ID)
ID{tpEvento}{chaveCTe}{nSeqEvento}
| Pos. | Tam. | Campo | Descrição |
|---|---|---|---|
| 1–2 | 2 | Prefixo literal | ID (preservado do XML SEFAZ). |
| 3–8 | 6 | tpEvento | Código do evento (ver §6.2). Ex.: 110111 (cancelamento), 110110 (CC-e CT-e), 610110 (prestação em desacordo). |
| 9–52 | 44 | chaveCTe | Chave de acesso do CT-e referenciado (§13.3). |
| 53–55 | 3 | nSeqEvento | Sequencial do evento (1–999), com zeros à esquerda. |
⚠️ Diferença em relação ao evento NF-e: o CT-e preserva o prefixo
IDe 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):
| Campo | Valor | Leitura |
|---|---|---|
| Prefixo | ID | Literal |
tpEvento | 110111 | Cancelamento de CT-e |
chaveCTe | 35260555442952000157570010168676041999999990 | CT-e referenciado |
nSeqEvento | 001 | Primeiro 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.