Webhooks
Catálogo de eventos, cabeçalhos de entrega, assinatura HMAC-SHA256, payloads por tipo de documento, retentativas e circuit breaker dos webhooks da TF Fiscal.
Modelo de entrega
A plataforma envia os resultados dos documentos e os veredictos de verificação ao seu servidor, sem necessidade de polling.
- Uma URL de callback por aplicação, registrada em Registrar webhook. Chamar o endpoint novamente sobrescreve a URL e o
token. Os resultados de NF-e, CT-e e DC-e e o veredicto da verificação de NF-e são todos entregues nessa URL. - Entrega ao menos uma vez (at-least-once). Uma retentativa após timeout pode chegar mesmo que a tentativa original tenha sido processada, e o mesmo evento pode ser distribuído a vários destinos. Deduplique pelo id do evento (cabeçalho
X-Tffiscal-Event-Ide, nos payloads com envelope,event_id): ele é idêntico em todas as tentativas do mesmo evento. - Cada entrega é um HTTP
POSTcom corpo JSON (Content-Type: application/json; charset=utf-8) para a URL registrada. - Documentos de teste e de produção seguem o mesmo contrato, a mesma assinatura e o mesmo cronograma de retentativas, veja Ambientes.
Catálogo de eventos
| Evento | Disparado quando | Payload |
|---|---|---|
invoice.authorized | NF-e autorizada pela SEFAZ (cStat 100) | Objeto de resultado NF-e, nfeStatus = Autorizada |
invoice.rejected | NF-e rejeitada pela SEFAZ | Objeto de resultado NF-e, nfeStatus = Negada, motivo em nfeMotivoStatus |
invoice.canceled | Cancelamento da NF-e registrado na SEFAZ (cStat 135) | Envelope de evento, data traz invoice_id / chave / protocolo |
invoice.cce.registered | Carta de correção (CC-e) registrada em uma NF-e | Envelope de evento, data traz invoice_id / chave / n_seq. Reservado: o registro da CC-e é síncrono hoje e este evento ainda não é entregue |
invoice.verify.completed | A verificação de NF-e chegou ao veredicto final (VALIDATED / REJECTED / VALIDATION_ERROR) | Envelope de evento com o veredicto em data |
cte.authorized | CT-e autorizado (cStat 100) | Objeto de resultado CT-e, cteStatus = Autorizada |
cte.rejected | CT-e rejeitado pela SEFAZ (cStat diferente de 100) | Objeto de resultado CT-e, cteStatus = Negada, cteMotivoStatus = cStat - motivo |
cte.canceled | Cancelamento do CT-e registrado (cStat 135) | Objeto de resultado CT-e, cteStatus = Cancelada |
cte.event.registered | Qualquer outro evento de CT-e registrado (carta de correção, comprovante de entrega, insucesso de entrega, desacordo de serviço e seus cancelamentos) | Envelope de evento, data traz event_code / n_seq / protocolo |
dce.authorized | DC-e autorizada (cStat 100) | Objeto de resultado DC-e, dceStatus = Autorizada |
dce.rejected | DC-e rejeitada pela SEFAZ, ou a tarefa de emissão falhou de forma terminal (inclusive antes de o documento ser numerado) | Objeto de resultado DC-e, dceStatus = Negada |
dce.canceled | Cancelamento da DC-e registrado (cStat 135 / 136 / 155) | Objeto de resultado DC-e, dceStatus = Cancelada |
dce.cancel_rejected | A SEFAZ rejeitou o cancelamento da DC-e, ou a tarefa de cancelamento falhou de forma terminal | Objeto de resultado DC-e, dceStatus = CancelamentoNegado, o documento continua autorizado |
Há dois formatos de payload:
- Objeto de resultado do documento: objeto JSON simples cujo primeiro campo é
tipo(NF-e/CT-e/DC-e), com ordem fixa de campos. Usado pelos resultados de autorização, rejeição e cancelamento de cada tipo de documento. Não possui campoevent_id; deduplique pelo cabeçalhoX-Tffiscal-Event-Id. - Envelope de evento:
{ "version", "event_id", "event_type", "occurred_at", "data" }. Usado porinvoice.verify.completede pelas notificações de registro de eventos. Os campos dentro dedatasó são adicionados, nunca renomeados ou removidos; uma mudança incompatível incrementaversion.
Nota: rejeições da SEFAZ nunca são erros HTTP da chamada de emissão. Elas aparecem como status
Negadana consulta e como eventoinvoice.rejected/cte.rejected/dce.rejected. Uma tarefa de CT-e que falha de forma terminal no lado da plataforma (Falha) não gera callback; use a consulta de CT-e para detectá-la.
Cabeçalhos da requisição
| Cabeçalho | Significado |
|---|---|
X-Tffiscal-Event | Código do evento, por exemplo invoice.authorized (webhook.verify na entrega de teste enviada ao salvar uma URL no console) |
X-Tffiscal-Event-Id | Id do evento, a chave de idempotência; não muda entre retentativas e é compartilhado por todos os destinos do mesmo evento |
X-Tffiscal-Delivery-Id | Identificador da entrega; não deduplique por ele, use o id do evento |
X-Tffiscal-Timestamp | Segundos Unix, gerado novamente a cada tentativa |
X-Tffiscal-Signature | hex( HMAC-SHA256( app_secret, timestamp + "." + body ) ), em minúsculas |
token | O token de verificação enviado em Registrar webhook, devolvido sem alteração |
x-token | Mesmo valor de token; verifique qualquer um dos dois |
Verificando a assinatura
Calcule HMAC-SHA256 sobre a string timestamp + "." + rawBody com o seu app_secret e compare em tempo constante com o cabeçalho X-Tffiscal-Signature. Use os bytes brutos recebidos: desserializar e serializar de novo o payload altera a ordem dos campos ou os espaços e invalida a assinatura. Rejeite entregas com timestamp antigo demais (5 minutos é uma tolerância razoável) para evitar replay.
Verificar a assinatura é recomendado. No mínimo, compare o cabeçalho token com o valor registrado.
Node.js:
const crypto = require('node:crypto');/*** Verifica uma entrega de webhook da TF Fiscal.* @param {string} secret seu app_secret* @param {string} timestamp valor do cabeçalho X-Tffiscal-Timestamp* @param {Buffer|string} rawBody corpo bruto da requisição, sem parse* @param {string} signature valor do cabeçalho X-Tffiscal-Signature* @returns {boolean} true quando a assinatura é autêntica*/function verifyWebhook(secret, timestamp, rawBody, signature) {const expected = crypto.createHmac('sha256', secret).update(timestamp + '.' + rawBody, 'utf8').digest('hex');return (expected.length === signature.length &&crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(signature, 'utf8')));}
Java:
import java.nio.charset.StandardCharsets;import java.security.MessageDigest;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;public final class WebhookVerifier {/*** Verifica uma entrega de webhook da TF Fiscal.** @param secret seu app_secret* @param timestamp valor do cabeçalho X-Tffiscal-Timestamp* @param rawBody corpo bruto da requisição, sem parse* @param signature valor do cabeçalho X-Tffiscal-Signature* @return true quando a assinatura é autêntica*/public static boolean verify(String secret, String timestamp, String rawBody, String signature) {try {Mac mac = Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));byte[] digest = mac.doFinal((timestamp + "." + rawBody).getBytes(StandardCharsets.UTF_8));StringBuilder hex = new StringBuilder(digest.length * 2);for (byte b : digest) {hex.append(String.format("%02x", b));}return MessageDigest.isEqual(hex.toString().getBytes(StandardCharsets.UTF_8),signature.getBytes(StandardCharsets.UTF_8));} catch (Exception e) {return false;}}}
Payload de NF-e
Eventos invoice.authorized e invoice.rejected. Registrados em Registrar webhook; veja NF-e para o fluxo de emissão.
Autorizada:
{"tipo": "NF-e","empresaId": "1934811222334455","nfeId": "NFe-000014553","nfeStatus": "Autorizada","nfeLinkDanfe": "https://api.v2.tffiscal.com/openapi/files/danfe/35241204893402000113650010000117691017244265?token=MXw3fDEwMXwxNzU4...","nfeLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/8801?token=MXw3fDEwMXwxNzU4...","nfeNumero": "11769","nfeSerie": "10","nfeChaveAcesso": "35241204893402000113650010000117691017244265","nfeDataEmissao": "2024-12-04T17:44:26Z","nfeDataAutorizacao": "2024-12-04T17:44:26Z","nfeNumeroProtocolo": "135240002599237"}
Negada:
{"tipo": "NF-e","empresaId": "1934811222334455","nfeId": "NFe-000014553","nfeStatus": "Negada","nfeMotivoStatus": "778 - Rejeicao: NCM inexistente","nfeNumero": "11769","nfeSerie": "10","nfeChaveAcesso": "35241204893402000113650010000117691017244265","nfeDataEmissao": "2024-12-04T17:44:26Z"}
| Campo | Tipo | Descrição |
|---|---|---|
tipo | string | Sempre NF-e |
empresaId | string | Identificador da empresa |
nfeId | string | O id enviado na emissão |
nfeStatus | string | Autorizada / Negada |
nfeMotivoStatus | string | Motivo da negativa: código de status da SEFAZ + descrição; vazio quando autorizada |
nfeLinkDanfe | string | Link de download do PDF do DANFE (veja Download de arquivos; utilizável no momento do callback, o primeiro download dispara a renderização); vazio quando negada |
nfeLinkXml | string | Link de download do XML autorizado; vazio quando negada |
nfeNumero | string | Número da nota |
nfeSerie | string | Série |
nfeChaveAcesso | string | Chave de acesso de 44 dígitos |
nfeDataEmissao | string | Data de emissão, ISO-8601 UTC |
nfeDataAutorizacao | string | Data de autorização; vazio quando negada |
nfeNumeroProtocolo | string | Número do protocolo de autorização; vazio quando negada |
nfeDigestValue | string | Digest da assinatura, não fornecido no momento |
Campos sem valor aparecem no payload como valores vazios.
Cancelamento e carta de correção de NF-e
invoice.canceled usa o envelope de evento com data trazendo invoice_id (identificador da nota na plataforma), chave (chave de acesso de 44 dígitos) e protocolo (protocolo do evento de cancelamento). O cancelamento em si é confirmado de forma síncrona por Cancelar NF-e e pelo status Cancelada na consulta.
invoice.cce.registered está reservado no catálogo (data: invoice_id / chave / n_seq). O endpoint de registro da CC-e devolve o protocolo de forma síncrona e nenhum callback é entregue para ele no momento.
Payload de CT-e
Mesmo registro e mesmos cabeçalhos da NF-e; veja CT-e para o fluxo de emissão. O objeto de resultado usa tipo = CT-e com ordem fixa de campos.
| Evento | Disparo | cteStatus |
|---|---|---|
cte.authorized | Autorização 100 | Autorizada |
cte.rejected | Rejeição da SEFAZ (cStat diferente de 100) | Negada (cteMotivoStatus é cStat - motivo); uma falha terminal da tarefa (Falha) não gera callback, use a API de consulta |
cte.canceled | Cancelamento 135 | Cancelada |
cte.event.registered | Qualquer outro evento registrado | Envelope de evento, data traz event_code / n_seq / protocolo |
Autorizado:
{"tipo": "CT-e","empresaId": "1934811222334455","cteId": "CTE-ORD-1","cteStatus": "Autorizada","cteMotivoStatus": null,"cteLinkDacte": "https://api.v2.tffiscal.com/openapi/files/dacte/3526...?token=...","cteLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/7?token=...","cteNumero": "1","cteSerie": "1","cteChaveAcesso": "3526...","cteDataEmissao": "2026-09-06T12:00:00Z","cteDataAutorizacao": "2026-09-06T12:00:03Z","cteNumeroProtocolo": "135260000000001","cteDigestValue": "..."}
| Campo | Tipo | Descrição |
|---|---|---|
tipo | string | Sempre CT-e |
empresaId | string | Identificador da empresa |
cteId | string | O id enviado na emissão |
cteStatus | string | Autorizada / Negada / Cancelada |
cteMotivoStatus | string | null | cStat - motivo quando rejeitado; null nos demais casos |
cteLinkDacte | string | null | Link de download do PDF do DACTE (renderizado no primeiro download); null quando rejeitado |
cteLinkXml | string | null | Link de download do XML autorizado; null quando rejeitado |
cteNumero | string | Número do documento |
cteSerie | string | Série |
cteChaveAcesso | string | Chave de acesso de 44 dígitos |
cteDataEmissao | string | Data de emissão, ISO-8601 UTC |
cteDataAutorizacao | string | null | Data de autorização; null quando rejeitado |
cteNumeroProtocolo | string | null | Número do protocolo de autorização; null quando rejeitado |
cteDigestValue | string | null | Digest da assinatura do XML autorizado |
cteLinkDacte e cteLinkXml são utilizáveis assim que o callback de autorização chega; os links não precisam de cabeçalhos de assinatura e redirecionam com 302, veja Download de arquivos.
Evento registrado (cte.event.registered), por exemplo um comprovante de entrega (110180):
{"version": "1.0","event_id": "987654321098765432","event_type": "cte.event.registered","occurred_at": "2026-09-06T13:00:00Z","data": {"event_code": "110180","n_seq": "1","protocolo": "135260000000099"}}
| Campo | Tipo | Descrição |
|---|---|---|
event_code | string | Código do evento na SEFAZ, por exemplo 110110 carta de correção, 110180 comprovante de entrega |
n_seq | string | Número de sequência do evento |
protocolo | string | Número do protocolo do evento |
Payload de DC-e
Mesmo registro e mesmos cabeçalhos da NF-e; veja DC-e para o fluxo de emissão. O objeto de resultado usa tipo = DC-e com 14 campos em ordem fixa.
| Evento | Disparo | dceStatus |
|---|---|---|
dce.authorized | Autorização 100 | Autorizada |
dce.rejected | Rejeição da SEFAZ ou falha terminal da tarefa (inclusive falhas antes de o documento ser numerado, que não têm chave) | Negada (dceMotivoStatus é cStat - motivo; uma falha terminal sem cStat traz apenas o motivo) |
dce.canceled | Cancelamento registrado (135 / 136 / 155) | Cancelada (dceDataAutorizacao é o momento do registro do cancelamento, dceNumeroProtocolo o protocolo do evento de cancelamento) |
dce.cancel_rejected | A SEFAZ rejeitou o cancelamento ou a tarefa de cancelamento falhou de forma terminal | CancelamentoNegado (o documento continua autorizado; traz o protocolo de autorização / digest / link do XML) |
Autorizada:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "Autorizada","dceMotivoStatus": null,"dceLinkDace": "https://api.v2.tffiscal.com/openapi/files/dace/35260940673061000134990010000000011100000012?token=...","dceLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/7?token=...","dceNumero": "1","dceSerie": "1","dceChaveAcesso": "41260940673061000134990010000000011101234567","dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": "2026-09-06T12:00:03Z","dceNumeroProtocolo": "141260000000001","dceDigestValue": "tasG40Ic6Zh7PIBiLEEH7Hb940Y="}
Rejeitada:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "Negada","dceMotivoStatus": "225 - Rejeicao: Falha no schema XML","dceLinkDace": null,"dceLinkXml": null,"dceNumero": "1","dceSerie": "1","dceChaveAcesso": "4126...","dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": null,"dceNumeroProtocolo": null,"dceDigestValue": null}
Falha antes da numeração (empresa não configurada para DC-e, série ausente, falha de mapeamento da mensagem e outras falhas terminais em que o documento nunca foi numerado e não tem chave): dce.rejected continua sendo entregue, os campos de fato do documento são null, dceMotivoStatus traz o motivo da falha e dceDataEmissao o momento da aceitação. Correlacione pelo dceId e nunca presuma que dceChaveAcesso está presente:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "Negada","dceMotivoStatus": "Empresa não configurada para emissão de DC-e","dceLinkDace": null,"dceLinkXml": null,"dceNumero": null,"dceSerie": null,"dceChaveAcesso": null,"dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": null,"dceNumeroProtocolo": null,"dceDigestValue": null}
Cancelada:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "Cancelada","dceMotivoStatus": null,"dceLinkDace": "https://api.v2.tffiscal.com/openapi/files/dace/35260940673061000134990010000000011100000012?token=...","dceLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/7?token=...","dceNumero": "1","dceSerie": "1","dceChaveAcesso": "4126...","dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": "2026-09-06T15:00:00Z","dceNumeroProtocolo": "141260000000099","dceDigestValue": null}
Cancelamento rejeitado:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "CancelamentoNegado","dceMotivoStatus": "594 - Rejeicao: O numero de sequencia do evento informado e maior que o permitido","dceLinkDace": null,"dceLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/7?token=...","dceNumero": "1","dceSerie": "1","dceChaveAcesso": "4126...","dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": null,"dceNumeroProtocolo": "141260000000001","dceDigestValue": "tasG40Ic6Zh7PIBiLEEH7Hb940Y="}
| Campo | Tipo | Descrição |
|---|---|---|
tipo | string | Sempre DC-e |
empresaId | string | Identificador da empresa |
dceId | string | O id enviado na emissão; chave de correlação de todos os eventos, presente mesmo quando o documento nunca foi numerado |
dceStatus | string | Autorizada / Negada / Cancelada / CancelamentoNegado |
dceMotivoStatus | string | null | cStat - motivo em rejeição da SEFAZ; apenas o motivo em falha terminal sem cStat; null em sucesso |
dceLinkDace | string | null | Link de download do PDF do DACE, renderizado no primeiro download; null quando rejeitada e em CancelamentoNegado |
dceLinkXml | string | null | Link de download do XML autorizado; null quando rejeitada |
dceNumero | string | null | Número do documento; null quando nunca numerado |
dceSerie | string | null | Série; null quando nunca numerado |
dceChaveAcesso | string | null | Chave de acesso de 44 dígitos; null quando nunca numerado |
dceDataEmissao | string | Data de emissão, ISO-8601 UTC (momento da aceitação em falha antes da numeração) |
dceDataAutorizacao | string | null | Data de autorização; momento do registro do cancelamento em Cancelada; null quando rejeitada |
dceNumeroProtocolo | string | null | Protocolo de autorização; protocolo do evento de cancelamento em Cancelada; null quando rejeitada |
dceDigestValue | string | null | Digest da assinatura do XML autorizado; null quando rejeitada e em Cancelada |
Nos callbacks de autorização / cancelamento, dceLinkDace é renderizado sob demanda pela chave (nada é renderizado no momento da entrega; o primeiro download renderiza e arquiva). Os dois links seguem o formato da API de consulta (/openapi/files/{kind}/{ref}?token=...) e sua validade vem da configuração do tenant (7 dias por padrão), veja Download de arquivos.
Payload do veredicto de verificação
Quando a verificação de nível 2 na SEFAZ é concluída, a plataforma envia invoice.verify.completed para a sua URL de webhook. Este é o único canal de push para veredictos finais; a consulta por chave pode servir como fallback por polling.
{"version": "1.0","event_id": "1950000000000001","event_type": "invoice.verify.completed","occurred_at": "2026-07-23T17:16:23Z","data": {"chaveAcesso": "35260764962869000108550990001366171195929648","validationStatus": "VALIDATED","status": "Autorizada","cStat": "100","xMotivo": "Autorizado o uso da NF-e","protocolo": { "numero": "135262955451772", "digestValue": "oAEE...HwY=" },"dataAutorizacao": "2026-07-23T14:30:09Z","eventos": [],"verifiedAt": "2026-07-23T17:16:23Z","reason": "present only for REJECTED / VALIDATION_ERROR (stable English text)"}}
Envelope:
| Campo | Tipo | Presença | Descrição |
|---|---|---|---|
version | string | sempre | Versão do schema do payload, atualmente 1.0 |
event_id | string | sempre | Id do evento, chave de idempotência, idêntico entre retentativas |
event_type | string | sempre | invoice.verify.completed |
occurred_at | string | sempre | Momento do evento, ISO-8601 UTC |
data | object | sempre | Corpo do veredicto, abaixo |
data:
| Campo | Tipo | Presença | Descrição |
|---|---|---|---|
chaveAcesso | string | sempre | Chave de acesso de 44 dígitos da nota verificada, a chave de junção com o seu envio |
validationStatus | string | sempre | Veredicto final: VALIDATED / REJECTED / VALIDATION_ERROR, veja Verificação de NF-e |
status | string | sempre | Situação fiscal na SEFAZ: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida |
cStat | string | null | anulável | Código de retorno bruto da SEFAZ (por exemplo 100 autorizada, 101 cancelada); null quando a SEFAZ não foi alcançada |
xMotivo | string | null | anulável | Mensagem de retorno bruta da SEFAZ (português, sem alteração) |
protocolo | object | null | anulável | Objeto de protocolo (numero, digestValue) do registro oficial |
dataAutorizacao | string | null | anulável | Data de autorização na SEFAZ, ISO-8601 UTC |
eventos[] | array | sempre (pode ser vazio) | Eventos fiscais registrados na nota (cancelamento, cartas de correção); vazio quando não há |
verifiedAt | string | sempre | Quando a verificação de nível 2 foi concluída, ISO-8601 UTC |
reason | string | apenas em falha | Presente apenas em REJECTED / VALIDATION_ERROR; texto estável em inglês que explica o veredicto |
validationStatus | Terminal | Ação |
|---|---|---|
VALIDATED | Sim | Seguro prosseguir (liberar mercadoria, liquidar) |
REJECTED | Sim | Não prossiga; reason explica o veredicto da SEFAZ (cancelada / denegada / inutilizada / não encontrada / protocolo divergente) |
VALIDATION_ERROR | Não | Falha de verificação do lado da plataforma, não é um julgamento da nota; reenvie depois pela verificação de XML com o cabeçalho forceRevalidate: true |
Os payloads de webhook trazem apenas valores de enumeração independentes de idioma; não há campos *Description. O texto de apresentação fica a cargo do receptor.
Retentativas e circuit breaker
- Qualquer status 2xx confirma a entrega. Qualquer outro status, erro de rede ou ausência de resposta em 10 segundos conta como falha.
- Entregas com falha são reenviadas com backoff após a tentativa inicial:
1 min, 5 min, 30 min, 2 h, 6 h
- Cinco retentativas (seis tentativas no total, em cerca de 8,6 horas). Depois disso a entrega fica na fila de dead-letter; a plataforma pode reenviá-la manualmente mediante solicitação. Cada tentativa é registrada com status HTTP, duração e resumo da resposta, que é a evidência usada ao investigar um callback ausente.
- Circuit breaker: falhas consecutivas de entrega (10 por padrão) desativam automaticamente o webhook. Enquanto estiver desativado, nenhum evento novo é enfileirado para ele. Chamar Registrar webhook novamente (ou revalidar e salvar a URL no console) restaura a entrega e zera o contador de falhas; peça à plataforma o reenvio dos eventos gerados enquanto o webhook estava desativado.
- A URL do webhook e o
tokensão relidos antes de cada tentativa, portanto um novo registro passa a valer na próxima retentativa.
Requisitos do receptor
- Responda 2xx em até 10 segundos. Boa prática: persista a entrega bruta, responda 2xx imediatamente e processe de forma assíncrona.
- Verifique a origem: recalcule o HMAC sobre os bytes brutos recebidos e compare o cabeçalho
tokencom o valor registrado. - Deduplique pelo id do evento (
X-Tffiscal-Event-Idouevent_idnos payloads com envelope). - URL pública: URL absoluta
http/httpsacessível pela internet, com no máximo 500 caracteres. Endereços de loopback, privados e link-local são rejeitados no registro. - Responda à entrega de teste: salvar a URL no console envia um evento com
X-Tffiscal-Event: webhook.verify; responda 2xx sem nenhum processamento de negócio. - Condicione as ações de negócio aos campos de resultado (
nfeStatus,cteStatus,dceStatus,validationStatus), nunca apenas à resposta síncrona da API. - Nunca presuma que campos opcionais estão presentes:
dceChaveAcessopode sernull,reasonsó aparece em falha e campos vazios de NF-e chegam como valores vazios.
Solução de problemas
Callbacks não chegam?
Confirme que a URL é um endereço https/http acessível publicamente e que responde 2xx em até 10 segundos. A plataforma reenvia com backoff e desativa o webhook após falhas consecutivas; chamar o endpoint de registro novamente restaura a entrega.
A assinatura sempre falha?
A causa mais comum é desserializar o payload e serializá-lo de novo antes de calcular o HMAC, o que altera a ordem dos campos ou os espaços. Sempre calcule o hash sobre os bytes brutos recebidos e concatene o valor de X-Tffiscal-Timestamp exatamente como recebido.
O mesmo evento chegou duas vezes?
Isso é esperado na entrega ao menos uma vez. Deduplique pelo id do evento; o id de entrega varia por destino e não deve ser usado para isso.
