Webhooks são URLs HTTP do seu servidor que o Brasil NFe chama automaticamente quando algo acontece nas suas notas. Em vez de você ficar consultando a API em loop, é o Brasil NFe que avisa você — enviando um POST com o payload em JSON.
Use webhooks para integrar com ERP, sistema de logística, CRM, dashboards internos ou qualquer coisa que precise reagir a eventos fiscais em tempo (quase) real.
Cadastro pelo painel
O cadastro é feito em api.brasilnfe.com.br → Painel → Webhooks:
- Clique em Adicionar webhook.
- Informe um nome (ex.: "Integração ERP") e a URL que vai receber os POSTs (precisa ser HTTPS em produção).
- Salve. O secret é gerado uma única vez — copie e guarde no seu cofre de segredos. Se perder, use Regerar secret (a integração antiga deixa de funcionar imediatamente).
- Em Empresa → Credenciais, selecione qual webhook esta empresa específica utiliza.
Resolução por empresa
Para cada disparo, o Brasil NFe escolhe o webhook assim:
- Se a empresa tem um webhook explicitamente vinculado (campo
IdWebhook), ele é usado. - Senão, se existir exatamente um webhook ativo sob a mesma conta, ele é usado como fallback.
- Se houver mais de um ativo e nenhum explicitamente vinculado, nenhum disparo acontece — a configuração é ambígua.
O envelope do payload
Cada disparo é um POST com Content-Type: application/json e o seguinte envelope:
Code
O envelope (
event,deliveryId,timestamp,data) é sempre camelCase. O conteúdo dedatasegue o casing da entidade no domínio: eventos de lote (nfe.lote.*) usam camelCase; eventos de documento de entrada (documento.entrada.*) usam PascalCase (ver schema de cada evento na seção Eventos suportados).
| Campo | Descrição |
|---|---|
event | Nome do evento (ex.: nfe.lote.finalizado). Veja a tabela de eventos. |
deliveryId | Identificador único da tentativa de entrega. Use como chave de idempotência no seu lado. |
timestamp | ISO-8601 em UTC, momento em que o disparo foi montado. |
data | Carga específica do evento. |
Headers enviados
| Header | Conteúdo |
|---|---|
User-Agent | BrasilNFe-Webhooks/1.0 |
X-Webhook-Event | Nome do evento (mesmo valor de event no body). |
X-Webhook-Delivery | Mesmo valor de deliveryId. Idêntico em todas as tentativas do mesmo evento. |
X-Webhook-Timestamp | Mesmo valor de timestamp. Reflete o momento em que o disparo original foi montado, não a tentativa atual. |
X-Webhook-Signature | sha256=<hex> — HMAC-SHA256 do body bruto com o secret do webhook. |
X-Webhook-Attempt | Número da tentativa atual (1 no disparo original, 2..5 em retentativas). |
Headers extras por evento
Eventos de documento de entrada (documento.entrada.recebida, documento.entrada.cancelada) carregam dois headers adicionais para facilitar roteamento sem precisar parsear o body:
| Header | Conteúdo |
|---|---|
X-Document-Model | Modelo fiscal — 10 (NFS-e), 55 (NF-e), 57 (CT-e). |
X-Document-Chave | Chave de acesso (44 dígitos para NF-e/CT-e; código de verificação para NFS-e quando aplicável; pode vir vazio se a nota ainda não tem chave). |
Verificação da assinatura
A assinatura é o HMAC-SHA256 do corpo bruto da requisição, codificada em hexadecimal e prefixada por sha256=. Sempre compare em tempo constante para evitar timing attacks.
Code
Importante. Calcule o HMAC sobre o body exatamente como recebido — sem reserializar o JSON. Frameworks que parseiam o body antes de você ler costumam reordenar chaves e quebram a assinatura. No Express, use
express.raw(); no ASP.NET, leia o stream antes do model binding.
Idempotência
Use o X-Webhook-Delivery (= deliveryId no body) como chave de idempotência com TTL de pelo menos 24h. Em caso de retentativas (veja abaixo) ou raros timeouts dos dois lados, o mesmo deliveryId pode chegar mais de uma vez — armazene-o e processe apenas a primeira ocorrência.
A idempotência via
deliveryIdé a defesa primária. O Brasil NFe garante que o mesmo evento sempre carrega o mesmodeliveryId, em todas as tentativas.
Janela anti-replay (defesa secundária)
Como o X-Webhook-Timestamp reflete o momento do disparo original, e retentativas podem chegar até ~1h12min depois, use uma janela de ±2 horas se quiser validar timestamp como camada adicional. A proteção primária contra replay deve ser a idempotência por deliveryId.
Resposta esperada
Seu endpoint deve responder 2xx em até 15 segundos. Qualquer outra coisa — erro HTTP (4xx/5xx), timeout, exceção de rede — marca a tentativa como falha. A entrega é então re-tentada automaticamente seguindo a tabela de backoff abaixo.
Se você precisa de garantia de entrega no seu lado, devolva 2xx imediatamente ao receber e processe de forma assíncrona do seu lado (fila interna). É o padrão recomendado.
Retentativas e backoff
O Brasil NFe faz até 5 tentativas de entrega para cada evento, com backoff exponencial:
| Tentativa | Quando dispara | Header X-Webhook-Attempt |
|---|---|---|
| 1 | Imediatamente, no momento do evento. | 1 |
| 2 | 30 segundos após a tentativa 1 falhar. | 2 |
| 3 | 2 minutos após a tentativa 2 falhar. | 3 |
| 4 | 10 minutos após a tentativa 3 falhar. | 4 |
| 5 | 1 hora após a tentativa 4 falhar. | 5 |
Tempo total máximo: ~1h12min entre o evento e a quinta (e última) tentativa. Após a quinta tentativa falhar, o evento é considerado perdido — mas todas as tentativas ficam registradas em Painel → Webhooks → Logs para auditoria.
O que se mantém entre tentativas
deliveryId— idêntico em todas as tentativas. Use para idempotência.event— idêntico.- Body completo — byte-a-byte idêntico (logo, a
X-Webhook-Signaturetambém é idêntica). X-Webhook-Timestamp— reflete o disparo original, não muda.
O que muda entre tentativas
X-Webhook-Attempt— incrementa de 1 a 5.- URL e secret — usam a configuração atual no momento da tentativa. Se você atualizou o webhook entre o disparo original e a retentativa, a próxima tentativa vai para a nova URL com o novo secret. Webhook desativado entre tentativas → não há mais retentativas.
Eventos de teste não são re-tentados
O evento test.ping (botão "Testar" no painel) nunca é re-tentado — é uma checagem manual e o resultado é mostrado no painel imediatamente.
Eventos suportados
Mais eventos serão adicionados gradualmente. O nome do evento é estável — quando um evento entra na lista, o nome não muda.
test.ping
Disparado pelo botão Testar no painel. Nunca é re-tentado (resultado mostrado imediatamente no painel).
Headers extras: nenhum. Casing: camelCase.
| Campo | Tipo | Descrição |
|---|---|---|
test | boolean | Sempre true neste evento. |
message | string | Mensagem informativa. |
sentAt | datetime | Momento do envio (ISO-8601 em UTC). |
nfe.lote.finalizado
Disparado quando todas as notas de um envio em lote via /EnviarNotaFiscalLote receberam resposta da SEFAZ (sucesso ou erro). É o evento de fechamento do lote — uma única notificação por lote, independente de quantas notas ele tem.
Headers extras: nenhum. Casing: camelCase.
| Campo | Tipo | Descrição |
|---|---|---|
codLote | string | Identificador do lote no Brasil NFe. |
tipoAmbiente | int | 1 = produção, 2 = homologação. |
modeloDocumento | int | 55 (NF-e) ou 65 (NFC-e). |
status | int | 4 = lote finalizado com ao menos uma nota emitida; 5 = lote finalizado sem nenhuma nota emitida. |
qtdTotal | int | Total de notas no lote. |
qtdEmitida | int | Quantas foram autorizadas (codStatus 100 ou 150). |
qtdErro | int | qtdTotal - qtdEmitida. |
notas[] | array | Uma entrada por nota do lote (ver schema abaixo). |
Cada item de notas[]:
| Campo | Tipo | Descrição |
|---|---|---|
id | long | Id interno da nota no Brasil NFe. |
numero | long | Número da nota. |
serie | int | Série. |
chaveAcesso | string | Chave de 44 dígitos (vazio quando a nota não foi autorizada). |
numeroProtocolo | string | Protocolo SEFAZ (vazio em caso de erro). |
codStatus | int | Código de status SEFAZ (ex.: 100 autorizado, 150 autorizado fora do prazo). |
dsStatus | string | Descrição do status SEFAZ. |
error | string | null | Mensagem de erro quando a nota não foi autorizada. |
documento.entrada.recebida
Disparado quando uma nova nota de entrada é detectada para o CNPJ da empresa: NF-e/CT-e via manifestação automática na SEFAZ, ou NFS-e capturada pelo scraping do portal nacional/da prefeitura. Mesmo que a nota já chegue cancelada, o evento de criação é sempre recebida — o consumidor identifica pelo campo Status do payload.
Headers extras: X-Document-Model, X-Document-Chave (ver acima). Casing: PascalCase.
| Campo | Tipo | Descrição |
|---|---|---|
Chave | string | Chave de acesso (44 dígitos para NF-e/CT-e; código de verificação para NFS-e). |
IdentificadorInterno | string | Identificador interno do emissor (apenas NFS-e, quando disponível). |
CodLote | string | Código do lote (apenas NFS-e). |
Numero | long | Número da nota (extraído da chave para NF-e/CT-e). |
ModeloDocumento | int | 10 (NFS-e), 55 (NF-e), 57 (CT-e). |
Valor | decimal | Valor total da nota. |
ValorIcms | decimal | null | Valor de ICMS (quando aplicável). |
CnpjEmissor | string | CNPJ/CPF do emissor da nota. |
NomeEmissor | string | Razão social/nome do emissor. |
IeEmissor | string | null | Inscrição estadual do emissor (apenas NF-e/CT-e). |
CnpjDestinatario | string | CNPJ da empresa destinatária (a sua empresa). |
NomeDestinatario | string | Razão social da destinatária. |
NumeroProtocolo | string | Protocolo de autorização SEFAZ. |
Cfops | string | CFOPs presentes nos itens (separados por vírgula). |
DigestValue | string | Digest do XML assinado / código de verificação NFS-e. |
Status | int | 1 = autorizado, 2 = cancelado, 3 = uso denegado. |
DtEmissao | datetime | Data de emissão (ISO-8601). |
DtRecebimento | datetime | Quando o Brasil NFe identificou e armazenou a nota. |
documento.entrada.cancelada
Disparado quando uma nota de entrada previamente recebida (NF-e modelo 55 ou CT-e modelo 57) é marcada como cancelada — gatilho é o registro do evento SEFAZ 110111 (Cancelamento) associado à chave da nota.
Cancelamentos de NFS-e não passam por aqui: o cancelamento de NFS-e do portal nacional vem como nova varredura e dispara
documento.entrada.recebidacomStatus: 2(se a nota ainda não estava cadastrada). Notas NFS-e que já existiam no Brasil NFe não recebem evento adicional.
Headers extras: X-Document-Model, X-Document-Chave (ver acima). Casing: PascalCase.
O schema dos campos é idêntico ao de documento.entrada.recebida. A única diferença prática é o campo Status, que neste evento sempre vem como 2 (cancelado).
Code
Boas práticas de segurança
- Use uma URL dedicada ao webhook (ex.:
/webhooks/brasilnfe) — não compartilhe com outros endpoints públicos. - Sempre HTTPS em produção. Endereços HTTP planos serão chamados, mas o secret e o payload trafegam em claro.
- Valide a assinatura antes de qualquer parsing custoso ou consulta a banco.
- Guarde o secret em variável de ambiente ou cofre (Vault, AWS Secrets Manager, etc.), nunca no código.
- Se o secret vazou, regere imediatamente pelo painel — o secret antigo é invalidado na hora.
Teste local
Para testar sem precisar gerar uma nota real:
- No painel, abra Webhooks e clique no ícone de avião (Testar) ao lado do webhook.
- O Brasil NFe envia um POST com
event: "test.ping"e payload mínimo. - Veja o resultado imediatamente (HTTP status, duração) e o request/response completo em Logs.
Para desenvolvimento local atrás de NAT, use um túnel como ngrok ou localtunnel e cadastre a URL pública gerada.

