---
title: "Webhook events + validação HMAC — NFe/CTe Inbound"
description: "Catálogo dos eventos do webhook NFe/CTe Inbound (product_invoice_inbound, transportation_invoice_inbound), payload e contrato de validação HMAC-SHA1 (x-hub-signature)."
source_url: https://nfe.io/docs/distribuicao-nfe-cte-webhook/
last_updated: 2026-07-30
---

# Webhook events + validação HMAC

A NFE.io envia **HTTP POST** ao endpoint configurado em `webhookUrl` da empresa sempre que um documento NF-e ou CT-e novo (ou evento associado) é capturado do **Ambiente Nacional da SEFAZ**. Este documento cataloga os eventos, o shape do payload, a política de entrega e os mecanismos de segurança.

## Sumário

- [Política de entrega](#política-de-entrega)
- [Eventos](#eventos)
- [Formato do Payload](#formato-do-payload)
- [Fluxo end-to-end](#fluxo-end-to-end)
- [Validação HMAC](#validação-hmac)
- [Idempotência](#idempotência)

## Política de entrega

- **Entrega:** at-least-once — seu handler **deve ser idempotente**.
- **Method:** `POST` com `Content-Type: application/json; charset=utf-8`.
- **Timeout:** 30 segundos para sua resposta.
- **Retry:** até 24 horas com backoff exponencial.
- **Códigos:** `2xx` confirma entrega; `4xx` (exceto `408`/`429`) marca falha definitiva 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/productinvoices/{access_key}/processwebhook`.

## Eventos

Quando um documento é recebido, fazemos um `POST` para sua URL. O tipo de evento chega em dois lugares: o header **`X-Hook-Event`** identifica a área (NF-e ou CT-e) e o campo **`body.action`** identifica a ação específica.

| `X-Hook-Event` | `body.action` | Quando ocorre |
|---|---|---|
| `product_invoice_inbound` | `issued_successfully` \| `outbound_successfully` | NF-e recebida (ou emitida pela própria conta, se você optou por recebê-las) |
| `product_invoice_inbound` | `input_event_raised_successfully` | Evento de manifestação do destinatário (Ciência, Confirmação, Desconhecimento, Operação não Realizada) |
| `product_invoice_inbound` | `event_raised_successfully` | Outro evento de NF-e (cancelamento, CC-e, EPEC, eventos NT 2025.002) |
| `product_invoice_inbound_summary` | mesmos valores acima | Variante resumida (sem XML completo) do mesmo evento |
| `transportation_invoice_inbound` | `issued_successfully` \| `outbound_successfully` | CT-e recebido (ou emitido pela própria conta) |
| `transportation_invoice_inbound` | `event_raised_successfully` | Evento de CT-e |

## Formato do Payload

O corpo é sempre embrulhado num envelope `{"body": {...}}`. Exemplo de NF-e recebida (`X-Hook-Event: product_invoice_inbound`, `body.action: issued_successfully`):

```json
{
  "body": {
    "action": "issued_successfully",
    "accountId": "5f9a1b2c3d4e5f6a7b8c9d0e",
    "accessKey": "35240112345678000195550010000012341234567890",
    "createdOn": "2024-03-15T14:22:10Z",
    "parentAccessKey": "",
    "company": {
      "id": "comp_123",
      "federalTaxNumber": "98765432000100"
    },
    "issuer": {
      "federalTaxNumber": "12345678000195",
      "name": "Fornecedor LTDA"
    },
    "buyer": {
      "federalTaxNumber": "98765432000100",
      "name": "Minha Empresa S.A."
    },
    "links": {
      "xml": "https://api.nfe.io/v2/companies/comp_123/inbound/nfe/35240112345678000195550010000012341234567890/xml",
      "pdf": "https://api.nfe.io/v2/companies/comp_123/inbound/nfe/35240112345678000195550010000012341234567890/pdf"
    },
    "blobUrl": "comp_123/2024/03/35240112345678000195550010000012341234567890.xml",
    "type": "productInvoice",
    "nsu": "21825",
    "nsuParent": "",
    "nfeNumber": "1234",
    "nfeSerialNumber": "1",
    "issuedOn": "2024-03-15T10:00:00Z",
    "description": "Autorizado o uso da NF-e",
    "totalInvoiceAmount": "1500.00",
    "operationType": "Incoming",
    "environmentType": 1,
    "direction": "Received"
  }
}
```

Note que `totalInvoiceAmount` é **string**, e o identificador da empresa vem em `company` (objeto), não em um campo solto `companyId`.

Para CT-e, os campos `issuer` (transportadora) e `taker` (tomador do serviço) substituem `issuer`/`buyer`. Para eventos (`body.action` = `event_raised_successfully` ou `input_event_raised_successfully`), o `body` carrega também o `eventCode` e o tipo de evento associado.

## Fluxo end-to-end

```mermaid
sequenceDiagram
    participant SEFAZ
    participant NFEIO as nfe.io
    participant Voce as Seu sistema
    SEFAZ-->>NFEIO: Documento autorizado (polling NSU)
    NFEIO->>NFEIO: Parse XML + persist
    NFEIO->>Voce: POST webhook (x-hub-signature)
    Voce->>Voce: Valida HMAC + processa
    Voce-->>NFEIO: HTTP 200 (em ≤5s)
    NFEIO->>NFEIO: Confirma entrega
```

## 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/nfeio")
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"] — veja X-Hook-Event / body["action"]
    return "", 200
```

**Use comparacã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

Catálogo canônico (PT/EN/ES, 13 seções, 5 implementações de referência, troubleshooting, rotação de secret): https://github.com/nfe/shared-webhook-api/blob/main/docs/webhook-signature-validation.md

:::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.**
:::

## Idempotência

Webhooks podem chegar **mais de uma vez** após retry. Seu handler deve deduplicar usando `(companyId, accessKey)` ou `(companyId, nsu)` como chave única.

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 em `links.xml` expiram em 1 hora.
- **Monitore HTTP 5xx no seu endpoint** — detecte problemas antes que o orçamento de retry esgote.

## Veja também

- [Configurar webhook](../how-to/configurar-webhook.md)
- [Endpoints de NF-e](./endpoints-nfe.md)
- [Endpoints de CT-e](./endpoints-cte.md)
- [Códigos HTTP e tratamento de erros](./http-errors.md)
- [Conceitos: Arquitetura](../explanation/arquitetura.md)
