Payloads dos webhooks de emissão
Esta página documenta o que chega no corpo da requisição quando a NFE.io notifica seu endpoint sobre uma emissão. Para o cadastro do webhook, veja Como cadastrar; para o conceito geral, Conceitos.
Os exemplos desta página são entregas reais de produção, com dados sensíveis substituídos. CNPJ, CPF e chaves de acesso foram trocados por valores fictícios com dígito verificador válido — servem para testar parsing e validação de DV, mas não correspondem a documentos existentes. Valores monetários, alíquotas, códigos fiscais, datas e a estrutura são fiéis ao original.
1. A regra que quebra integrações: dois envelopes
O corpo entregue não tem a mesma forma para todos os produtos. Esta é a diferença mais importante desta página:
| Tipo de evento | Envelope | Onde ficam os campos |
|---|---|---|
service_invoice (NFS-e) | {"payload": { ... }} | dentro de payload |
product_invoice (NF-e) | achatado | na raiz do corpo |
consumer_invoice (NFC-e) | achatado | na raiz do corpo |
*_inbound, product_tax, tax_payment_form | achatado | na raiz do corpo |
Ou seja: para NFS-e você acessa body.payload.status; para NF-e/NFC-e, body.status.
Se você assina mais de um tipo de evento no mesmo endpoint, normalize antes de processar:
// Funciona para NFS-e (envelopado) e NF-e/NFC-e (achatado)
const documento = body.payload ?? body;
Fazer isso desde o início evita retrabalho quando você passar a emitir outro tipo de documento.
2. Como descobrir qual evento chegou
Combine o cabeçalho com o corpo — os dois carregam informações diferentes:
| Origem | O que traz | Exemplo |
|---|---|---|
Cabeçalho X-Hook-Event | o tipo do evento | service_invoice |
Cabeçalho X-Hook-Id | identificador único da entrega (use para deduplicar) | 4efa03baf6014b788e591da82efbaba8 |
Cabeçalho X-Hook-Attempts | número da tentativa | 1 |
Corpo (flowStatus / status) | o resultado da operação | Issued, IssueFailed, Error |
X-Hook-Event traz o tipo do evento (service_invoice), não o par tipo + ação. Para saber o que aconteceu, leia flowStatus (NFS-e) ou status (NF-e/NFC-e) no corpo.
3. NFS-e — service_invoice
Envelope: {"payload": { ... }}. Terminologia própria: o emissor é provider e o tomador é borrower.
3.1. Emissão bem-sucedida
{
"payload": {
"id": "6a6898a87864720001efa2dc",
"externalId": "e671beec-254b-4952-a046-430df91b2d2f",
"environment": "Production",
"flowStatus": "Issued",
"status": "Issued",
"provider": {
"name": "EMPRESA EXEMPLO LTDA",
"federalTaxNumber": "09505320001905",
"municipalTaxNumber": "111111111",
"address": {
"postalCode": "20040020",
"street": "Rua Exemplo",
"number": "100",
"district": "Centro",
"city": { "code": "3304557", "name": "Rio de Janeiro" },
"state": "RJ",
"country": "BRA"
},
"type": "LegalPerson, Company"
},
"borrower": {
"name": "EXEMPLO DISTRIBUIDORA SA",
"federalTaxNumber": "34332984000120",
"address": {
"postalCode": "74915-240",
"city": { "code": "5201405", "name": "Aparecida de Goiânia" },
"state": "GO",
"country": "BRA"
},
"type": "LegalPerson"
},
"number": 2905,
"rpsNumber": 68521,
"rpsSerialNumber": "1",
"rpsType": "Rps",
"rpsStatus": "Normal",
"taxationType": "WithinCity",
"cityServiceCode": "002",
"federalServiceCode": "170102",
"nbsCode": "118061000",
"servicesAmount": 1620.0,
"baseTaxAmount": 1620.0,
"issRate": 0.05,
"issTaxAmount": 81.0,
"pisAmountWithheld": 10.53,
"cofinsAmountWithheld": 48.6,
"irAmountWithheld": 24.3,
"csllAmountWithheld": 16.2,
"inssAmountWithheld": 0.0,
"issAmountWithheld": 0.0,
"retentionType": "notWithheld",
"amountWithheld": 99.63,
"amountNet": 1520.37,
"issuedOn": "2026-07-28T08:55:20-03:00",
"createdOn": "2026-07-28T11:55:20.9045977+00:00",
"apiVersion": 2
}
}
Campos que a maioria das integrações usa:
| Campo | Tipo | Para que serve |
|---|---|---|
id | string | Identificador da nota na NFE.io. Use para consultar via API. |
externalId | string | O seu identificador, informado na emissão. Melhor chave para casar com o seu pedido. |
flowStatus | string | Estado do fluxo: Issued, IssueFailed, Cancelled… |
status | string | Estado da nota: Issued, Error, Cancelled. |
environment | string | Production ou Development. Sempre confira antes de gravar em produção. |
number | number | Número da NFS-e. 0 quando a emissão falhou. |
rpsNumber / rpsSerialNumber | number / string | Número e série do RPS. |
amountNet | number | Valor líquido após retenções. |
documentUrl e documentXmlUrl não são links públicosQuando presentes, esses campos podem vir com o esquema interno b2://, que não é acessível pelo seu sistema. Para obter PDF e XML, use os endpoints da API REST da nota.
3.2. Falha na emissão
Mesmo envelope; o que muda é o par flowStatus + flowMessage:
{
"payload": {
"id": "6a636c5a7864720001d754b3",
"externalId": "CUSTOMER_MONTHLY_SERVICE_FEE#CUSTOMER#00000000-0000-4000-8000-000000000000",
"environment": "Development",
"flowStatus": "IssueFailed",
"flowMessage": "max retry reached on send batch stage",
"status": "Error",
"number": 0,
"rpsNumber": 2190,
"rpsSerialNumber": "IO",
"cityServiceCode": "5895",
"federalServiceCode": "15.10",
"servicesAmount": 9.18,
"issRate": 0.02,
"issTaxAmount": 0.1836,
"amountNet": 9.18,
"approximateTax": {
"source": "IBPT/empresometro.com.br",
"version": "21.1.F",
"totalRate": 0.1829,
"totalAmount": 1.679022
},
"apiVersion": 2
}
}
Note que number vem 0 e status vem Error. A causa legível fica em flowMessage — registre esse campo no seu log, é o que o suporte pede primeiro.
4. NF-e — product_invoice
Envelope achatado. Terminologia: issuer (emitente) e buyer (destinatário).
{
"id": "3d4c1b2a5f6e7d8c9b0a1f2e3d4c5b6a",
"serie": 8,
"number": 25969,
"status": "Issued",
"authorization": {
"accessKey": "35260739425723000178550080000259691818677728"
},
"operationNature": "Outras Entradas - Retorno Simbólico",
"operationType": "Incoming",
"environmentType": "Production",
"purposeType": "Normal",
"issuer": {
"name": "MODELO SERVICOS LTDA",
"federalTaxNumber": 31305761000185,
"taxRegime": "LucroReal",
"address": {
"postalCode": "16204393",
"city": { "code": "3506508", "name": "BIRIGUI" },
"state": "SP",
"country": "BRA"
},
"type": "LegalEntity"
},
"buyer": {
"name": "EXEMPLO DISTRIBUIDORA SA",
"federalTaxNumber": 32308042000180,
"stateTaxNumberIndicator": "TaxPayer",
"address": {
"city": { "code": "3518800", "name": "Guarulhos" },
"state": "SP",
"country": "BRA"
},
"type": "LegalEntity"
},
"totals": {
"icms": {
"baseTax": 78.56,
"icmsAmount": 14.14,
"productAmount": 78.56,
"invoiceAmount": 78.56
},
"ibsCbs": {
"basis": 64.42,
"ibs": {
"state": { "amount": 0.06 },
"municipal": { "amount": 0.00 },
"totalAmount": 0.06
},
"cbs": { "amount": 0.58 }
}
},
"transport": { "freightModality": "Free" },
"payment": [
{ "paymentDetail": [ { "method": "WithoutPayment", "amount": 0 } ] }
],
"lastEvents": {
"events": [
{
"type": "Authorized",
"sequence": 5,
"data": {
"accessKey": "35260739425723000178550080000259691818677728",
"description": "Autorizado o uso da NF-e",
"protocolNumber": "135262973194611",
"statusCode": 100,
"environmentType": "Production"
}
},
{
"type": "WebHooksDispatched",
"sequence": 8,
"data": { "action": "product_invoice.issued_successfully" }
}
],
"hasMore": false
},
"apiVersion": 2
}
4.1. Onde está a chave de acesso
A accessKey aparece em dois lugares e ambos são válidos:
authorization.accessKey— o caminho direto, prefira este.lastEvents.events[]no item detype: "Authorized", emdata.accessKey.
const chave =
body.authorization?.accessKey ??
body.lastEvents?.events?.find(e => e.type === "Authorized")?.data?.accessKey;
4.2. lastEvents é o histórico da nota
O array traz o caminho percorrido, cada item com type e sequence (ordem crescente de acontecimento). Tipos observados em produção:
type | Significado |
|---|---|
DefinedNumberAndSerieSuccessfully | Número e série atribuídos |
InvoiceSetAccessKey | Chave de acesso calculada |
InvoiceXmlSigned | XML assinado |
Authorized | Autorizada pela SEFAZ — traz protocolNumber e statusCode |
Merged | XML final consolidado |
SendSignedBatchFailed | Falha no envio do lote (transiente; a nota pode seguir e autorizar) |
WebHooksDispatched | Registro do próprio disparo de webhook |
SendSignedBatchFailed no histórico não significa nota com erroÉ comum ver essa entrada em notas autorizadas com sucesso — houve uma falha transiente e o reenvio funcionou. Considere sempre o status da raiz, não a presença de um evento de falha no histórico.
5. NFC-e — consumer_invoice
Mesma estrutura da NF-e (envelope achatado, issuer/buyer, totals, lastEvents). A diferença prática é o destinatário pessoa física:
{
"id": "8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d",
"serie": 2,
"number": 1572,
"status": "Issued",
"authorization": {
"accessKey": "51260771924245000153650020000015721152114412"
},
"operationNature": "Venda de mercadorias",
"operationType": "Outgoing",
"environmentType": "Production",
"issuer": {
"name": "TESTE LOGISTICA SA",
"federalTaxNumber": 71924245000153,
"taxRegime": "SimplesNacional",
"type": "LegalEntity"
},
"buyer": {
"name": "Maria Silva",
"federalTaxNumber": "50465923046",
"email": "[email protected]",
"stateTaxNumberIndicator": "NonTaxPayer",
"address": {
"postalCode": "78360000",
"city": { "code": "5102637", "name": "Campo Novo do Parecis" },
"state": "MT",
"country": "BRA",
"phone": "11999999999"
},
"type": "NaturalPerson"
},
"totals": {
"icms": {
"productAmount": 22.9,
"freightAmount": 7.5,
"discountAmount": 5.72,
"othersAmount": 0.99,
"invoiceAmount": 25.67
}
},
"payment": [
{
"paymentDetail": [
{ "method": "Others", "methodDescription": "Marketplace Online", "amount": 25.67 }
],
"payBack": 0
}
],
"apiVersion": 2
}
Em NFC-e o buyer costuma ser type: "NaturalPerson", com CPF, e-mail e telefone. Trate o corpo como dado pessoal: não registre em log aberto, restrinja acesso e observe sua política de retenção (LGPD).
6. Regras de serialização que afetam seu parser
Comportamentos reais do serializador — considere todos ao escrever o código:
- Campos nulos são omitidos. A chave simplesmente não vem. Nunca use "chave existe" como sinal; use acesso seguro (
?.,.get()). - O tipo de
federalTaxNumbervaria. Em NF-e/NFC-e costuma vir número (31305761000185); em NFS-e, string. Normalize para string antes de comparar, e cuidado com zero à esquerda: um CNPJ que começa com0perde o dígito se tratado como inteiro. numbertambém varia entre string e inteiro conforme o produto.- Datas em ISO 8601 com offset, às vezes com milissegundos (
2026-07-28T11:55:20.9045977+00:00). Não presuma resolução de segundos. - Enums chegam como string (
"Production"), não como número. - Campos novos podem aparecer sem aviso. Ignore desconhecidos em vez de falhar — os campos
ibsCbsda reforma tributária, por exemplo, foram adicionados a payloads já existentes. - Não presuma ordem de chaves.
7. Sucesso da entrega ≠ sucesso fiscal
O erro mais comum nesta integração:
| Pergunta | Onde responder |
|---|---|
| "Recebi a notificação?" | status HTTP que você retorna |
| "A nota foi emitida?" | flowStatus / status no corpo |
Um evento issued_failed é uma entrega bem-sucedida de uma notícia ruim: responda 2xx, e registre a falha internamente pelo flowMessage.
Retornar erro HTTP faz a NFE.io reenviar o mesmo evento repetidamente. Responda 2xx ao receber e trate o estado fiscal no seu sistema.
8. Checklist de integração
- Normaliza o envelope (
body.payload ?? body) para aceitar NFS-e e NF-e/NFC-e. - Valida a assinatura HMAC antes de processar — veja Dúvidas frequentes.
- Deduplica por
X-Hook-Id(reentrega é esperada). - Responde
2xxrápido e processa de forma assíncrona. - Trata
_successfully,_failede_error. - Confere
environment/environmentTypeantes de gravar em produção. - Acesso seguro a todo campo opcional (nulos são omitidos).
- Normaliza
federalTaxNumberenumberpara string. - Ignora campos desconhecidos.
- Não registra CPF/e-mail/telefone de NFC-e em log aberto.
Próximos passos
- Payloads dos webhooks de documentos recebidos — NF-e, CT-e e NFS-e capturadas de terceiros.
- Dúvidas frequentes — validação de assinatura e cabeçalhos.
- IPs de origem — allowlist de firewall.