Catálogo de eventos de webhook — NFS-e
Esta página documenta os 7 eventos de webhook do eventType service_invoice. O corpo vem sempre no formato {"action": "...", "payload": {...}}: a ação do evento em action e a nota em payload. Veja Payloads de emissão para a regra geral dos dois envelopes.
Diferente de NF-e e NFC-e, o payload de NFS-e não tem array lastEvents. O histórico de tentativas não é exposto — você recebe apenas flowStatus e, em erro, flowMessage.
Política de entrega
- Entrega: at-least-once. Garanta idempotência por
X-Hook-Id. - Retry: reentrega automática em falha de rede ou resposta não-2xx — até 16 tentativas ao longo de ~45 h, exceto nos códigos de recusa definitiva. Veja os números da política de entrega.
- Timeout: responda 2xx rápido e processe de forma assíncrona.
- Assinatura: valide o HMAC do cabeçalho antes de processar. Veja Dúvidas frequentes.
Como issued_error e issued_failed se distinguem
A distinção chega pronta no campo action do corpo: issued_error ou issued_failed. Use esse campo.
Para referência, a NFE.io aplica uma regra simples e literal: se flowMessage começa com o texto "max retry", o evento é issued_failed — a emissão esgotou o número de tentativas. Qualquer outro texto em flowMessage gera issued_error — uma rejeição pontual da prefeitura ou uma falha de comunicação isolada.
No cancelamento, a regra tem um passo antes: se o cancelamento falhou e a nota continua emitida (status: "Issued") — o caso comum de recusa da prefeitura —, o evento é sempre cancelled_failed, qualquer que seja o flowMessage. Nota já com status: "Cancelled" sempre gera cancelled_successfully. Nos demais status vale a regra do texto: flowMessage começando com "max retry" gera cancelled_failed; qualquer outro erro gera cancelled_error.
flowStatusflowStatus é o mesmo (Error ou IssueFailed/CancelFailed) nos dois casos. A distinção entre issued_error e issued_failed chega pronta no campo action do corpo. O cabeçalho X-Hook-Event traz só o tipo (service_invoice).
Eventos de emissão
service_invoice.issued_successfully
Quando dispara: a NFS-e foi emitida e autorizada pela prefeitura.
Payload:
{
"action": "issued_successfully",
"payload": {
"id": "d4911190b46ba44f",
"externalId": "seu-id-externo",
"environment": "Production",
"flowStatus": "Issued",
"provider": {
"tradeName": "Atacado Ferreira & Filhos LTDA",
"taxRegime": "SimplesNacional",
"specialTaxRegime": "MicroempresaMunicipal",
"legalNature": "SociedadeEmpresariaLimitada",
"companyRegistryNumber": 6202300,
"regionalTaxNumber": 355030999,
"municipalTaxNumber": "44338200330345",
"issRate": 0.0,
"id": "265f492ca6f35591",
"name": "Atacado Ferreira & Filhos LTDA",
"federalTaxNumber": 44338200330345,
"email": "contato@atacadoferreira.example.com",
"address": {
"street": "Avenida Central",
"number": "955",
"city": { "code": "3550308", "name": "Sao Paulo" },
"state": "SP",
"postalCode": "39257-113",
"country": "BRA"
},
"status": "Active",
"type": "LegalPerson, Company"
},
"borrower": {
"id": "654b17b903ade39e",
"name": "Carlos Eduardo Lima",
"federalTaxNumber": 30817158650,
"email": "contato842@example.com",
"address": {
"street": "Rua Sete de Setembro",
"number": "291",
"city": { "code": "3304557", "name": "Rio de Janeiro" },
"state": "RJ",
"postalCode": "87858-233",
"country": "BRA"
},
"status": "Active",
"type": "NaturalPerson"
},
"apiVersion": 2,
"issuedOn": "2026-08-17T21:46:14-03:00",
"number": 6909,
"status": "Issued",
"rpsType": "Rps",
"rpsStatus": "Normal",
"taxationType": "WithinCity",
"rpsSerialNumber": "ZZ",
"rpsNumber": 3610,
"cityServiceCode": "5771",
"federalServiceCode": "15.01",
"servicesAmount": 70.00,
"baseTaxAmount": 70.00,
"issRate": 0.02,
"issTaxAmount": 0.0,
"amountNet": 70.00
}
}
Em NFS-e, o federalTaxNumber de provider e de borrower chega como número quando o valor é numérico e a nota foi criada pela API v1/v2 — é o caso deste exemplo (apiVersion: 2). Chega como string quando o valor é alfanumérico ou quando a nota foi criada pela API v3.
Como número, o zero à esquerda some: o CNPJ 09505320001905 chega como 9505320001905. Normalize para string com zeros à esquerda antes de comparar: 14 dígitos para CNPJ, 11 para CPF. Campos nulos são omitidos, não vêm como null.
Idempotency key: payload.id ou o cabeçalho X-Hook-Id.
service_invoice.issued_error
Quando dispara: a prefeitura rejeitou a nota, ou houve falha pontual de comunicação — não é esgotamento de retry.
Payload: mesmo shape de issued_successfully, com:
{
"action": "issued_error",
"payload": {
"flowStatus": "IssueFailed",
"flowMessage": "[1001] XML não compatível com Schema. The 'CodigoServico' element is invalid - The value '040802.001' is invalid according to its datatype '...:tpCodigoServico' - The Pattern constraint failed.",
"status": "Error"
}
}
Mensagens de issued_error variam bastante — vêm de rejeições distintas da prefeitura ou de falha de validação de schema, como no exemplo acima. Trate flowMessage como texto livre para exibição/log, não como valor para parsing por padrão fixo.
flowStatus em issued_error é sempre IssueFailedMesmo quando a ação do webhook é issued_error (não issued_failed), o campo flowStatus no corpo aparece como IssueFailed — é o mesmo estado interno para os dois casos. A distinção entre as duas ações chega pronta no campo action do corpo. Ela segue o prefixo "max retry" de flowMessage, nunca o flowStatus.
Idempotency key: payload.id.
service_invoice.issued_failed
Quando dispara: a NFE.io esgotou as tentativas de comunicação com o webservice da prefeitura. flowMessage sempre começa com "max retry".
Payload: mesmo shape de issued_successfully, com:
{
"action": "issued_failed",
"payload": {
"flowStatus": "IssueFailed",
"flowMessage": "max retry: falha na comunicacao com o webservice da prefeitura apos numero maximo de tentativas",
"status": "IssueFailed"
}
}
Idempotency key: payload.id.
Eventos de cancelamento
service_invoice.cancelled_successfully
Quando dispara: o cancelamento foi homologado pela prefeitura.
Payload: mesmo shape de issued_successfully, com:
{
"action": "cancelled_successfully",
"payload": {
"flowStatus": "Cancelled",
"status": "Cancelled",
"rpsStatus": "Cancelled"
}
}
Idempotency key: payload.id.
service_invoice.cancelled_error
Quando dispara: o cancelamento falhou por um erro pontual (não é esgotamento de retry) e a nota não está com status Issued nem Cancelled. A recusa comum da prefeitura, com a nota ainda emitida, chega como cancelled_failed (veja abaixo).
Payload: mesmo shape base, com:
{
"action": "cancelled_error",
"payload": {
"flowStatus": "CancelFailed",
"flowMessage": "Falha de comunicacao com o webservice da prefeitura",
"status": "Error"
}
}
O exemplo é ilustrativo: status diferente de Issued e de Cancelled é o que separa este evento do cancelled_failed e do cancelled_successfully.
Idempotency key: payload.id.
service_invoice.cancelled_failed
Quando dispara: o cancelamento falhou e a nota continua emitida (status: "Issued") — por exemplo, a prefeitura recusou o pedido —, ou a NFE.io esgotou as tentativas de comunicar o cancelamento (flowMessage começando com "max retry").
Payload: mesmo shape base, com:
{
"action": "cancelled_failed",
"payload": {
"flowStatus": "CancelFailed",
"flowMessage": "Prazo de cancelamento expirado junto a prefeitura",
"status": "Issued"
}
}
Note que status permanece Issued — o cancelamento falhou, a nota continua válida.
Idempotency key: payload.id.
Evento sem exemplo observado: pulled
pulled existe no contrato de eventos de NFS-e, mas não teve nenhuma ocorrência registrada em 180 dias de produção até a publicação desta página. Não documentamos payload de exemplo para não apresentar uma estrutura hipotética como real. Se você assinar este evento e receber uma entrega, entre em contato — vamos atualizar esta página com o caso real.
Como validar a assinatura
Veja o exemplo de validação de HMAC em Dúvidas frequentes.