TF Fiscal
Documentation

Webhooks

Event catalogue, delivery headers, HMAC-SHA256 signature, per-document payloads, retries and circuit breaker of TF Fiscal webhooks.

Delivery model

The platform pushes document results and verification verdicts to your server so you do not need to poll.

  • One callback URL per application, registered with Register webhook. Calling the endpoint again overwrites the URL and the token. NF-e, CT-e and DC-e results and the NF-e verification verdict are all delivered to this URL.
  • At-least-once delivery. A retry after a timeout can reach you even though the original attempt was processed, and the same event can be fanned out to several targets. Deduplicate on the event id (X-Tffiscal-Event-Id header, and event_id in enveloped payloads): it is identical across every attempt of the same event.
  • Every delivery is an HTTP POST with a JSON body (Content-Type: application/json; charset=utf-8) to the registered URL.
  • Test and production documents follow the same contract, signature and retry schedule, see Environments.

Event catalogue

EventTriggered whenPayload
invoice.authorizedNF-e authorized by SEFAZ (cStat 100)NF-e result object, nfeStatus = Autorizada
invoice.rejectedNF-e rejected by SEFAZNF-e result object, nfeStatus = Negada, reason in nfeMotivoStatus
invoice.canceledNF-e cancellation registered by SEFAZ (cStat 135)Event envelope, data carries invoice_id / chave / protocolo
invoice.cce.registeredCorrection letter (CC-e) registered on an NF-eEvent envelope, data carries invoice_id / chave / n_seq. Reserved: CC-e registration is synchronous today and this event is not delivered yet
invoice.verify.completedNF-e verification reached its final verdict (VALIDATED / REJECTED / VALIDATION_ERROR)Event envelope with the verdict in data
cte.authorizedCT-e authorized (cStat 100)CT-e result object, cteStatus = Autorizada
cte.rejectedCT-e rejected by SEFAZ (cStat other than 100)CT-e result object, cteStatus = Negada, cteMotivoStatus = cStat - reason
cte.canceledCT-e cancellation registered (cStat 135)CT-e result object, cteStatus = Cancelada
cte.event.registeredAny other CT-e event registered (correction letter, delivery receipt, delivery failure, service disagreement and their cancellations)Event envelope, data carries event_code / n_seq / protocolo
dce.authorizedDC-e authorized (cStat 100)DC-e result object, dceStatus = Autorizada
dce.rejectedDC-e rejected by SEFAZ, or the issuance task failed terminally (including failures before the document was numbered)DC-e result object, dceStatus = Negada
dce.canceledDC-e cancellation registered (cStat 135 / 136 / 155)DC-e result object, dceStatus = Cancelada
dce.cancel_rejectedSEFAZ rejected the DC-e cancellation, or the cancellation task failed terminallyDC-e result object, dceStatus = CancelamentoNegado, the document stays authorized

Two payload shapes are used:

  • Document result object: a bare JSON object whose first field is tipo (NF-e / CT-e / DC-e), with a fixed field order. Used by the authorized, rejected and cancelled results of each document type. It has no event_id field; deduplicate on the X-Tffiscal-Event-Id header.
  • Event envelope: { "version", "event_id", "event_type", "occurred_at", "data" }. Used by invoice.verify.completed and by the event registration notifications. Fields inside data are only ever added, never renamed or removed; a breaking change bumps version.

Note: SEFAZ rejections are never HTTP errors of the issuance call. They surface as the query status Negada and as the invoice.rejected / cte.rejected / dce.rejected event. A CT-e task that fails terminally on the platform side (Falha) sends no callback; use the CT-e query to detect it.

Request headers

HeaderMeaning
X-Tffiscal-EventEvent code, for example invoice.authorized (webhook.verify for the test delivery sent when a URL is saved in the console)
X-Tffiscal-Event-IdEvent id, the idempotency key; unchanged across retries and shared by all targets of the same event
X-Tffiscal-Delivery-IdDelivery identifier; do not deduplicate on it, use the event id
X-Tffiscal-TimestampUnix seconds, regenerated on every attempt
X-Tffiscal-Signaturehex( HMAC-SHA256( app_secret, timestamp + "." + body ) ), lowercase
tokenThe verification token you sent to Register webhook, returned verbatim
x-tokenSame value as token; verify either one

Verifying the signature

Compute HMAC-SHA256 over the string timestamp + "." + rawBody with your app_secret and compare it in constant time with the X-Tffiscal-Signature header. Use the raw received bytes: deserializing and re-serializing the payload first changes field order or whitespace and breaks the signature. Reject deliveries whose timestamp is too old (5 minutes is a reasonable tolerance) to prevent replay.

Verifying the signature is recommended. At minimum compare the token header with the value you registered.

Node.js:

javascript
const crypto = require('node:crypto');
/**
* Verifies a TF Fiscal webhook delivery.
* @param {string} secret your app_secret
* @param {string} timestamp value of the X-Tffiscal-Timestamp header
* @param {Buffer|string} rawBody the raw, unparsed request body
* @param {string} signature value of the X-Tffiscal-Signature header
* @returns {boolean} true when the signature is authentic
*/
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 {
/**
* Verifies a TF Fiscal webhook delivery.
*
* @param secret your app_secret
* @param timestamp value of the X-Tffiscal-Timestamp header
* @param rawBody the raw, unparsed request body
* @param signature value of the X-Tffiscal-Signature header
* @return true when the signature is authentic
*/
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;
}
}
}

NF-e payload

Events invoice.authorized and invoice.rejected. Registered through Register webhook; see NF-e for the issuance flow.

Authorized:

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"
}

Denied:

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"
}
FieldTypeDescription
tipostringAlways NF-e
empresaIdstringCompany identifier
nfeIdstringThe id sent at issuance
nfeStatusstringAutorizada / Negada
nfeMotivoStatusstringDenial reason: SEFAZ status code + description; empty when authorized
nfeLinkDanfestringDANFE PDF download link (see File download; usable at callback time, the first download triggers rendering); empty when denied
nfeLinkXmlstringAuthorized XML download link; empty when denied
nfeNumerostringInvoice number
nfeSeriestringSeries
nfeChaveAcessostring44-digit access key
nfeDataEmissaostringIssuance time, ISO-8601 UTC
nfeDataAutorizacaostringAuthorization time; empty when denied
nfeNumeroProtocolostringAuthorization protocol number; empty when denied
nfeDigestValuestringSignature digest, not provided at present

Fields without a value appear in the payload as empty values.

NF-e cancellation and correction letter

invoice.canceled uses the event envelope with data carrying invoice_id (the platform invoice identifier), chave (44-digit access key) and protocolo (cancellation event protocol). The cancellation itself is confirmed synchronously by Cancel NF-e and by the query status Cancelada.

invoice.cce.registered is reserved in the catalogue (data: invoice_id / chave / n_seq). The CC-e registration endpoint returns the protocol synchronously and no callback is delivered for it at present.

CT-e payload

Same registration and headers as NF-e; see CT-e for the issuance flow. The result object uses tipo = CT-e with a fixed field order.

EventTriggercteStatus
cte.authorizedAuthorization 100Autorizada
cte.rejectedSEFAZ rejection (cStat other than 100)Negada (cteMotivoStatus is cStat - reason); a terminal task failure (Falha) sends no callback, use the query API
cte.canceledCancellation 135Cancelada
cte.event.registeredAny other event registeredEvent envelope, data carries event_code / n_seq / protocolo

Authorized:

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": "..."
}
FieldTypeDescription
tipostringAlways CT-e
empresaIdstringCompany identifier
cteIdstringThe id sent at issuance
cteStatusstringAutorizada / Negada / Cancelada
cteMotivoStatusstring | nullcStat - reason when rejected; null otherwise
cteLinkDactestring | nullDACTE PDF download link (rendered on the first download); null when rejected
cteLinkXmlstring | nullAuthorized XML download link; null when rejected
cteNumerostringDocument number
cteSeriestringSeries
cteChaveAcessostring44-digit access key
cteDataEmissaostringIssuance time, ISO-8601 UTC
cteDataAutorizacaostring | nullAuthorization time; null when rejected
cteNumeroProtocolostring | nullAuthorization protocol number; null when rejected
cteDigestValuestring | nullSignature digest of the authorized XML

cteLinkDacte and cteLinkXml are usable as soon as the authorization callback arrives; the links need no signature headers and redirect with 302, see File download.

Event registered (cte.event.registered), for example a delivery receipt (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"
}
}
FieldTypeDescription
event_codestringSEFAZ event code, for example 110110 correction letter, 110180 delivery receipt
n_seqstringEvent sequence number
protocolostringEvent protocol number

DC-e payload

Same registration and headers as NF-e; see DC-e for the issuance flow. The result object uses tipo = DC-e with 14 fields in fixed order.

EventTriggerdceStatus
dce.authorizedAuthorization 100Autorizada
dce.rejectedSEFAZ rejection or terminal task failure (including failures before the document is numbered, which have no chave)Negada (dceMotivoStatus is cStat - reason; a terminal failure without cStat carries the reason only)
dce.canceledCancellation registered (135 / 136 / 155)Cancelada (dceDataAutorizacao is the cancellation registration time, dceNumeroProtocolo the cancellation event protocol)
dce.cancel_rejectedSEFAZ rejected the cancellation or the cancellation task failed terminallyCancelamentoNegado (the document stays authorized; carries the authorization protocol / digest / XML link)

Authorized:

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="
}

Rejected:

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
}

Failure before numbering (company not configured for DC-e, missing series, message mapping failure and other terminal failures where the document was never numbered and has no chave): dce.rejected is still delivered, the document fact fields are null, dceMotivoStatus carries the failure reason and dceDataEmissao the acceptance time. Correlate by dceId and never assume dceChaveAcesso is present:

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
}

Cancelled:

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
}

Cancellation rejected:

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="
}
FieldTypeDescription
tipostringAlways DC-e
empresaIdstringCompany identifier
dceIdstringThe id sent at issuance; the correlation key for every event, present even when the document was never numbered
dceStatusstringAutorizada / Negada / Cancelada / CancelamentoNegado
dceMotivoStatusstring | nullcStat - reason for a SEFAZ rejection; reason only for a terminal failure without cStat; null on success
dceLinkDacestring | nullDACE PDF download link, rendered on the first download; null when rejected and in CancelamentoNegado
dceLinkXmlstring | nullAuthorized XML download link; null when rejected
dceNumerostring | nullDocument number; null when never numbered
dceSeriestring | nullSeries; null when never numbered
dceChaveAcessostring | null44-digit access key; null when never numbered
dceDataEmissaostringIssuance time, ISO-8601 UTC (acceptance time for a failure before numbering)
dceDataAutorizacaostring | nullAuthorization time; cancellation registration time in Cancelada; null when rejected
dceNumeroProtocolostring | nullAuthorization protocol; cancellation event protocol in Cancelada; null when rejected
dceDigestValuestring | nullSignature digest of the authorized XML; null when rejected and in Cancelada

In the authorized / cancelled callbacks dceLinkDace is rendered lazily by chave (nothing is rendered at delivery time; the first download renders and archives it). Both links share the query API shape (/openapi/files/{kind}/{ref}?token=...) and their lifetime comes from the tenant-level setting (7 days by default), see File download.

Verification verdict payload

When Tier-2 SEFAZ verification settles, the platform POSTs invoice.verify.completed to your webhook URL. This is the only push channel for final verdicts; the chave lookup can serve as a polling fallback.

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:

FieldTypePresenceDescription
versionstringalwaysPayload schema version, currently 1.0
event_idstringalwaysEvent id, idempotency key, identical across retries
event_typestringalwaysinvoice.verify.completed
occurred_atstringalwaysEvent time, ISO-8601 UTC
dataobjectalwaysVerdict body, below

data:

FieldTypePresenceDescription
chaveAcessostringalways44-digit access key of the verified invoice, the join key back to your submission
validationStatusstringalwaysFinal verdict: VALIDATED / REJECTED / VALIDATION_ERROR, see NF-e verification
statusstringalwaysSEFAZ fiscal status: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida
cStatstring | nullnullableRaw SEFAZ return code (for example 100 authorized, 101 cancelled); null when SEFAZ was not reached
xMotivostring | nullnullableRaw SEFAZ return message (Portuguese, verbatim)
protocoloobject | nullnullableProtocol object (numero, digestValue) from the official record
dataAutorizacaostring | nullnullableSEFAZ authorization time, ISO-8601 UTC
eventos[]arrayalways (may be empty)Fiscal events registered against the invoice (cancellation, correction letters); empty when none
verifiedAtstringalwaysWhen Tier-2 verification completed, ISO-8601 UTC
reasonstringonly on failurePresent only for REJECTED / VALIDATION_ERROR; stable English text explaining the verdict
validationStatusTerminalAction
VALIDATEDYesSafe to proceed (release goods, settle)
REJECTEDYesDo not proceed; reason explains the SEFAZ verdict (cancelled / denied / voided / not found / protocol mismatch)
VALIDATION_ERRORNoPlatform-side verification failure, not an invoice judgment; resubmit later through XML verification with header forceRevalidate: true

Webhook payloads carry only language-independent enum values; there are no *Description fields. Presentation text is up to the receiver.

Retries and circuit breaker

  • Any 2xx status acknowledges the delivery. Any other status, a network error or no response within 10 seconds counts as a failure.
  • Failed deliveries are retried with backoff after the initial attempt:
text
1 min, 5 min, 30 min, 2 h, 6 h
  • Five retries (six attempts in total, spanning about 8.6 hours). After that the delivery is parked in a dead-letter queue; the platform can re-push it manually on request. Every attempt is logged with its HTTP status, duration and response summary, which is the evidence used when investigating a missing callback.
  • Circuit breaker: consecutive failed deliveries (10 by default) automatically disable the webhook. While it is disabled no new event is queued for it. Calling Register webhook again (or re-verifying and saving the URL in the console) restores delivery and resets the failure counter; ask the platform to replay events raised while the webhook was disabled.
  • The webhook URL and token are re-read before every attempt, so a re-registration takes effect on the next retry.

Receiver requirements

  1. Return 2xx within 10 seconds. Best practice: persist the raw delivery, return 2xx immediately and process asynchronously.
  2. Verify the origin: recompute the HMAC over the raw received bytes, and compare the token header with the registered value.
  3. Deduplicate on the event id (X-Tffiscal-Event-Id, or event_id in enveloped payloads).
  4. Public URL: an absolute http / https URL reachable from the internet, at most 500 characters. Loopback, private and link-local addresses are rejected at registration.
  5. Answer the test delivery: saving the URL in the console sends an event with X-Tffiscal-Event: webhook.verify; return 2xx without any business processing.
  6. Gate business actions on the result fields (nfeStatus, cteStatus, dceStatus, validationStatus), never on the synchronous API response alone.
  7. Never assume optional fields are present: dceChaveAcesso can be null, reason only appears on failure, and empty NF-e fields arrive as empty values.

Troubleshooting

Callbacks not arriving?

Make sure the URL is a publicly reachable https/http address that returns 2xx within 10 seconds. The platform retries with backoff and disables the webhook after consecutive failures; calling the registration endpoint again restores delivery.

Signature keeps failing?

The most common cause is deserializing the payload and re-serializing it before computing the HMAC, which changes field order or whitespace. Always hash the raw received bytes, and concatenate the X-Tffiscal-Timestamp value exactly as received.

Same event received twice?

This is expected under at-least-once delivery. Deduplicate on the event id; the delivery id differs per target and must not be used for that.