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.completedsubscription; subscribe to other events witheventoswhen 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
tokenis optional. When one is set (entered in the console or sent at registration), every callback carries it verbatim in thetokenandx-tokenheaders, 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-Idheader, andevent_idin enveloped payloads): it is identical across every attempt of the same event. - Every delivery is an HTTP
POSTwith 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
| Event | Triggered when | Payload |
|---|---|---|
invoice.authorized | NF-e authorized by SEFAZ (cStat 100) | NF-e result object, nfeStatus = Autorizada |
invoice.rejected | NF-e rejected by SEFAZ | NF-e result object, nfeStatus = Negada, reason in nfeMotivoStatus |
invoice.canceled | NF-e cancellation registered by SEFAZ (cStat 135) | Event envelope, data carries invoice_id / chave / protocolo |
invoice.cce.registered | Correction letter (CC-e) registered on an NF-e | Event envelope, data carries invoice_id / chave / n_seq. Reserved: CC-e registration is synchronous today and this event is not delivered yet |
invoice.verify.completed | NF-e verification reached its final verdict (VALIDATED / REJECTED / VALIDATION_ERROR) | Event envelope with the verdict in data |
cte.authorized | CT-e authorized (cStat 100) | CT-e result object, cteStatus = Autorizada |
cte.rejected | CT-e rejected by SEFAZ (cStat other than 100) | CT-e result object, cteStatus = Negada, cteMotivoStatus = cStat - reason |
cte.canceled | CT-e cancellation registered (cStat 135) | CT-e result object, cteStatus = Cancelada |
cte.event.registered | Any 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.authorized | DC-e authorized (cStat 100) | DC-e result object, dceStatus = Autorizada |
dce.rejected | DC-e rejected by SEFAZ, or the issuance task failed terminally (including failures before the document was numbered) | DC-e result object, dceStatus = Negada |
dce.canceled | DC-e cancellation registered (cStat 135 / 136 / 155) | DC-e result object, dceStatus = Cancelada |
dce.cancel_rejected | SEFAZ rejected the DC-e cancellation, or the cancellation task failed terminally | DC-e result object, dceStatus = CancelamentoNegado, the document stays authorized |
nfce.authorized | NFC-e authorized; also sent when an offline invoice is authorized after transmission | NFC-e result object, nfeStatus = Autorizada |
nfce.rejected | NFC-e rejected by SEFAZ, or failed during platform processing; also sent when an offline invoice is rejected on transmission | NFC-e result object, nfeStatus = Negada |
nfce.canceled | NFC-e cancellation (including by substitution) registered | NFC-e result object, nfeStatus = Cancelada |
nfce.cancel_rejected | SEFAZ rejected the NFC-e cancellation | NFC-e result object, nfeStatus = CancelamentoNegado, the invoice stays valid |
nfce.contingency_issued | NFC-e issued offline while SEFAZ was down | NFC-e result object, nfeStatus = ContingenciaPendente |
nfse.authorized | NFS-e issued successfully | NFS-e result object, nfseStatus = Autorizada |
nfse.rejected | NFS-e rejected by the authority, or failed on the platform side | NFS-e result object, nfseStatus = Negada, reason in nfseMotivoStatus |
nfse.canceled | NFS-e cancellation completed (including an approved review) | NFS-e result object, nfseStatus = Cancelada |
nfse.cancel_requested | NFS-e cancellation review accepted | NFS-e result object, nfseStatus = EmProcessoDeCancelamento |
nfse.cancel_denied | NFS-e cancellation did not happen and the document stays authorized | NFS-e result object, nfseStatus = Autorizada, reason in nfseMotivoStatus |
nfse.substituted | NFS-e replaced by a substitute | NFS-e result object, nfseStatus = Substituida, plus nfseChaveSubstituta |
nfse.inbound.received | Inbound NFS-e received, this issuer being the recipient / intermediary (explicit subscription required) | Inbound NFS-e notification |
nfse.inbound.canceled | Inbound NFS-e cancelled (explicit subscription required) | Inbound NFS-e notification |
nfse.inbound.substituted | Inbound NFS-e substituted (explicit subscription required) | Inbound NFS-e notification |
nfse.inbound.manifested | The manifestation (confirmation / rejection) of an inbound NFS-e was registered (explicit subscription required) | Event envelope, result in data |
nfse.inbound.manifest_failed | The 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.sent | Invoice e-mail handed to the mail service (explicit subscription required) | Event envelope, data.status = Enviado |
email.failed | Invoice 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 noevent_idfield; deduplicate on theX-Tffiscal-Event-Idheader. - Event envelope:
{ "version", "event_id", "event_type", "occurred_at", "data" }. Used byinvoice.canceled,invoice.cce.registered,invoice.verify.completed,cte.event.registered,nfse.inbound.*andemail.sent/email.failed. Fields insidedataare only ever added, never renamed or removed; a breaking change bumpsversion.
Note: SEFAZ rejections are never HTTP errors of the issuance call. They surface as the query status
Negadaand as theinvoice.rejected/cte.rejected/dce.rejectedevent. 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
| Header | Meaning |
|---|---|
X-Tffiscal-Event | Event code, for example invoice.authorized (webhook.verify for the test delivery sent when a URL is saved in the console) |
X-Tffiscal-Event-Id | Event id, the idempotency key; unchanged across retries and shared by all targets of the same event |
X-Tffiscal-Delivery-Id | Delivery identifier; do not deduplicate on it, use the event id |
X-Tffiscal-Timestamp | Unix seconds, regenerated on every attempt |
X-Tffiscal-Signature | hex( HMAC-SHA256( app_secret, timestamp + "." + body ) ), lowercase |
token | Optional callback token: the value set in the console or sent to Register webhook, returned verbatim. Absent when no token is set |
x-token | Same 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:
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:
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:
{"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:
{"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"}
| Field | Type | Description |
|---|---|---|
tipo | string | Always NF-e |
empresaId | string | Company identifier |
nfeId | string | The id sent at issuance |
nfeStatus | string | Autorizada / Negada |
nfeMotivoStatus | string | Denial reason: SEFAZ status code + description; empty when authorized |
nfeLinkDanfe | string | DANFE PDF download link (see File download; usable at callback time); empty when denied |
nfeLinkXml | string | Authorized XML download link; empty when denied |
nfeNumero | string | Invoice number |
nfeSerie | string | Series |
nfeChaveAcesso | string | 44-digit access key |
nfeDataEmissao | string | Issuance time, ISO-8601 UTC |
nfeDataAutorizacao | string | Authorization time; empty when denied |
nfeNumeroProtocolo | string | Authorization protocol number; empty when denied |
nfeDigestValue | string | Signature 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.
| Event | Trigger | cteStatus |
|---|---|---|
cte.authorized | Authorization 100 | Autorizada |
cte.rejected | SEFAZ rejection (cStat other than 100) | Negada (cteMotivoStatus is cStat - reason); a terminal task failure (Falha) sends no callback, use the query API |
cte.canceled | Cancellation 135 | Cancelada |
cte.event.registered | Any other event registered | Event envelope, data carries event_code / n_seq / protocolo |
Authorized:
{"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": "..."}
| Field | Type | Description |
|---|---|---|
tipo | string | Always CT-e |
empresaId | string | Company identifier |
cteId | string | The id sent at issuance |
cteStatus | string | Autorizada / Negada / Cancelada |
cteMotivoStatus | string | null | cStat - reason when rejected; null otherwise |
cteLinkDacte | string | null | DACTE PDF download link; null when rejected |
cteLinkXml | string | null | Authorized XML download link; null when rejected |
cteNumero | string | Document number |
cteSerie | string | Series |
cteChaveAcesso | string | 44-digit access key |
cteDataEmissao | string | Issuance time, ISO-8601 UTC |
cteDataAutorizacao | string | null | Authorization time; null when rejected |
cteNumeroProtocolo | string | null | Authorization protocol number; null when rejected |
cteDigestValue | string | null | Signature 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):
{"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"}}
| Field | Type | Description |
|---|---|---|
event_code | string | SEFAZ event code, for example 110110 correction letter, 110180 delivery receipt |
n_seq | string | Event sequence number |
protocolo | string | Event 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.
| Event | Trigger | dceStatus |
|---|---|---|
dce.authorized | Authorization 100 | Autorizada |
dce.rejected | SEFAZ 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.canceled | Cancellation registered (135 / 136 / 155) | Cancelada (dceDataAutorizacao is the cancellation registration time, dceNumeroProtocolo the cancellation event protocol) |
dce.cancel_rejected | SEFAZ rejected the cancellation or the cancellation task failed terminally | CancelamentoNegado (the document stays authorized; carries the authorization protocol / digest / XML link) |
Authorized:
{"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:
{"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:
{"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:
{"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:
{"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="}
| Field | Type | Description |
|---|---|---|
tipo | string | Always DC-e |
empresaId | string | Company identifier |
dceId | string | The id sent at issuance; the correlation key for every event, present even when the document was never numbered |
dceStatus | string | Autorizada / Negada / Cancelada / CancelamentoNegado |
dceMotivoStatus | string | null | cStat - reason for a SEFAZ rejection; reason only for a terminal failure without cStat; null on success |
dceLinkDace | string | null | DACE PDF download link; null when rejected and in CancelamentoNegado |
dceLinkXml | string | null | Authorized XML download link; null when rejected |
dceNumero | string | null | Document number; null when never numbered |
dceSerie | string | null | Series; null when never numbered |
dceChaveAcesso | string | null | 44-digit access key; null when never numbered |
dceDataEmissao | string | Issuance time, ISO-8601 UTC (acceptance time for a failure before numbering) |
dceDataAutorizacao | string | null | Authorization time; cancellation registration time in Cancelada; null when rejected |
dceNumeroProtocolo | string | null | Authorization protocol; cancellation event protocol in Cancelada; null when rejected |
dceDigestValue | string | null | Signature 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 code | Trigger | nfeStatus |
|---|---|---|
nfce.authorized | Authorized, or authorized after an offline invoice is transmitted | Autorizada |
nfce.rejected | SEFAZ rejection, failure during platform processing, or rejection of a transmitted offline invoice | Negada (nfeMotivoStatus carries the code and reason) |
nfce.canceled | Cancellation or cancellation by substitution registered | Cancelada (nfeDataAutorizacao is the cancellation time, nfeNumeroProtocolo the cancellation protocol) |
nfce.cancel_rejected | SEFAZ rejected the cancellation | CancelamentoNegado (the invoice stays valid; reason in nfeMotivoStatus) |
nfce.contingency_issued | Issued offline while SEFAZ was down | ContingenciaPendente (receipt link and nfeQrCode, no protocol) |
Authorized:
{"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:
{"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}
| Field | Type | Description |
|---|---|---|
tipo | string | Always NFC-e |
empresaId | string | Company identifier |
nfeId | string | The id sent at issuance; correlate callbacks on it |
nfeStatus | string | Autorizada / Negada / Cancelada / CancelamentoNegado / ContingenciaPendente |
nfeMotivoStatus | string | null | Reason for a rejection or a rejected cancellation; otherwise null |
nfeLinkDanfe | string | null | 80 mm receipt PDF link; null on rejection |
nfeLinkXml | string | null | XML link; null on rejection |
nfeNumero | string | null | Invoice number |
nfeSerie | string | null | Series |
nfeChaveAcesso | string | null | 44-digit access key |
nfeDataEmissao | string | null | Issue time, ISO-8601 UTC |
nfeDataAutorizacao | string | null | Authorization time; the cancellation time when Cancelada |
nfeNumeroProtocolo | string | null | Authorization protocol; the cancellation protocol when Cancelada |
nfeQrCode | string | null | QR 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.
| Event | Trigger | nfseStatus |
|---|---|---|
nfse.authorized | Issued successfully | Autorizada |
nfse.rejected | Rejected by the authority or failed on our side | Negada |
nfse.canceled | Cancellation completed (including an approved review) | Cancelada |
nfse.cancel_requested | Cancellation review accepted | EmProcessoDeCancelamento |
nfse.cancel_denied | Cancellation did not happen and the document stays authorized: synchronous refusal by the authority, dispatch exhausted, or review request refused by the municipality | Autorizada |
nfse.substituted | This document was replaced by a substitute; the payload carries an extra nfseChaveSubstituta | Substituida |
nfse.inbound.received | Inbound NFS-e received (this issuer is the recipient / intermediary, explicit subscription required) | — |
nfse.inbound.canceled | Inbound NFS-e cancelled (explicit subscription required) | — |
nfse.inbound.substituted | Inbound 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:
{"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"}
| Field | Type | Description |
|---|---|---|
tipo | string | Always NFS-e |
empresaId | string | Company identifier |
nfseId | string | The id sent at issuance; the correlation key for every event, present even when issuance failed |
nfseStatus | string | Autorizada / Negada / Cancelada / EmProcessoDeCancelamento / Substituida |
nfseMotivoStatus | string | null | On 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 |
nfseLinkDanfse | string | null | DANFSe PDF download link; null when absent |
nfseLinkXml | string | null | Download link of the official original XML; null when absent |
nfseNumero | string | null | NFS-e number assigned by the authority; null when issuance did not succeed |
nfseSerie | string | null | Series; null when issuance did not succeed |
nfseChaveAcesso | string | null | 50-digit access key, generated by the authority; null when issuance failed |
nfseIdentificacaoDps | string | null | 45-character DPS identifier; the only usable reconciliation key for a failed document, and null as well for a failure before numbering |
nfseDataEmissao | string | Issuance time, ISO-8601 UTC |
nfseDataAutorizacao | string | null | Authorization time; null when not authorized |
nfseNumeroProtocolo | string | null | Authority protocol number; null when the authority returns none |
nfseAtualizadoEm | string | Time of this status change, ISO-8601 UTC |
nfseChaveSubstituta | string | 50-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.
{"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:
| Field | Type | Presence | Description |
|---|---|---|---|
version | string | always | Payload schema version, currently 1.0 |
event_id | string | always | Event id, idempotency key, identical across retries |
event_type | string | always | invoice.verify.completed |
occurred_at | string | always | Event time, ISO-8601 UTC |
data | object | always | Verdict body, below |
data:
| Field | Type | Presence | Description |
|---|---|---|---|
chaveAcesso | string | always | 44-digit access key of the verified invoice, the join key back to your submission |
validationStatus | string | always | Final verdict: VALIDATED / REJECTED / VALIDATION_ERROR, see NF-e verification |
status | string | null | nullable | SEFAZ fiscal status: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida; null for VALIDATION_ERROR (no SEFAZ verdict was obtained) |
cStat | string | null | nullable | Raw SEFAZ return code (for example 100 authorized, 101 cancelled); null when SEFAZ was not reached |
xMotivo | string | null | nullable | Raw SEFAZ return message (Portuguese, verbatim) |
protocolo | object | null | nullable | Protocol object (numero, digestValue) from the official record |
dataAutorizacao | string | null | nullable | SEFAZ authorization time, ISO-8601 UTC |
eventos[] | array | always (may be empty) | Fiscal events registered against the invoice (cancellation, correction letters); empty when none |
verifiedAt | string | always | When Tier-2 verification completed, ISO-8601 UTC |
reason | string | only on failure | Present only for REJECTED / VALIDATION_ERROR; stable English text explaining the verdict |
validationStatus | Terminal | Action |
|---|---|---|
VALIDATED | Yes | Safe to proceed (release goods, settle) |
REJECTED | Yes | Do not proceed; reason explains the SEFAZ verdict (cancelled / denied / voided / not found / protocol mismatch) |
VALIDATION_ERROR | Yes, retryable | Platform-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.
{"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:
| Field | Type | Presence | Description |
|---|---|---|---|
email_id | string | always | The emailId returned at acceptance; use it in Query invoice e-mail |
id | string | always | Request identifier sent with the send |
tipo_documento | string | always | NFe / NFSe / CTe / DCe |
documento_id | string | always | The id sent at issuance |
chave | string | null | optional | Access key, 44 or 50 digits; null when the failure happened before the key existed |
destinatarios | array | always | Effective recipients |
status | string | always | Enviado (email.sent) or Falha (email.failed) |
motivo_falha | string | null | optional | Failure reason, free text; null on email.sent |
Failure example (São Paulo NFS-e whose wait for the key ran out):
{"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:
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
- Return 2xx within 10 seconds. Best practice: persist the raw delivery, return 2xx immediately and process asynchronously.
- Verify the origin: recompute the HMAC over the raw received bytes; if you set a callback token, also compare the
tokenheader with it. - Deduplicate on the event id (
X-Tffiscal-Event-Id, orevent_idin enveloped payloads). - Public URL: an absolute
http/httpsURL reachable from the internet, at most 512 characters. Loopback, private and link-local addresses are rejected at registration. - Answer the test delivery: saving the URL in the console sends an event with
X-Tffiscal-Event: webhook.verify(carryingtoken/x-tokenwhen a token is set); return 2xx without any business processing. - Gate business actions on the result fields (
nfeStatus,cteStatus,dceStatus,nfseStatus,validationStatus), never on the synchronous API response alone. - Never assume optional fields are present:
dceChaveAcessocan benull,reasononly 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.
