TF Fiscal
开发文档

Webhooks

TF Fiscal webhook 的事件目录、投递请求头、HMAC-SHA256 签名、各单据类型载荷、重试与熔断。

投递模型

平台把单据结果与验证结论主动推送到你的服务器,无需轮询。

  • 每个应用一个回调地址,通过 Webhook 注册 登记。重复调用即覆盖地址与 token。NF-e、CT-e、DC-e 的结果以及 NF-e 验证结论都投递到这个地址。
  • 至少一次投递。 超时后的重试可能在原尝试实际已被处理的情况下再次到达,同一事件也可能扇出到多个目标。请按事件 id(请求头 X-Tffiscal-Event-Id,信封载荷里另有 event_id)去重:同一事件的所有尝试共用同一个值。
  • 每次投递都是向登记地址发起的 HTTP POST,JSON 请求体(Content-Type: application/json; charset=utf-8)。
  • 测试环境与生产环境的单据遵循同一套契约、签名与重试计划,见环境

事件目录

事件触发时机载荷
invoice.authorizedNF-e 被 SEFAZ 授权(cStat 100)NF-e 结果对象,nfeStatus = Autorizada
invoice.rejectedNF-e 被 SEFAZ 驳回NF-e 结果对象,nfeStatus = Negada,原因在 nfeMotivoStatus
invoice.canceledNF-e 作废在 SEFAZ 登记完成(cStat 135)事件信封,datainvoice_id / chave / protocolo
invoice.cce.registeredNF-e 更正函(CC-e)登记完成事件信封,datainvoice_id / chave / n_seq。预留:CC-e 登记目前为同步返回,本事件暂不推送
invoice.verify.completedNF-e 验证得出最终结论(VALIDATED / REJECTED / VALIDATION_ERROR事件信封,结论在 data
cte.authorizedCT-e 授权通过(cStat 100)CT-e 结果对象,cteStatus = Autorizada
cte.rejectedCT-e 被 SEFAZ 驳回(cStat 非 100)CT-e 结果对象,cteStatus = NegadacteMotivoStatuscStat - 原因
cte.canceledCT-e 取消登记完成(cStat 135)CT-e 结果对象,cteStatus = Cancelada
cte.event.registeredCT-e 其他事件登记成功(更正函、送达凭证、送达失败、服务异议及其撤销)事件信封,dataevent_code / n_seq / protocolo
dce.authorizedDC-e 授权通过(cStat 100)DC-e 结果对象,dceStatus = Autorizada
dce.rejectedDC-e 被 SEFAZ 驳回,或发行任务终局失败(含单据编号前的失败)DC-e 结果对象,dceStatus = Negada
dce.canceledDC-e 取消登记成功(cStat 135 / 136 / 155)DC-e 结果对象,dceStatus = Cancelada
dce.cancel_rejectedDC-e 取消被 SEFAZ 拒绝,或取消任务终局失败DC-e 结果对象,dceStatus = CancelamentoNegado,单据仍为已授权

载荷有两种形状:

  • 单据结果对象:裸 JSON 对象,首字段为 tipoNF-e / CT-e / DC-e),字段顺序固定。各单据类型的授权、驳回、取消结果使用此形状。它没有 event_id 字段,请按请求头 X-Tffiscal-Event-Id 去重。
  • 事件信封{ "version", "event_id", "event_type", "occurred_at", "data" }invoice.verify.completed 与事件登记通知使用此形状。data 内只增不改不删,破坏性变更升 version

注意: SEFAZ 驳回从不作为发行调用的 HTTP 错误返回,而是体现为查询状态 Negadainvoice.rejected / cte.rejected / dce.rejected 事件。CT-e 任务在平台侧终局失败(Falha)不发回调,请通过 CT-e 查询 获知。

请求头

请求头说明
X-Tffiscal-Event事件编码,如 invoice.authorized(在控制台保存地址时发出的测试投递为 webhook.verify
X-Tffiscal-Event-Id事件 id,即幂等键;重试间不变,同一事件的所有目标共用
X-Tffiscal-Delivery-Id投递标识;不要据此去重,请用事件 id
X-Tffiscal-TimestampUnix 秒,每次尝试重新生成
X-Tffiscal-Signaturehex( HMAC-SHA256( app_secret, timestamp + "." + body ) ),小写
token你在 Webhook 注册 时提交的校验令牌,原样回传
x-tokentoken 同值,校验任一即可

验签

用你的 app_secret 对字符串 timestamp + "." + rawBody 计算 HMAC-SHA256,并与请求头 X-Tffiscal-Signature 做常量时间比较。必须用收到的原始字节:先反序列化再重序列化会改变字段顺序或空白,导致签名失败。时间戳过旧的投递应拒绝(容忍 5 分钟是合理取值),以防重放。

建议校验签名;至少要把请求头 token 与注册值比对。

Node.js:

javascript
const crypto = require('node:crypto');
/**
* 校验 TF Fiscal webhook 投递。
* @param {string} secret 你的 app_secret
* @param {string} timestamp 请求头 X-Tffiscal-Timestamp 的值
* @param {Buffer|string} rawBody 未解析的原始请求体
* @param {string} signature 请求头 X-Tffiscal-Signature 的值
* @returns {boolean} 签名有效时返回 true
*/
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 {
/**
* 校验 TF Fiscal webhook 投递。
*
* @param secret 你的 app_secret
* @param timestamp 请求头 X-Tffiscal-Timestamp 的值
* @param rawBody 未解析的原始请求体
* @param signature 请求头 X-Tffiscal-Signature 的值
* @return 签名有效时返回 true
*/
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 载荷

事件 invoice.authorizedinvoice.rejected。通过 Webhook 注册 登记;开票流程见 NF-e

授权:

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

驳回:

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"
}
字段类型说明
tipostring固定 NF-e
empresaIdstring主体标识
nfeIdstring开票时传入的 id
nfeStatusstringAutorizada / Negada
nfeMotivoStatusstring驳回原因:SEFAZ 返回码 + 描述;授权时无值
nfeLinkDanfestringDANFE PDF 下载地址(见文件下载;回调时刻即可用,首次下载触发渲染);驳回时无值
nfeLinkXmlstring授权 XML 下载地址;驳回时无值
nfeNumerostring票号
nfeSeriestring系列号
nfeChaveAcessostring44 位访问密钥
nfeDataEmissaostring开票时间,ISO-8601 UTC
nfeDataAutorizacaostring授权时间;驳回时无值
nfeNumeroProtocolostring授权协议号;驳回时无值
nfeDigestValuestring签名摘要,当前不提供

无值的字段以空值形式出现在载荷中。

NF-e 作废与更正函

invoice.canceled 使用事件信封,datainvoice_id(平台发票标识)、chave(44 位访问密钥)与 protocolo(作废事件协议号)。作废本身由 NF-e 作废 同步确认,查询状态随之变为 Cancelada

invoice.cce.registered 在目录中预留(datainvoice_id / chave / n_seq)。更正函登记接口同步返回协议号,目前不推送该回调。

CT-e 载荷

复用 NF-e 的登记与请求头;发行流程见 CT-e。结果对象 tipo = CT-e,字段顺序固定。

事件触发cteStatus
cte.authorized授权 100Autorizada
cte.rejectedSEFAZ 驳回(cStat 非 100)NegadacteMotivoStatuscStat - 原因);任务终态失败(Falha)不发回调,请经查询接口获知
cte.canceled取消 135Cancelada
cte.event.registered其他事件登记成功事件信封,dataevent_code / n_seq / protocolo

授权:

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": "..."
}
字段类型说明
tipostring固定 CT-e
empresaIdstring主体标识
cteIdstring发行时传入的 id
cteStatusstringAutorizada / Negada / Cancelada
cteMotivoStatusstring | null驳回时为 cStat - 原因;其余为 null
cteLinkDactestring | nullDACTE PDF 下载地址(首次下载才渲染);驳回时为 null
cteLinkXmlstring | null授权 XML 下载地址;驳回时为 null
cteNumerostring单据号
cteSeriestring系列号
cteChaveAcessostring44 位访问密钥
cteDataEmissaostring发行时间,ISO-8601 UTC
cteDataAutorizacaostring | null授权时间;驳回时为 null
cteNumeroProtocolostring | null授权协议号;驳回时为 null
cteDigestValuestring | null授权 XML 的签名摘要

cteLinkDactecteLinkXml 在授权回调到达时即可用;链接不需要签名头、302 跳转,见文件下载

事件登记(cte.event.registered),以送达凭证(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"
}
}
字段类型说明
event_codestringSEFAZ 事件码,如 110110 更正函、110180 送达凭证
n_seqstring事件序号
protocolostring事件协议号

DC-e 载荷

复用 NF-e 的登记与请求头;发行流程见 DC-e。结果对象 tipo = DC-e,14 个字段顺序固定。

事件触发dceStatus
dce.authorized授权 100Autorizada
dce.rejectedSEFAZ 驳回或任务终局失败(含单据编号前的失败,此时无 chave)NegadadceMotivoStatuscStat - 原因;终局失败无 cStat 时只有原因)
dce.canceled取消登记成功(135 / 136 / 155)CanceladadceDataAutorizacao 为取消登记时刻、dceNumeroProtocolo 为取消事件协议号)
dce.cancel_rejected取消被 SEFAZ 拒绝或取消任务终局失败CancelamentoNegado(单据仍为已授权,带授权协议号 / 摘要 / XML 链接)

授权:

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

驳回:

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
}

单据编号前失败(主体未配置 DC-e、号段缺失、报文映射失败等终局失败,单据未编号、无 chave):同样收到 dce.rejected,单据事实字段为 nulldceMotivoStatus 为失败原因,dceDataEmissao 为受理时刻。请以 dceId 关联,不要假定 dceChaveAcesso 一定有值:

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
}

取消成功:

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
}

取消被拒:

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="
}
字段类型说明
tipostring固定 DC-e
empresaIdstring主体标识
dceIdstring发行时传入的 id;所有事件的关联键,单据未编号时也有值
dceStatusstringAutorizada / Negada / Cancelada / CancelamentoNegado
dceMotivoStatusstring | nullSEFAZ 驳回时为 cStat - 原因;无 cStat 的终局失败只有原因;成功时为 null
dceLinkDacestring | nullDACE PDF 下载地址,首次下载才渲染;驳回与 CancelamentoNegado 时为 null
dceLinkXmlstring | null授权 XML 下载地址;驳回时为 null
dceNumerostring | null单据号;未编号时为 null
dceSeriestring | null系列号;未编号时为 null
dceChaveAcessostring | null44 位访问密钥;未编号时为 null
dceDataEmissaostring发行时间,ISO-8601 UTC(编号前失败时为受理时刻)
dceDataAutorizacaostring | null授权时间;Cancelada 时为取消登记时刻;驳回时为 null
dceNumeroProtocolostring | null授权协议号;Cancelada 时为取消事件协议号;驳回时为 null
dceDigestValuestring | null授权 XML 的签名摘要;驳回与 Cancelada 时为 null

dceLinkDace 在授权 / 取消回调里为按 chave 懒渲染的 DACE 链接(投递时刻不渲染,首次下载才渲染归档);两个链接与查询接口同形(/openapi/files/{kind}/{ref}?token=...),有效期取租户级设置(缺省 7 天),见文件下载

验证结论载荷

二级 SEFAZ 核验落定后,平台向你的 webhook 地址推送 invoice.verify.completed这是最终结论的唯一推送通道chave 查询 可作轮询兜底。

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

信封:

字段类型出现性说明
versionstring恒有载荷 schema 版本,当前 1.0
event_idstring恒有事件 id,幂等键,重试间不变
event_typestring恒有invoice.verify.completed
occurred_atstring恒有事件时间,ISO-8601 UTC
dataobject恒有结论主体,见下

data

字段类型出现性说明
chaveAcessostring恒有被验证发票的 44 位访问密钥,回关联你提交记录的键
validationStatusstring恒有最终结论:VALIDATED / REJECTED / VALIDATION_ERROR,见 NF-e 验证
statusstring恒有SEFAZ 税务状态:Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida
cStatstring | null可空SEFAZ 原始返回码(如 100 已授权、101 已取消);未触达 SEFAZ 时为 null
xMotivostring | null可空SEFAZ 原始返回说明(葡语原文)
protocoloobject | null可空协议对象(numerodigestValue),取自官方记录
dataAutorizacaostring | null可空SEFAZ 授权时间,ISO-8601 UTC
eventos[]array恒有(可为空)该发票已登记的税务事件(取消、更正函);无则为空数组
verifiedAtstring恒有二级核验完成时间,ISO-8601 UTC
reasonstring仅失败时 REJECTED / VALIDATION_ERROR 出现;解释结论的英文稳定文案
validationStatus终态处置
VALIDATED可放行(发货、结算)
REJECTED不可放行;reason 说明 SEFAZ 判定(已取消 / 否决 / 作废 / 查无 / 协议不符)
VALIDATION_ERROR平台侧核验失败,非发票判定;稍后经 XML 验证 带请求头 forceRevalidate: true 重新提交

webhook 载荷只含语言无关的枚举值,无 *Description 字段。展示文案由接收方自理。

重试与熔断

  • 返回任意 2xx 即视为投递成功。其他状态码、网络异常或 10 秒内未响应均计为失败。
  • 失败的投递在首发之后按退避重试:
text
1 分钟、5 分钟、30 分钟、2 小时、6 小时
  • 共 5 次重试(总计 6 次尝试,跨度约 8.6 小时)。之后投递进入死信队列,可联系平台人工重推。每次尝试都会记录 HTTP 状态、耗时与响应摘要,是排查「没收到回调」的事实依据。
  • 熔断:连续投递失败(默认 10 次)会自动停用该 webhook。停用期间新事件不再为它入队。重新调用 Webhook 注册(或在控制台重新校验并保存地址)即恢复投递并清零失败计数;停用期间产生的事件请联系平台回放。
  • 每次尝试前都会重读 webhook 地址与 token,因此重新登记会在下一次重试时生效。

接收方要求

  1. 10 秒内返回 2xx。 最佳实践:先持久化原始投递,立即返回 2xx,再异步处理。
  2. 校验来源:对收到的原始字节复算 HMAC,并把请求头 token 与注册值比对。
  3. 按事件 id 去重X-Tffiscal-Event-Id,信封载荷里另有 event_id)。
  4. 公网地址:可从互联网访问的 http / https 绝对地址,最长 500 字符。回环、内网与链路本地地址在登记时会被拒绝。
  5. 响应测试投递:在控制台保存地址时会发出 X-Tffiscal-Event: webhook.verify 的事件,直接返回 2xx 即可,无需业务处理。
  6. 业务动作以结果字段(nfeStatuscteStatusdceStatusvalidationStatus)为准,切勿只依赖同步接口响应。
  7. 不要假定可选字段一定存在:dceChaveAcesso 可能为 nullreason 仅失败时出现,NF-e 的无值字段以空值形式出现。

排障

收不到回调?

确认地址是公网可达的 https/http 地址且在 10 秒内返回 2xx。平台按退避重试,连续失败后停用该 webhook;重新调用注册接口即可恢复投递。

验签总是失败?

最常见原因是先反序列化载荷再重序列化后才计算 HMAC,导致字段顺序或空白改变。务必对收到的原始字节做哈希,并原样拼接请求头 X-Tffiscal-Timestamp 的值。

同一事件收到两次?

这是至少一次投递的正常现象。请按事件 id 去重;投递 id 每个目标不同,不能用于去重。