---
title: "Catálogo de eventos do webhook — NFS-e Inbound"
description: "Catálogo dos 6 eventos do webhook NFS-e Inbound: envelope, payload, schema do document (amounts, taxes, IBS/CBS), validação HMAC (x-hub-signature + SHA1), política de retry e idempotência."
source_url: https://nfe.io/docs/distribuicao-nfse-inbound-webhook/
last_updated: 2026-07-30
---

# Catálogo de eventos do webhook — NFS-e Inbound

A NFE.io envia **HTTP POST** ao endpoint configurado em `webhookUrl` da empresa sempre que algo relevante acontece no subsistema NFS-e Inbound. Este documento cataloga os **6 eventos** disponíveis, o shape do envelope, a política de retry e os mecanismos de segurança e idempotência.

Para **desenvolvedores cliente** que vão implementar o handler do webhook. Para visão sistêmica do fluxo, leia [Arquitetura](../explanation/arquitetura.md) primeiro.

## Sumário

- [Política de entrega](#política-de-entrega)
- [Eventos](#eventos)
- [Schema do objeto `document`](#schema-do-objeto-document)
- [Validação HMAC](#validação-hmac)
- [Idempotência](#idempotência)

## Política de entrega

- **Entrega:** at-least-once — seu handler **deve ser idempotente** (ver seção dedicada).
- **Method:** `POST` com `Content-Type: application/json; charset=utf-8`.
- **Timeout:** 30 segundos para sua resposta.
- **Retry:** até **50 tentativas em 24 horas** com backoff exponencial (30s → 60s → 120s → ... → max 7200s) e jitter de ±20%.
- **Códigos:** `2xx` confirma entrega; `4xx` (exceto `408`/`429`) marca `DefinitivelyFailed` sem retry; `408`/`429`/`5xx` ou timeout disparam retry.
- **Assinatura:** header `x-hub-signature` com `HMAC-SHA1` do body bruto, formato `sha1=<HEX>` — ver [Validação HMAC](#validação-hmac).

Após esgotar as tentativas, o documento permanece consultável via API. Reenvie manualmente com `POST /v2/companies/{companyId}/inbound/nfse/{id}/resend-webhook`.

## Eventos

Todos os payloads são embrulhados num envelope `{ "body": { ... } }`. Os exemplos abaixo mostram apenas o conteúdo de `body` para clareza.

| Evento (`body.eventName`) | Quando dispara | NSU novo? |
|---|---|---|
| `inbound.serviceInvoice.received` | NFS-e, DPS, EventRegistrationRequest, CNC ou documento desconhecido capturado do ADN | Sim |
| `inbound.serviceInvoice.event.received` | Evento de NFS-e (cancelamento, ciência, rejeição, ato de ofício) capturado do ADN | Sim |
| `inbound.manifestation.submitted` | Resultado da sua submissão de Ciência/Rejeição (callback assíncrono) | Não |
| `inbound.company.deactivated` | Empresa desativada automaticamente (circuit breaker) | Não |
| `inbound.document.failed` | Falha no processamento de um documento (XML corrompido, parse error) | Não |
| `inbound.company.event` | Eventos operacionais (rate limit, reativação, consolidação) | Não |

:::warning Vocabulário atualizado em 2026-05-08
Os campos `body.action` (legacy snake_case `issued_successfully` / `event_raised_successfully`) continuam sendo enviados para retrocompatibilidade. **Novas integrações devem filtrar por `body.eventName`** (dotted). O `type` emite `serviceInvoice` para documentos e `serviceInvoiceEvent` para eventos (esse vocabulário não varia por versão do payload — veja [Versionamento de webhook](versionamento-webhook.md) para o que de fato muda entre v1 e v2).
:::

### `inbound.serviceInvoice.received`

Disparado quando um documento NFS-e, DPS, EventRegistrationRequest, CNC ou `unknown` é capturado do ADN. O `type` (na raiz do envelope) discrimina a variante.

```json
{
  "body": {
    "eventName": "inbound.serviceInvoice.received",
    "action": "issued_successfully",
    "type": "serviceInvoice",
    "companyId": "54244e0ee340420fdc94ad10",
    "accountId": "acc-123",
    "document": {
      "id": "66b2c3d4e5f6a7890bcdef12",
      "direction": "Received",
      "nsu": 12345,
      "accessKey": "35250612345678000190000001234567890123451234",
      "generatedOn": "2026-05-13T14:42:15Z",
      "issuedOn": "2026-05-13",
      "provider": { "federalTaxNumber": "12345678000190", "name": "Fornecedor LTDA", "cityCode": "3550308", "state": "SP" },
      "borrower": { "federalTaxNumber": "98765432000110", "name": "Sua Empresa LTDA", "cityCode": "3106705", "state": "MG" },
      "servicesAmount": 1500.00,
      "federalServiceCode": "010501",
      "cityServiceCode": null,
      "amounts": { "deductionsAmount": 0, "amountNet": 1500.00 },
      "taxes": { "issqn": { "base": 1500.00, "rate": 2.00, "amount": 30.00 } },
      "environment": "Production",
      "xmlUrl": "https://api.nfse.io/v2/.../inbound/nfse/66b2.../xml",
      "pdfUrl": "https://api.nfse.io/v2/.../inbound/nfse/66b2.../pdf"
    }
  }
}
```

**Idempotência:** `(body.companyId, body.document.nsu)` ou `body.document.id`.

### `inbound.serviceInvoice.event.received`

Disparado quando um evento de NFS-e é capturado (cancelamento, ciência, rejeição, ato de ofício). Carrega `type = "serviceInvoiceEvent"` (na raiz do envelope) e os campos `eventCode` (`tpEvento` raw) e `eventType` (legível, pode vir `null` para códigos novos).

```json
{
  "body": {
    "eventName": "inbound.serviceInvoice.event.received",
    "action": "event_raised_successfully",
    "type": "serviceInvoiceEvent",
    "companyId": "54244e0ee340420fdc94ad10",
    "document": {
      "id": "69f3f9f0731bd9db773ba624",
      "eventType": "Cancellation",
      "eventCode": "101101",
      "nsu": 12346,
      "accessKey": "35250612345678000190...",
      "generatedOn": "2026-05-13T15:00:00Z",
      "environment": "Production",
      "xmlUrl": "https://api.nfse.io/v2/.../inbound/nfse/69f3.../xml",
      "pdfUrl": null
    }
  }
}
```

A tabela completa de `eventCode` está em [Códigos de evento](codigos-evento.md) — atenção aos **dois contextos distintos** (XSD `tpEvento` vs CSV `EventCode` do bulk export).

**Campos fiscais sempre `null` em eventos:** `provider`, `borrower`, `servicesAmount`, `amounts`, `taxes`, `pdfUrl`. Para obter esses dados, consulte a NFS-e hospedeira via `document.accessKey`.

### `inbound.manifestation.submitted`

Callback do fluxo assíncrono de manifestação. Disparado quando o evento de Ciência (`203202`) ou Rejeição (`203206`) que você submeteu via API atinge estado terminal no SEFIN Nacional.

```json
{
  "body": {
    "eventName": "inbound.manifestation.submitted",
    "companyId": "54244e0ee340420fdc94ad10",
    "manifestation": {
      "id": "69e193685b7241b5c4fe8e3c",
      "accessKey": "35503081223301943000745000000002734526036434454892",
      "eventCode": 203202,
      "reasonCode": null,
      "status": "Accepted",
      "submittedAt": "2026-04-17T02:47:08Z",
      "acceptedAt": "2026-04-17T02:47:11Z",
      "errorCode": null,
      "errorMessage": null
    }
  }
}
```

Status terminais: `Accepted`, `Rejected`, `Failed`. Quando `Rejected`/`Failed`, `errorCode` traz o `cStat` SEFIN (135 OK, 217 NFS-e não consta, 573 duplicidade etc.) e `errorMessage` o `xMotivo`.

### `inbound.company.deactivated`

Disparado quando uma empresa é desativada automaticamente pelo circuit breaker da NFE.io. Ação imediata recomendada — sem reativação, nenhum documento novo será capturado.

```json
{
  "body": {
    "eventName": "inbound.company.deactivated",
    "companyId": "54244e0ee340420fdc94ad10",
    "company": {
      "id": "54244e0ee340420fdc94ad10",
      "name": "Empresa Teste LTDA",
      "isActive": false,
      "deactivationReason": "CertificateExpired",
      "deactivatedAt": "2026-04-05T10:00:00Z"
    }
  }
}
```

`deactivationReason` possíveis: `CertificateNotFound`, `CertificateExpired`, `CertificateRejected`, `CompanyNotAuthorized`. Veja [Troubleshooting](troubleshooting.md#razões-de-desativação-automática-deactivationreason) para causa e ação recomendada de cada um.

### `inbound.document.failed`

Disparado quando o processamento de um documento falha de forma irrecuperável (XML corrompido na origem, root element desconhecido, GZip inválido).

```json
{
  "body": {
    "eventName": "inbound.document.failed",
    "companyId": "54244e0ee340420fdc94ad10",
    "document": {
      "id": "6621a3f2e340420fdc000002",
      "type": "None",
      "nsu": 43,
      "accessKey": null,
      "generatedOn": "2026-04-05T10:01:00Z",
      "environment": "Production",
      "xmlUrl": null,
      "pdfUrl": null
    }
  }
}
```

O XML **não fica armazenado** para documentos com falha. Casos recorrentes para o mesmo `companyId` indicam problema sistêmico — abra chamado no suporte com o `companyId` e `nsu`.

### `inbound.company.event`

Notificações operacionais de baixa prioridade (rate limit ativado, reativação automática, consolidação de lote). Use para monitoramento — não exige ação na maioria dos casos.

```json
{
  "body": {
    "eventName": "inbound.company.event",
    "companyId": "54244e0ee340420fdc94ad10",
    "eventType": "RateLimited",
    "occurredAt": "2026-04-17T04:00:00Z",
    "description": "ADN retornou 429 com Retry-After=3600; captura pausada até 2026-04-17T05:00:00Z."
  }
}
```

## Schema do objeto `document`

Para `type=serviceInvoice` (NFS-e autorizada), os blocos `amounts`, `taxes` e `taxes.ibsCbs` vêm populados:

| Bloco | Campos principais | Observações |
|---|---|---|
| `document.amounts` | `discountUnconditionedAmount`, `discountConditionedAmount`, `deductionsAmount`, `municipalBenefit{type,amount}`, `reimbursement`, `amountNet` | Componentes monetários **não-tributários**. Só `discountUnconditionedAmount` e `deductionsAmount` reduzem a BC do ISSQN. |
| `document.taxes.issqn` | `situationCode`, `retentionType`, `base`, `rate`, `amount` | ISSQN municipal. `retentionType=1` prestador recolhe, `2` retido pelo tomador, `3` retido pelo intermediário. |
| `document.taxes.federal` | `irAmountWithheld`, `inssAmountWithheld`, `pisDueAmount`, `cofinsDueAmount`, `socialContributions{retentionType,withheldAmount}` | **NT007/2026:** `pisDueAmount`/`cofinsDueAmount` são DEVIDOS (não retidos). Retenções consolidadas em `socialContributions.withheldAmount`. |
| `document.taxes.ibsCbs` | `situationCode`, `classCode`, `basis`, `ibs.{state,municipal,totalAmount}`, `cbs.{rate,effectiveRate,amount}` | **Reforma Tributária do Consumo.** IBS repartido UF+Município. Vem `null` em NFS-e pré-RTC. |

Garantia de shape: campos top-level do `document` aparecem sempre, mesmo quando `null` (parser strongly-typed seguro). Esta garantia **não se estende a sub-campos** — valide o pai antes de acessar (`if (doc.amounts) { doc.amounts.amountNet }`).

## Validação HMAC

A NFE.io assina cada POST com **HMAC-SHA1** sobre o body bruto. Validar a assinatura é **obrigatório** — sem isso, qualquer um que descubra a URL do seu endpoint pode forjar requisições.

| Item | Valor |
|---|---|
| **Header** | `x-hub-signature` (lowercase) |
| **Algoritmo** | HMAC-SHA1 |
| **Encoding** | hex **MAIÚSCULO**, sem separadores |
| **Formato** | `sha1=<HEX>` (prefixo obrigatório, 45 chars total) |
| **Conteúdo assinado** | Body bruto UTF-8, exatamente como recebido |
| **Secret** | String ASCII de 32 a 64 caracteres, configurada na subscrição |

### Vetor de teste

```text
Secret: SuperSecretWebhookKey12345678901
Body:   {"event":"test","id":"abc123","amount":42.00}
→ Header: sha1=502BC91DE70F6802FC16CD2E599A9AF752064FE5
```

Se sua implementação não gera exatamente esse hash com esses inputs, ela tem um bug — corrija antes de testar contra webhook real.

### Exemplo (Python/Flask)

```python
import hmac, hashlib
from flask import request, abort

WEBHOOK_SECRET = b"<seu-segredo-32-a-64-chars>"

@app.post("/webhook/nfse")
def receive():
    sig = request.headers.get("x-hub-signature", "")
    if not sig.startswith("sha1="):
        abort(401)
    expected = "sha1=" + hmac.new(
        WEBHOOK_SECRET, request.get_data(cache=True), hashlib.sha1
    ).hexdigest().upper()
    if not hmac.compare_digest(expected, sig):
        abort(401)
    # processar request.json["body"]
    return "", 200
```

**Use comparação `timing-safe`** (`hmac.compare_digest` em Python, `crypto.timingSafeEqual` em Node, `CryptographicOperations.FixedTimeEquals` em C#) — comparação byte-a-byte vaza informação por análise de tempo.

### Documentação completa

A NFE.io mantém o **catálogo canônico de validação HMAC** em [nfe/shared-webhook-api/docs/webhook-signature-validation.md](https://github.com/nfe/shared-webhook-api/blob/main/docs/webhook-signature-validation.md) (PT/EN/ES, 13 seções). Lá você encontra:

- 5 implementações de referência completas (Node.js, Python, PHP, C#, Go)
- Passo-a-passo de validação com comparação em tempo constante
- Troubleshooting (10 causas reais de falha em produção)
- Política de rotação do secret com janela de transição
- Headers adicionais que acompanham o webhook (`Content-MD5`, `X-Hook-Id`, `X-Hook-Attempts`, `X-Hook-Event`)

:::warning Não confunda autenticação com integridade
`x-hub-signature` (HMAC) **autentica** — prova que o webhook veio da NFE.io e não foi adulterado.

`Content-MD5` apenas **detecta corrupção** em trânsito — qualquer atacante pode calcular o MD5 do payload forjado dele. **MD5 ≠ autenticação.**
:::

### Compatibilidade legada

Integrações em `Version = raw-payload` ou `v1` recebem **também** o header `X-NFEIO-Signature` com formato `sha1=<base64>` (HMAC-SHA1 em base64, mantido para retrocompatibilidade). Novas integrações devem usar exclusivamente `x-hub-signature`.

## Idempotência

Webhooks podem chegar **mais de uma vez** após retry. Seu handler deve deduplicar:

- **Para documentos** (`serviceInvoice.received`, `serviceInvoice.event.received`, `document.failed`): use o par `(body.companyId, body.document.nsu)` ou `body.document.id` como chave única no seu banco.
- **Para manifestações** (`manifestation.submitted`): use `body.manifestation.id`.
- **Para eventos de empresa** (`company.deactivated`, `company.event`): use `(body.companyId, body.occurredAt)` ou aceite duplicação benigna (apenas re-aplique o estado).

**Não use `nsu` puro como chave** — o NSU é único por empresa, não globalmente. Sempre combine com `companyId`.

Boas práticas adicionais:

- **Responda rápido**: retorne `2xx` em até 5 segundos e processe assincronamente (fila interna).
- **Persista o payload antes** de processar — não perca dados em crash do consumer.
- **Baixe XML/PDF cedo** — URLs assinadas no `xmlUrl`/`pdfUrl` expiram em 30 minutos.
- **Monitore HTTP 5xx no seu endpoint** — detecte problemas antes que o orçamento de retry esgote.

## Veja também

- [Arquitetura do NFS-e Inbound](../explanation/arquitetura.md) — fluxo SEFIN → ADN → webhook + pontos de falha
- [Integração via REST](../how-to/integracao-rest.md) — como ativar a empresa e configurar `webhookUrl`
- [Receita — Manifestação do tomador](../how-to/manifestacao-tomador.md) — submeter Ciência/Rejeição e tratar o callback `manifestation.submitted`
- [Troubleshooting](troubleshooting.md) — `deactivationReason`, `cStat` SEFIN, `webhookStatus=DefinitivelyFailed`
- [Códigos de evento](codigos-evento.md) — XSD `tpEvento` vs CSV `EventCode`
- [Mapa cross-repos](../99-mapa-cross-repos.md) — `nfe/shared-webhook-api` como fonte canônica do HMAC
