TF Fiscal
Documentação

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-Id e, nos payloads com envelope, event_id): ele é idêntico em todas as tentativas do mesmo evento.
  • Cada entrega é um HTTP POST com 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

EventoDisparado quandoPayload
invoice.authorizedNF-e autorizada pela SEFAZ (cStat 100)Objeto de resultado NF-e, nfeStatus = Autorizada
invoice.rejectedNF-e rejeitada pela SEFAZObjeto de resultado NF-e, nfeStatus = Negada, motivo em nfeMotivoStatus
invoice.canceledCancelamento da NF-e registrado na SEFAZ (cStat 135)Envelope de evento, data traz invoice_id / chave / protocolo
invoice.cce.registeredCarta de correção (CC-e) registrada em uma NF-eEnvelope 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.completedA verificação de NF-e chegou ao veredicto final (VALIDATED / REJECTED / VALIDATION_ERROR)Envelope de evento com o veredicto em data
cte.authorizedCT-e autorizado (cStat 100)Objeto de resultado CT-e, cteStatus = Autorizada
cte.rejectedCT-e rejeitado pela SEFAZ (cStat diferente de 100)Objeto de resultado CT-e, cteStatus = Negada, cteMotivoStatus = cStat - motivo
cte.canceledCancelamento do CT-e registrado (cStat 135)Objeto de resultado CT-e, cteStatus = Cancelada
cte.event.registeredQualquer 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.authorizedDC-e autorizada (cStat 100)Objeto de resultado DC-e, dceStatus = Autorizada
dce.rejectedDC-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.canceledCancelamento da DC-e registrado (cStat 135 / 136 / 155)Objeto de resultado DC-e, dceStatus = Cancelada
dce.cancel_rejectedA SEFAZ rejeitou o cancelamento da DC-e, ou a tarefa de cancelamento falhou de forma terminalObjeto 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 campo event_id; deduplique pelo cabeçalho X-Tffiscal-Event-Id.
  • Envelope de evento: { "version", "event_id", "event_type", "occurred_at", "data" }. Usado por invoice.verify.completed e pelas notificações de registro de eventos. Os campos dentro de data só são adicionados, nunca renomeados ou removidos; uma mudança incompatível incrementa version.

Nota: rejeições da SEFAZ nunca são erros HTTP da chamada de emissão. Elas aparecem como status Negada na consulta e como evento invoice.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çalhoSignificado
X-Tffiscal-EventCódigo do evento, por exemplo invoice.authorized (webhook.verify na entrega de teste enviada ao salvar uma URL no console)
X-Tffiscal-Event-IdId do evento, a chave de idempotência; não muda entre retentativas e é compartilhado por todos os destinos do mesmo evento
X-Tffiscal-Delivery-IdIdentificador da entrega; não deduplique por ele, use o id do evento
X-Tffiscal-TimestampSegundos Unix, gerado novamente a cada tentativa
X-Tffiscal-Signaturehex( HMAC-SHA256( app_secret, timestamp + "." + body ) ), em minúsculas
tokenO token de verificação enviado em Registrar webhook, devolvido sem alteração
x-tokenMesmo 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:

javascript
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:

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:

json
{
"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:

json
{
"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"
}
CampoTipoDescrição
tipostringSempre NF-e
empresaIdstringIdentificador da empresa
nfeIdstringO id enviado na emissão
nfeStatusstringAutorizada / Negada
nfeMotivoStatusstringMotivo da negativa: código de status da SEFAZ + descrição; vazio quando autorizada
nfeLinkDanfestringLink 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
nfeLinkXmlstringLink de download do XML autorizado; vazio quando negada
nfeNumerostringNúmero da nota
nfeSeriestringSérie
nfeChaveAcessostringChave de acesso de 44 dígitos
nfeDataEmissaostringData de emissão, ISO-8601 UTC
nfeDataAutorizacaostringData de autorização; vazio quando negada
nfeNumeroProtocolostringNúmero do protocolo de autorização; vazio quando negada
nfeDigestValuestringDigest 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.

EventoDisparocteStatus
cte.authorizedAutorização 100Autorizada
cte.rejectedRejeiçã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.canceledCancelamento 135Cancelada
cte.event.registeredQualquer outro evento registradoEnvelope de evento, data traz event_code / n_seq / protocolo

Autorizado:

json
{
"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": "..."
}
CampoTipoDescrição
tipostringSempre CT-e
empresaIdstringIdentificador da empresa
cteIdstringO id enviado na emissão
cteStatusstringAutorizada / Negada / Cancelada
cteMotivoStatusstring | nullcStat - motivo quando rejeitado; null nos demais casos
cteLinkDactestring | nullLink de download do PDF do DACTE (renderizado no primeiro download); null quando rejeitado
cteLinkXmlstring | nullLink de download do XML autorizado; null quando rejeitado
cteNumerostringNúmero do documento
cteSeriestringSérie
cteChaveAcessostringChave de acesso de 44 dígitos
cteDataEmissaostringData de emissão, ISO-8601 UTC
cteDataAutorizacaostring | nullData de autorização; null quando rejeitado
cteNumeroProtocolostring | nullNúmero do protocolo de autorização; null quando rejeitado
cteDigestValuestring | nullDigest 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):

json
{
"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"
}
}
CampoTipoDescrição
event_codestringCódigo do evento na SEFAZ, por exemplo 110110 carta de correção, 110180 comprovante de entrega
n_seqstringNúmero de sequência do evento
protocolostringNú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.

EventoDisparodceStatus
dce.authorizedAutorização 100Autorizada
dce.rejectedRejeiçã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.canceledCancelamento registrado (135 / 136 / 155)Cancelada (dceDataAutorizacao é o momento do registro do cancelamento, dceNumeroProtocolo o protocolo do evento de cancelamento)
dce.cancel_rejectedA SEFAZ rejeitou o cancelamento ou a tarefa de cancelamento falhou de forma terminalCancelamentoNegado (o documento continua autorizado; traz o protocolo de autorização / digest / link do XML)

Autorizada:

json
{
"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:

json
{
"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:

json
{
"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:

json
{
"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:

json
{
"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="
}
CampoTipoDescrição
tipostringSempre DC-e
empresaIdstringIdentificador da empresa
dceIdstringO id enviado na emissão; chave de correlação de todos os eventos, presente mesmo quando o documento nunca foi numerado
dceStatusstringAutorizada / Negada / Cancelada / CancelamentoNegado
dceMotivoStatusstring | nullcStat - motivo em rejeição da SEFAZ; apenas o motivo em falha terminal sem cStat; null em sucesso
dceLinkDacestring | nullLink de download do PDF do DACE, renderizado no primeiro download; null quando rejeitada e em CancelamentoNegado
dceLinkXmlstring | nullLink de download do XML autorizado; null quando rejeitada
dceNumerostring | nullNúmero do documento; null quando nunca numerado
dceSeriestring | nullSérie; null quando nunca numerado
dceChaveAcessostring | nullChave de acesso de 44 dígitos; null quando nunca numerado
dceDataEmissaostringData de emissão, ISO-8601 UTC (momento da aceitação em falha antes da numeração)
dceDataAutorizacaostring | nullData de autorização; momento do registro do cancelamento em Cancelada; null quando rejeitada
dceNumeroProtocolostring | nullProtocolo de autorização; protocolo do evento de cancelamento em Cancelada; null quando rejeitada
dceDigestValuestring | nullDigest 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.

json
{
"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:

CampoTipoPresençaDescrição
versionstringsempreVersão do schema do payload, atualmente 1.0
event_idstringsempreId do evento, chave de idempotência, idêntico entre retentativas
event_typestringsempreinvoice.verify.completed
occurred_atstringsempreMomento do evento, ISO-8601 UTC
dataobjectsempreCorpo do veredicto, abaixo

data:

CampoTipoPresençaDescrição
chaveAcessostringsempreChave de acesso de 44 dígitos da nota verificada, a chave de junção com o seu envio
validationStatusstringsempreVeredicto final: VALIDATED / REJECTED / VALIDATION_ERROR, veja Verificação de NF-e
statusstringsempreSituação fiscal na SEFAZ: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida
cStatstring | nullanulávelCódigo de retorno bruto da SEFAZ (por exemplo 100 autorizada, 101 cancelada); null quando a SEFAZ não foi alcançada
xMotivostring | nullanulávelMensagem de retorno bruta da SEFAZ (português, sem alteração)
protocoloobject | nullanulávelObjeto de protocolo (numero, digestValue) do registro oficial
dataAutorizacaostring | nullanulávelData de autorização na SEFAZ, ISO-8601 UTC
eventos[]arraysempre (pode ser vazio)Eventos fiscais registrados na nota (cancelamento, cartas de correção); vazio quando não há
verifiedAtstringsempreQuando a verificação de nível 2 foi concluída, ISO-8601 UTC
reasonstringapenas em falhaPresente apenas em REJECTED / VALIDATION_ERROR; texto estável em inglês que explica o veredicto
validationStatusTerminalAção
VALIDATEDSimSeguro prosseguir (liberar mercadoria, liquidar)
REJECTEDSimNão prossiga; reason explica o veredicto da SEFAZ (cancelada / denegada / inutilizada / não encontrada / protocolo divergente)
VALIDATION_ERRORNãoFalha 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:
text
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 token são relidos antes de cada tentativa, portanto um novo registro passa a valer na próxima retentativa.

Requisitos do receptor

  1. Responda 2xx em até 10 segundos. Boa prática: persista a entrega bruta, responda 2xx imediatamente e processe de forma assíncrona.
  2. Verifique a origem: recalcule o HMAC sobre os bytes brutos recebidos e compare o cabeçalho token com o valor registrado.
  3. Deduplique pelo id do evento (X-Tffiscal-Event-Id ou event_id nos payloads com envelope).
  4. URL pública: URL absoluta http / https acessível pela internet, com no máximo 500 caracteres. Endereços de loopback, privados e link-local são rejeitados no registro.
  5. 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.
  6. Condicione as ações de negócio aos campos de resultado (nfeStatus, cteStatus, dceStatus, validationStatus), nunca apenas à resposta síncrona da API.
  7. Nunca presuma que campos opcionais estão presentes: dceChaveAcesso pode ser null, reason só 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.