TF Fiscal
开发文档

NF-e 验证

第三方 NF-e 的两级验证:XML 验证、chave 查询、经 webhook 推送的最终结论、幂等策略与错误模型。

能力概述

平台对第三方 NF-e(模型 55)执行两级验证

内容时机
一级(同步)XML 本地校验:语法、NF-e 4.00 XSD 版式、数字签名与证书归属、44 位访问密钥(chave)结构及与票面一致性、授权协议自洽提交请求即返回
二级(异步)向 SEFAZ(州税务局)实时核验真伪:授权状态、取消事件、协议号与摘要与官方记录比对一级通过后执行;结论经 webhook 推送(见 接收最终结论

另提供独立的 chave 查询:按访问密钥查询发票票面数据,纯查询接口,不含验证结论。

两个端点都需要三个签名请求头,见 认证与签名。响应为裸结构(不带平台信封);请求级错误为裸 {code, message} 对象。

验证生命周期

text
提交 XML ──► 一级校验
├─ 阻断错误 ─────────────────► REJECTED (终态)
└─ 通过 ──► PENDING_SEFAZ ──► VALIDATING(二级)
├──► VALIDATED (终态)
├──► REJECTED (终态,带 reason)
└──► VALIDATION_ERROR(可重试,带 reason)
validationStatus含义
VALIDATEDSEFAZ 确认发票真实有效(已授权、无取消事件、协议一致)
REJECTED验证不通过:一级阻断错误,或 SEFAZ 判定已取消 / 否决 / 作废 / 查无此票 / 协议不符,附 reason
VALIDATION_ERROR验证未能完成(SEFAZ 限流或查询重试耗尽)。并非对发票本身的判定,可稍后重新提交

对接前提(一次性)

  1. 应用凭证:申请应用并领取 app_secret仅回显一次,请妥善保存;如泄漏可申请轮换。
  2. 接口订阅:平台为你的应用开通 POST /openapi/v3/consultas/nf-e/xmlGET /openapi/v3/consultas/nf-e/{chave}
  3. webhook 接收地址:通过 注册 Webhook 登记回调 URL,并订阅事件 invoice.verify.completed(二级结论的唯一推送通道)。保存地址时平台会立即发送一条 event_type=webhook.verify 的测试推送,接收端须返回 2xx 才能保存成功(该测试事件直接回 200 即可,无需处理)。

端点

端点用途
POST /openapi/v3/consultas/nf-e/xml提交 NF-e XML 原文;返回一级结论与票面数据,并启动二级
GET /openapi/v3/consultas/nf-e/{chave}按访问密钥纯查询;返回票面数据,不含验证结论

接收最终结论

二级 SEFAZ 核验落定后,平台向已注册的 webhook URL 发起 POST。这是最终结论的唯一推送通道;chave 查询可作轮询兜底。

请求头

请求头说明
X-Tffiscal-Eventinvoice.verify.completed
X-Tffiscal-Event-Id事件 id,即幂等键:重试间不变,按此去重
X-Tffiscal-Delivery-Id投递 id,每次尝试唯一
X-Tffiscal-TimestampUnix 秒,每次尝试重新生成
X-Tffiscal-Signaturehex( HMAC-SHA256( secret, timestamp + "." + body ) ) 小写;secret = 你的 app_secret

载荷

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": "oAEEuC3tGmb2W7ZJxWkNVWuBHwY=" },
"dataAutorizacao": "2026-07-23T14:30:09Z",
"eventos": [],
"verifiedAt": "2026-07-23T17:16:23Z"
}
}

信封字段:

字段类型出现性说明
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(见 验证生命周期
statusstring恒有SEFAZ 税务状态:Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida
cStatstring | null可空SEFAZ 原始返回码(如 100 = 已授权、101 = 已取消);未触达 SEFAZ 时为 null
xMotivostring | null可空SEFAZ 原始返回说明(葡语原文)
protocoloobject | null可空Protocol 对象(numerodigestValue),结构同 XML 验证 响应;取自官方记录
dataAutorizacaostring | null可空SEFAZ 授权时间,ISO-8601 UTC
eventos[]array恒有(可为空)该发票已登记的税务事件(取消、更正函);无则为空数组
verifiedAtstring恒有二级核验完成时间,ISO-8601 UTC
reasonstring仅失败时 REJECTED / VALIDATION_ERROR 出现;解释结论的英文稳定文案

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

接收端要求

  1. 验签:复算 HMAC-SHA256(secret, timestamp + "." + 原始body) 与签名头比对。必须用收到的原始字节,不要先反序列化再重序列化(字段顺序变化会破坏签名)。
  2. 10 秒内返回 2xx。 其他情况(含超时)计为投递失败。
  3. 失败按 1m / 5m / 30m / 2h / 6h 退避重试 5 次,仍失败进死信队列(可联系平台人工重推)。
  4. event_id 幂等去重(重试与多目标扇出共用同一 event_id)。

结论处置

validationStatus终态?处置
VALIDATED可放行(发货、结算等)
REJECTED不可放行;reason 说明 SEFAZ 判定(已取消 / 否决 / 作废 / 查无 / 协议不符)
VALIDATION_ERROR平台侧核验失败,非发票判定;稍后带 forceRevalidate: true 重新提交

幂等与重验

  • XML 验证以 chave 为幂等键:验证进行中重复提交返回当前进度;终态结论 24 小时内复用(不重复消耗核验资源)。
  • 强制重验:在 XML 验证 端点加请求头 forceRevalidate: true(不参与签名)。chave 查询为固定 GET 形状,无重验通道;需强制重验请走 XML 端点重新提交。
  • 一级阻断的 REJECTED 无二级对象,重验须重新提交 XML。

错误模型

两种错误形状

业务与请求级错误(HTTP 400)为裸对象:

json
{ "code": 10015004, "message": "查无此票" }
字段类型说明
codeinteger平台错误码
messagestring人类可读说明,本地化(见 响应语言

鉴权层错误(HTTP 401 / 403 / 429)由平台网关在请求到达接口前产出,为平台信封形状:

json
{ "success": false, "errorType": 1, "code": 10009003, "message": "签名错误" }
字段类型说明
successboolean所有错误恒为 false
errorTypeinteger错误分类:1 API 错误、2 SEFAZ 驳回、3 系统异常、4 字段校验失败
codeinteger平台错误码
messagestring人类可读说明,本地化
dataobject | null错误时不填充

错误处理须兼容两种形状。稳妥做法:把响应体按 JSON 解析,从任一形状读取 code,以 success=false 或 HTTP >= 400 判定失败。

鉴权与授权错误

HTTPcode含义处置
40110009000缺少签名请求头(token / sign / timestamp修客户端:每次请求带全三个头
40110009001时间戳非法或时钟偏差超 ±300 秒校准时钟(NTP);每次请求重新生成时间戳,勿复用
40110009002token 无效核对 app_secret;如已轮换请更新配置
40110009003签名不匹配重新推导签名;见 排障 的清单
40310009004应用已停用联系平台
40310009015应用未生效(待审批或已驳回)等待审批 / 联系平台
40310009014集成商账户已停用联系平台
40310009005接口未订阅为所调端点申请订阅
42910009006超出限流额度退避重试;平滑调用频率。限流按应用与接口组两级实施

重试指引:401 与 403 属配置错误,不修复就重试没有意义,还可能触发限流。429 可重试,用指数退避(初始 1 秒,倍增至 30 秒上限,加抖动)。

请求级与校验错误

详见各端点页:

  • XML 验证:请求级错误 10015000 到 10015003(HTTP 400,裸形状,请求未进入校验流程),以及一级校验错误目录 XML_MALFORMEDXML_VERSION_UNSUPPORTED(HTTP 200,位于 validation.errors[])。
  • chave 查询:10015104(chave 非法)与 10015004(查无此票),HTTP 400,裸形状。
  • 二级结论不是 HTTP 错误,见 结论处置

HTTP 状态速查

HTTP场景响应形状
200请求受理成功,含校验不通过(看 validation 块)裸数据
400请求本身非法(空体 / 超限 / DTD / 编码 / chave 非法 / 查无此票){code, message}
401鉴权失败(token / sign / timestamp)平台信封
403应用停用 / 未生效 / 集成商停用 / 未订阅平台信封
429超出限流额度平台信封
5xx平台侧故障退避重试;持续失败请携带失败请求的 timestamp 与路径联系平台

响应语言

message*Description 字段随请求语言返回:Language 头(zh / en / pt / es)优先,其次 Accept-Language(支持 pt-BR 与 q 值)。未提供语言头时默认葡语(pt)

程序判断一律使用语言无关的枚举字段(validationStatusstatuserrors[].code),永远不要匹配描述文案。

联调清单

  1. 领取 app_secret;用 sign-helper 与本地签名实现对拍(POST 带 XML body 与 GET 空 body 各一次)。
  2. POST /openapi/v3/consultas/nf-e/xml 提交一张真实授权 nfeProc:HTTP 200,五项校验全 true,validationStatus=PENDING_SEFAZ
  3. 接收 invoice.verify.completed webhook:验签通过、按 event_id 去重、拿到 VALIDATED
  4. chave 查询:查已提交过的 chave 得到 200 票面数据;查不存在的 chave 得到 400 + 10015004;校验位错误的 chave 得到 400 + 10015104。
  5. 逆向用例:提交内容被篡改的 XML(SIGNATURE_INVALID);提交无 protNFe 的 XML(仅 warning,照常受理)。
  6. 幂等验证:同 chave 重复提交得到复用结论;带 forceRevalidate: true 触发重验,新结论经 webhook 推送。
  7. 失败路径:故意用错误 sign 调用确认 401(code 10009003);调用未订阅端点确认 403(code 10009005)。

排障

签名始终对不上(401,code 10009003)?

按出现频率依次检查:

  1. body 拼接前未去除 CR/LF;
  2. GET 请求把 body 拼成了 "null"(应为空字符串);
  3. path/openapi 前缀,或带上了查询串;
  4. sign 用了大写(必须小写十六进制);
  5. 拼接用的 timestamp 与请求头不是同一个字符串(两次生成);
  6. body 字节被重新编码(必须对线上实际发送的字节做哈希,UTF-8)。

HTTP 200 但发票是假的?

一级只判断 XML 本身是否自洽。真伪由二级对 SEFAZ 核验裁定并经 webhook 推送;业务放行动作(发货、结算)应挂在 webhook 的 VALIDATED 上,绝不能只看同步响应。

webhook 一直验签失败?

最常见原因是先反序列化载荷再重新序列化后参与 HMAC 计算,字段顺序或空白变了。必须对收到的原始字节做哈希。

VALIDATION_ERROR 是发票有问题吗?

不是。它表示平台核验通道故障(如 SEFAZ 限流),发票本身未被判定。稍后带 forceRevalidate: true 重新提交即可。

时钟偏差(401,code 10009001)?

你的服务器时钟与平台相差超过 300 秒。请用 NTP。永远不要跨请求缓存或复用时间戳。