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, configured either in the console ("Webhook configuration" in the application details) or through Register webhook. Both write the same configuration and behave identically; the latest save wins, and the callback format and headers never depend on where the URL was configured. The console currently offers only the invoice.verify.completed subscription; subscribe to other events with eventos when registering. NF-e, NFC-e, CT-e, DC-e and NFS-e results and the NF-e verification verdict are all delivered to this URL.
  • The callback token is optional. When one is set (entered in the console or sent at registration), every callback carries it verbatim in the token and x-token headers, whatever the event type; without one, neither header is sent. The console shows only a masked value, and the token can be changed or cleared there. Visible ASCII characters only (no spaces), up to 256.
  • 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
nfce.authorizedNFC-e authorized; also sent when an offline invoice is authorized after transmissionNFC-e result object, nfeStatus = Autorizada
nfce.rejectedNFC-e rejected by SEFAZ, or failed during platform processing; also sent when an offline invoice is rejected on transmissionNFC-e result object, nfeStatus = Negada
nfce.canceledNFC-e cancellation (including by substitution) registeredNFC-e result object, nfeStatus = Cancelada
nfce.cancel_rejectedSEFAZ rejected the NFC-e cancellationNFC-e result object, nfeStatus = CancelamentoNegado, the invoice stays valid
nfce.contingency_issuedNFC-e issued offline while SEFAZ was downNFC-e result object, nfeStatus = ContingenciaPendente
nfse.authorizedNFS-e issued successfullyNFS-e result object, nfseStatus = Autorizada
nfse.rejectedNFS-e rejected by the authority, or failed on the platform sideNFS-e result object, nfseStatus = Negada, reason in nfseMotivoStatus
nfse.canceledNFS-e cancellation completed (including an approved review)NFS-e result object, nfseStatus = Cancelada
nfse.cancel_requestedNFS-e cancellation review acceptedNFS-e result object, nfseStatus = EmProcessoDeCancelamento
nfse.cancel_deniedNFS-e cancellation did not happen and the document stays authorizedNFS-e result object, nfseStatus = Autorizada, reason in nfseMotivoStatus
nfse.substitutedNFS-e replaced by a substituteNFS-e result object, nfseStatus = Substituida, plus nfseChaveSubstituta
nfse.inbound.receivedInbound NFS-e received, this issuer being the recipient / intermediary (explicit subscription required)Inbound NFS-e notification
nfse.inbound.canceledInbound NFS-e cancelled (explicit subscription required)Inbound NFS-e notification
nfse.inbound.substitutedInbound NFS-e substituted (explicit subscription required)Inbound NFS-e notification
nfse.inbound.manifestedThe manifestation (confirmation / rejection) of an inbound NFS-e was registered (explicit subscription required)Event envelope, result in data
nfse.inbound.manifest_failedThe manifestation task for an inbound NFS-e failed; manifest again before the tacit confirmation deadline (explicit subscription required)Event envelope, failure reason in data.motivo
email.sentInvoice e-mail handed to the mail service (explicit subscription required)Event envelope, data.status = Enviado
email.failedInvoice e-mail failed for good: exhausted wait, exhausted attempts, document no longer authorized (explicit subscription required)Event envelope, data.status = Falha, reason in data.motivo_falha

Two payload shapes are used, chosen only by the event type (last column above) and never by whether the URL was configured in the console or through the registration endpoint:

  • Document result object: a bare JSON object whose first field is tipo (NF-e / NFC-e / CT-e / DC-e / NFS-e), with a fixed field order. Used by the NF-e authorized and rejected results, by all five NFC-e events (nfce.*) and by the authorized, rejected and cancellation-related results of CT-e, DC-e and NFS-e. 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.canceled, invoice.cce.registered, invoice.verify.completed, cte.event.registered, nfse.inbound.* and email.sent / email.failed. 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
tokenOptional callback token: the value set in the console or sent to Register webhook, returned verbatim. Absent when no token is set
x-tokenSame value as token, present whenever token is; 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).

Verifying the signature is recommended: it proves both the origin and that the body was not altered. If you set a callback token, you can also compare the token header with it.

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); 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; 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/35260940673061000134990010000000011100000016?token=...",
"dceLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/7?token=...",
"dceNumero": "1",
"dceSerie": "1",
"dceChaveAcesso": "41260940673061000134990010000000011101234565",
"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/35260940673061000134990010000000011100000016?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; 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

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.

NFC-e payload

Reuses the NF-e callback URL and headers; the issuance flow is in NFC-e. These five events are not part of the default subscription: list them in eventos when registering the webhook to have them delivered. The result object has the NF-e fields with tipo = NFC-e plus the QR code content in nfeQrCode; the 14 fields keep a fixed order and appear even when empty.

Event codeTriggernfeStatus
nfce.authorizedAuthorized, or authorized after an offline invoice is transmittedAutorizada
nfce.rejectedSEFAZ rejection, failure during platform processing, or rejection of a transmitted offline invoiceNegada (nfeMotivoStatus carries the code and reason)
nfce.canceledCancellation or cancellation by substitution registeredCancelada (nfeDataAutorizacao is the cancellation time, nfeNumeroProtocolo the cancellation protocol)
nfce.cancel_rejectedSEFAZ rejected the cancellationCancelamentoNegado (the invoice stays valid; reason in nfeMotivoStatus)
nfce.contingency_issuedIssued offline while SEFAZ was downContingenciaPendente (receipt link and nfeQrCode, no protocol)

Authorized:

json
{
"tipo": "NFC-e",
"empresaId": "1934811222334455",
"nfeId": "PDV01-000123",
"nfeStatus": "Autorizada",
"nfeMotivoStatus": null,
"nfeLinkDanfe": "https://api.v2.tffiscal.com/openapi/files/danfce/35261111222333000181650010000001231123456780?token=MXw3fDEwMXwxNzU4...",
"nfeLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/9901?token=MXw3fDEwMXwxNzU4...",
"nfeNumero": "123",
"nfeSerie": "1",
"nfeChaveAcesso": "35261111222333000181650010000001231123456780",
"nfeDataEmissao": "2026-11-04T15:20:01Z",
"nfeDataAutorizacao": "2026-11-04T15:20:02Z",
"nfeNumeroProtocolo": "135261234567890",
"nfeQrCode": "https://www.homologacao.nfce.fazenda.sp.gov.br/qrcode?p=35261111222333000181650010000001231123456780|3|2"
}

Rejected:

json
{
"tipo": "NFC-e",
"empresaId": "1934811222334455",
"nfeId": "PDV01-000123",
"nfeStatus": "Negada",
"nfeMotivoStatus": "725 - Rejeicao: NFC-e com CFOP invalido",
"nfeLinkDanfe": null,
"nfeLinkXml": null,
"nfeNumero": "123",
"nfeSerie": "1",
"nfeChaveAcesso": "35261111222333000181650010000001231123456780",
"nfeDataEmissao": "2026-11-04T15:20:01Z",
"nfeDataAutorizacao": null,
"nfeNumeroProtocolo": null,
"nfeQrCode": null
}
FieldTypeDescription
tipostringAlways NFC-e
empresaIdstringCompany identifier
nfeIdstringThe id sent at issuance; correlate callbacks on it
nfeStatusstringAutorizada / Negada / Cancelada / CancelamentoNegado / ContingenciaPendente
nfeMotivoStatusstring | nullReason for a rejection or a rejected cancellation; otherwise null
nfeLinkDanfestring | null80 mm receipt PDF link; null on rejection
nfeLinkXmlstring | nullXML link; null on rejection
nfeNumerostring | nullInvoice number
nfeSeriestring | nullSeries
nfeChaveAcessostring | null44-digit access key
nfeDataEmissaostring | nullIssue time, ISO-8601 UTC
nfeDataAutorizacaostring | nullAuthorization time; the cancellation time when Cancelada
nfeNumeroProtocolostring | nullAuthorization protocol; the cancellation protocol when Cancelada
nfeQrCodestring | nullQR code content; encode the whole string when printing the receipt

NFS-e payload

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

EventTriggernfseStatus
nfse.authorizedIssued successfullyAutorizada
nfse.rejectedRejected by the authority or failed on our sideNegada
nfse.canceledCancellation completed (including an approved review)Cancelada
nfse.cancel_requestedCancellation review acceptedEmProcessoDeCancelamento
nfse.cancel_deniedCancellation did not happen and the document stays authorized: synchronous refusal by the authority, dispatch exhausted, or review request refused by the municipalityAutorizada
nfse.substitutedThis document was replaced by a substitute; the payload carries an extra nfseChaveSubstitutaSubstituida
nfse.inbound.receivedInbound NFS-e received (this issuer is the recipient / intermediary, explicit subscription required)—
nfse.inbound.canceledInbound NFS-e cancelled (explicit subscription required)—
nfse.inbound.substitutedInbound NFS-e substituted (explicit subscription required)—

Note: Subscription semantics. An app that has never registered any subscription row receives all six outbound events. As soon as any subscription row exists for the app (for example an inbound or invoice e-mail event), the app switches to allow-list mode and only explicitly enabled events are delivered. When subscribing to inbound events, enable the outbound events you need as well, otherwise outbound notifications stay parked waiting for a target.

Authorized:

json
{
"tipo": "NFS-e",
"empresaId": "1001",
"nfseId": "PED-20260918-001",
"nfseStatus": "Autorizada",
"nfseMotivoStatus": null,
"nfseLinkDanfse": "https://api.v2.tffiscal.com/openapi/files/danfse/…",
"nfseLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/9001?token=…",
"nfseNumero": "1287",
"nfseSerie": "1",
"nfseChaveAcesso": "3550308…",
"nfseIdentificacaoDps": "DPS3550308…",
"nfseDataEmissao": "2026-09-18T13:00:00Z",
"nfseDataAutorizacao": "2026-09-18T13:00:03Z",
"nfseNumeroProtocolo": null,
"nfseAtualizadoEm": "2026-09-18T13:00:03Z"
}
FieldTypeDescription
tipostringAlways NFS-e
empresaIdstringCompany identifier
nfseIdstringThe id sent at issuance; the correlation key for every event, present even when issuance failed
nfseStatusstringAutorizada / Negada / Cancelada / EmProcessoDeCancelamento / Substituida
nfseMotivoStatusstring | nullOn failure the authority code plus description, or the platform-side rejection reason; on nfse.cancel_denied the cancellation verdict code plus text; null on success
nfseLinkDanfsestring | nullDANFSe PDF download link; null when absent
nfseLinkXmlstring | nullDownload link of the official original XML; null when absent
nfseNumerostring | nullNFS-e number assigned by the authority; null when issuance did not succeed
nfseSeriestring | nullSeries; null when issuance did not succeed
nfseChaveAcessostring | null50-digit access key, generated by the authority; null when issuance failed
nfseIdentificacaoDpsstring | null45-character DPS identifier; the only usable reconciliation key for a failed document, and null as well for a failure before numbering
nfseDataEmissaostringIssuance time, ISO-8601 UTC
nfseDataAutorizacaostring | nullAuthorization time; null when not authorized
nfseNumeroProtocolostring | nullAuthority protocol number; null when the authority returns none
nfseAtualizadoEmstringTime of this status change, ISO-8601 UTC
nfseChaveSubstitutastring50-digit access key of the substitute document, present only on nfse.substituted

nfseAtualizadoEm is the moment of this state change. Delivery order is not guaranteed: retries and multiple instances can make an older event arrive after a newer one. Compare it with the timestamp you already hold and drop the older one instead of overwriting state in arrival order.

On a failure callback nfseChaveAcesso may be empty (the authority never generated a key); reconcile by nfseIdentificacaoDps then. A callback for a failure before numbering carries neither, so only nfseId can reconcile it.

The cancellation window and the review flow are described in Cancel NFS-e; inbound documents and manifestation in Inbound NFS-e.

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
statusstring | nullnullableSEFAZ fiscal status: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida; null for VALIDATION_ERROR (no SEFAZ verdict was obtained)
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_ERRORYes, retryablePlatform-side verification failure, not an invoice judgment; like any verdict it is reused for 24 hours, so 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.

Invoice e-mail payload

When an e-mail accepted by Send invoice e-mail reaches its terminal state, the platform sends email.sent (the mail service accepted the message) or email.failed (final failure). Both require an explicit subscription: they are only delivered when listed in eventos at Webhook registration. After either one the same document accepts a new send. The payload is the event envelope, with data fields in snake_case.

json
{
"version": "1.0",
"event_id": "1950000000000006",
"event_type": "email.sent",
"occurred_at": "2026-09-28T12:00:31Z",
"data": {
"email_id": "2103160000000000001",
"id": "email-pedido-8842-1",
"tipo_documento": "NFe",
"documento_id": "pedido-8842",
"chave": "35241204893402000113650010000117691017244265",
"destinatarios": ["comprador@exemplo.com"],
"status": "Enviado",
"motivo_falha": null
}
}

data:

FieldTypePresenceDescription
email_idstringalwaysThe emailId returned at acceptance; use it in Query invoice e-mail
idstringalwaysRequest identifier sent with the send
tipo_documentostringalwaysNFe / NFSe / CTe / DCe
documento_idstringalwaysThe id sent at issuance
chavestring | nulloptionalAccess key, 44 or 50 digits; null when the failure happened before the key existed
destinatariosarrayalwaysEffective recipients
statusstringalwaysEnviado (email.sent) or Falha (email.failed)
motivo_falhastring | nulloptionalFailure reason, free text; null on email.sent

Failure example (São Paulo NFS-e whose wait for the key ran out):

json
{
"version": "1.0",
"event_id": "1950000000000007",
"event_type": "email.failed",
"occurred_at": "2026-09-28T14:00:30Z",
"data": {
"email_id": "2103160000000000002",
"id": "mail-sp-000045",
"tipo_documento": "NFSe",
"documento_id": "SP-20260928-045",
"chave": null,
"destinatarios": ["financeiro@cliente.com.br"],
"status": "Falha",
"motivo_falha": "等待超时:chaveAcesso"
}
}

email.sent means the mail service accepted the message, not that it reached the recipient inbox. Templates, attachments and states are in Invoice e-mail.

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 platform can re-push the delivery manually on request.
  • 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.
  • 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; if you set a callback token, also compare the token header with it.
  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 512 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 (carrying token / x-token when a token is set); return 2xx without any business processing.
  6. Gate business actions on the result fields (nfeStatus, cteStatus, dceStatus, nfseStatus, 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.