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.authorized | NF-e 被 SEFAZ 授权(cStat 100) | NF-e 结果对象,nfeStatus = Autorizada |
invoice.rejected | NF-e 被 SEFAZ 驳回 | NF-e 结果对象,nfeStatus = Negada,原因在 nfeMotivoStatus |
invoice.canceled | NF-e 作废在 SEFAZ 登记完成(cStat 135) | 事件信封,data 含 invoice_id / chave / protocolo |
invoice.cce.registered | NF-e 更正函(CC-e)登记完成 | 事件信封,data 含 invoice_id / chave / n_seq。预留:CC-e 登记目前为同步返回,本事件暂不推送 |
invoice.verify.completed | NF-e 验证得出最终结论(VALIDATED / REJECTED / VALIDATION_ERROR) | 事件信封,结论在 data |
cte.authorized | CT-e 授权通过(cStat 100) | CT-e 结果对象,cteStatus = Autorizada |
cte.rejected | CT-e 被 SEFAZ 驳回(cStat 非 100) | CT-e 结果对象,cteStatus = Negada,cteMotivoStatus 为 cStat - 原因 |
cte.canceled | CT-e 取消登记完成(cStat 135) | CT-e 结果对象,cteStatus = Cancelada |
cte.event.registered | CT-e 其他事件登记成功(更正函、送达凭证、送达失败、服务异议及其撤销) | 事件信封,data 含 event_code / n_seq / protocolo |
dce.authorized | DC-e 授权通过(cStat 100) | DC-e 结果对象,dceStatus = Autorizada |
dce.rejected | DC-e 被 SEFAZ 驳回,或发行任务终局失败(含单据编号前的失败) | DC-e 结果对象,dceStatus = Negada |
dce.canceled | DC-e 取消登记成功(cStat 135 / 136 / 155) | DC-e 结果对象,dceStatus = Cancelada |
dce.cancel_rejected | DC-e 取消被 SEFAZ 拒绝,或取消任务终局失败 | DC-e 结果对象,dceStatus = CancelamentoNegado,单据仍为已授权 |
载荷有两种形状:
- 单据结果对象:裸 JSON 对象,首字段为
tipo(NF-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 错误返回,而是体现为查询状态
Negada与invoice.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-Timestamp | Unix 秒,每次尝试重新生成 |
X-Tffiscal-Signature | hex( HMAC-SHA256( app_secret, timestamp + "." + body ) ),小写 |
token | 你在 Webhook 注册 时提交的校验令牌,原样回传 |
x-token | 与 token 同值,校验任一即可 |
验签
用你的 app_secret 对字符串 timestamp + "." + rawBody 计算 HMAC-SHA256,并与请求头 X-Tffiscal-Signature 做常量时间比较。必须用收到的原始字节:先反序列化再重序列化会改变字段顺序或空白,导致签名失败。时间戳过旧的投递应拒绝(容忍 5 分钟是合理取值),以防重放。
建议校验签名;至少要把请求头 token 与注册值比对。
Node.js:
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:
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.authorized 与 invoice.rejected。通过 Webhook 注册 登记;开票流程见 NF-e。
授权:
{"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"}
驳回:
{"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"}
| 字段 | 类型 | 说明 |
|---|---|---|
tipo | string | 固定 NF-e |
empresaId | string | 主体标识 |
nfeId | string | 开票时传入的 id |
nfeStatus | string | Autorizada / Negada |
nfeMotivoStatus | string | 驳回原因:SEFAZ 返回码 + 描述;授权时无值 |
nfeLinkDanfe | string | DANFE PDF 下载地址(见文件下载;回调时刻即可用,首次下载触发渲染);驳回时无值 |
nfeLinkXml | string | 授权 XML 下载地址;驳回时无值 |
nfeNumero | string | 票号 |
nfeSerie | string | 系列号 |
nfeChaveAcesso | string | 44 位访问密钥 |
nfeDataEmissao | string | 开票时间,ISO-8601 UTC |
nfeDataAutorizacao | string | 授权时间;驳回时无值 |
nfeNumeroProtocolo | string | 授权协议号;驳回时无值 |
nfeDigestValue | string | 签名摘要,当前不提供 |
无值的字段以空值形式出现在载荷中。
NF-e 作废与更正函
invoice.canceled 使用事件信封,data 含 invoice_id(平台发票标识)、chave(44 位访问密钥)与 protocolo(作废事件协议号)。作废本身由 NF-e 作废 同步确认,查询状态随之变为 Cancelada。
invoice.cce.registered 在目录中预留(data:invoice_id / chave / n_seq)。更正函登记接口同步返回协议号,目前不推送该回调。
CT-e 载荷
复用 NF-e 的登记与请求头;发行流程见 CT-e。结果对象 tipo = CT-e,字段顺序固定。
| 事件 | 触发 | cteStatus |
|---|---|---|
cte.authorized | 授权 100 | Autorizada |
cte.rejected | SEFAZ 驳回(cStat 非 100) | Negada(cteMotivoStatus 为 cStat - 原因);任务终态失败(Falha)不发回调,请经查询接口获知 |
cte.canceled | 取消 135 | Cancelada |
cte.event.registered | 其他事件登记成功 | 事件信封,data 含 event_code / n_seq / protocolo |
授权:
{"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": "..."}
| 字段 | 类型 | 说明 |
|---|---|---|
tipo | string | 固定 CT-e |
empresaId | string | 主体标识 |
cteId | string | 发行时传入的 id |
cteStatus | string | Autorizada / Negada / Cancelada |
cteMotivoStatus | string | null | 驳回时为 cStat - 原因;其余为 null |
cteLinkDacte | string | null | DACTE PDF 下载地址(首次下载才渲染);驳回时为 null |
cteLinkXml | string | null | 授权 XML 下载地址;驳回时为 null |
cteNumero | string | 单据号 |
cteSerie | string | 系列号 |
cteChaveAcesso | string | 44 位访问密钥 |
cteDataEmissao | string | 发行时间,ISO-8601 UTC |
cteDataAutorizacao | string | null | 授权时间;驳回时为 null |
cteNumeroProtocolo | string | null | 授权协议号;驳回时为 null |
cteDigestValue | string | null | 授权 XML 的签名摘要 |
cteLinkDacte 与 cteLinkXml 在授权回调到达时即可用;链接不需要签名头、302 跳转,见文件下载。
事件登记(cte.event.registered),以送达凭证(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"}}
| 字段 | 类型 | 说明 |
|---|---|---|
event_code | string | SEFAZ 事件码,如 110110 更正函、110180 送达凭证 |
n_seq | string | 事件序号 |
protocolo | string | 事件协议号 |
DC-e 载荷
复用 NF-e 的登记与请求头;发行流程见 DC-e。结果对象 tipo = DC-e,14 个字段顺序固定。
| 事件 | 触发 | dceStatus |
|---|---|---|
dce.authorized | 授权 100 | Autorizada |
dce.rejected | SEFAZ 驳回或任务终局失败(含单据编号前的失败,此时无 chave) | Negada(dceMotivoStatus 为 cStat - 原因;终局失败无 cStat 时只有原因) |
dce.canceled | 取消登记成功(135 / 136 / 155) | Cancelada(dceDataAutorizacao 为取消登记时刻、dceNumeroProtocolo 为取消事件协议号) |
dce.cancel_rejected | 取消被 SEFAZ 拒绝或取消任务终局失败 | CancelamentoNegado(单据仍为已授权,带授权协议号 / 摘要 / XML 链接) |
授权:
{"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="}
驳回:
{"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,单据事实字段为 null,dceMotivoStatus 为失败原因,dceDataEmissao 为受理时刻。请以 dceId 关联,不要假定 dceChaveAcesso 一定有值:
{"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}
取消成功:
{"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}
取消被拒:
{"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="}
| 字段 | 类型 | 说明 |
|---|---|---|
tipo | string | 固定 DC-e |
empresaId | string | 主体标识 |
dceId | string | 发行时传入的 id;所有事件的关联键,单据未编号时也有值 |
dceStatus | string | Autorizada / Negada / Cancelada / CancelamentoNegado |
dceMotivoStatus | string | null | SEFAZ 驳回时为 cStat - 原因;无 cStat 的终局失败只有原因;成功时为 null |
dceLinkDace | string | null | DACE PDF 下载地址,首次下载才渲染;驳回与 CancelamentoNegado 时为 null |
dceLinkXml | string | null | 授权 XML 下载地址;驳回时为 null |
dceNumero | string | null | 单据号;未编号时为 null |
dceSerie | string | null | 系列号;未编号时为 null |
dceChaveAcesso | string | null | 44 位访问密钥;未编号时为 null |
dceDataEmissao | string | 发行时间,ISO-8601 UTC(编号前失败时为受理时刻) |
dceDataAutorizacao | string | null | 授权时间;Cancelada 时为取消登记时刻;驳回时为 null |
dceNumeroProtocolo | string | null | 授权协议号;Cancelada 时为取消事件协议号;驳回时为 null |
dceDigestValue | string | null | 授权 XML 的签名摘要;驳回与 Cancelada 时为 null |
dceLinkDace 在授权 / 取消回调里为按 chave 懒渲染的 DACE 链接(投递时刻不渲染,首次下载才渲染归档);两个链接与查询接口同形(/openapi/files/{kind}/{ref}?token=...),有效期取租户级设置(缺省 7 天),见文件下载。
验证结论载荷
二级 SEFAZ 核验落定后,平台向你的 webhook 地址推送 invoice.verify.completed。这是最终结论的唯一推送通道;chave 查询 可作轮询兜底。
{"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)"}}
信封:
| 字段 | 类型 | 出现性 | 说明 |
|---|---|---|---|
version | string | 恒有 | 载荷 schema 版本,当前 1.0 |
event_id | string | 恒有 | 事件 id,幂等键,重试间不变 |
event_type | string | 恒有 | invoice.verify.completed |
occurred_at | string | 恒有 | 事件时间,ISO-8601 UTC |
data | object | 恒有 | 结论主体,见下 |
data:
| 字段 | 类型 | 出现性 | 说明 |
|---|---|---|---|
chaveAcesso | string | 恒有 | 被验证发票的 44 位访问密钥,回关联你提交记录的键 |
validationStatus | string | 恒有 | 最终结论:VALIDATED / REJECTED / VALIDATION_ERROR,见 NF-e 验证 |
status | string | 恒有 | SEFAZ 税务状态:Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida |
cStat | string | null | 可空 | SEFAZ 原始返回码(如 100 已授权、101 已取消);未触达 SEFAZ 时为 null |
xMotivo | string | null | 可空 | SEFAZ 原始返回说明(葡语原文) |
protocolo | object | null | 可空 | 协议对象(numero、digestValue),取自官方记录 |
dataAutorizacao | string | null | 可空 | SEFAZ 授权时间,ISO-8601 UTC |
eventos[] | array | 恒有(可为空) | 该发票已登记的税务事件(取消、更正函);无则为空数组 |
verifiedAt | string | 恒有 | 二级核验完成时间,ISO-8601 UTC |
reason | string | 仅失败时 | 仅 REJECTED / VALIDATION_ERROR 出现;解释结论的英文稳定文案 |
validationStatus | 终态 | 处置 |
|---|---|---|
VALIDATED | 是 | 可放行(发货、结算) |
REJECTED | 是 | 不可放行;reason 说明 SEFAZ 判定(已取消 / 否决 / 作废 / 查无 / 协议不符) |
VALIDATION_ERROR | 否 | 平台侧核验失败,非发票判定;稍后经 XML 验证 带请求头 forceRevalidate: true 重新提交 |
webhook 载荷只含语言无关的枚举值,无 *Description 字段。展示文案由接收方自理。
重试与熔断
- 返回任意 2xx 即视为投递成功。其他状态码、网络异常或 10 秒内未响应均计为失败。
- 失败的投递在首发之后按退避重试:
1 分钟、5 分钟、30 分钟、2 小时、6 小时
- 共 5 次重试(总计 6 次尝试,跨度约 8.6 小时)。之后投递进入死信队列,可联系平台人工重推。每次尝试都会记录 HTTP 状态、耗时与响应摘要,是排查「没收到回调」的事实依据。
- 熔断:连续投递失败(默认 10 次)会自动停用该 webhook。停用期间新事件不再为它入队。重新调用 Webhook 注册(或在控制台重新校验并保存地址)即恢复投递并清零失败计数;停用期间产生的事件请联系平台回放。
- 每次尝试前都会重读 webhook 地址与
token,因此重新登记会在下一次重试时生效。
接收方要求
- 10 秒内返回 2xx。 最佳实践:先持久化原始投递,立即返回 2xx,再异步处理。
- 校验来源:对收到的原始字节复算 HMAC,并把请求头
token与注册值比对。 - 按事件 id 去重(
X-Tffiscal-Event-Id,信封载荷里另有event_id)。 - 公网地址:可从互联网访问的
http/https绝对地址,最长 500 字符。回环、内网与链路本地地址在登记时会被拒绝。 - 响应测试投递:在控制台保存地址时会发出
X-Tffiscal-Event: webhook.verify的事件,直接返回 2xx 即可,无需业务处理。 - 业务动作以结果字段(
nfeStatus、cteStatus、dceStatus、validationStatus)为准,切勿只依赖同步接口响应。 - 不要假定可选字段一定存在:
dceChaveAcesso可能为null,reason仅失败时出现,NF-e 的无值字段以空值形式出现。
排障
收不到回调?
确认地址是公网可达的 https/http 地址且在 10 秒内返回 2xx。平台按退避重试,连续失败后停用该 webhook;重新调用注册接口即可恢复投递。
验签总是失败?
最常见原因是先反序列化载荷再重序列化后才计算 HMAC,导致字段顺序或空白改变。务必对收到的原始字节做哈希,并原样拼接请求头 X-Tffiscal-Timestamp 的值。
同一事件收到两次?
这是至少一次投递的正常现象。请按事件 id 去重;投递 id 每个目标不同,不能用于去重。
