NF-e 验证
第三方 NF-e 的两级验证:XML 验证、chave 查询、经 webhook 推送的最终结论、幂等策略与错误模型。
能力概述
平台对第三方 NF-e(模型 55)执行两级验证:
| 级 | 内容 | 时机 |
|---|---|---|
| 一级(同步) | XML 本地校验:语法、NF-e 4.00 XSD 版式、数字签名与证书归属、44 位访问密钥(chave)结构及与票面一致性、授权协议自洽 | 提交请求即返回 |
| 二级(异步) | 向 SEFAZ(州税务局)实时核验真伪:授权状态、取消事件、协议号与摘要与官方记录比对 | 一级通过后执行;结论经 webhook 推送(见 接收最终结论),chave 查询可读到当前状态作轮询兜底 |
另提供独立的 chave 查询:按访问密钥查询发票票面数据;对你经 XML 验证提交过的 chave,同时返回当前的 validation 块,webhook 未送达时可用它轮询结论。
两个端点都需要三个签名请求头,见 认证与签名。响应为无包装结构(不带平台统一响应体);请求级错误为无包装的 {code, message} 对象。
验证生命周期
提交 XML ──► 一级校验├─ 阻断错误 ─────────────────► REJECTED (终态)└─ 通过 ──► PENDING_SEFAZ ──► VALIDATING(二级)├──► VALIDATED (终态)├──► REJECTED (终态,带 reason)└──► VALIDATION_ERROR(可重试,带 reason)
| validationStatus | 含义 |
|---|---|
VALIDATED | SEFAZ 确认发票真实有效(已授权、无取消事件、协议一致) |
REJECTED | 验证不通过:一级阻断错误,或 SEFAZ 判定已取消 / 否决 / 作废 / 查无此票 / 协议不符,附 reason |
VALIDATION_ERROR | 验证未能完成(SEFAZ 限流或查询重试耗尽)。并非对发票本身的判定,可稍后带 forceRevalidate: true 重新提交 |
对接前提(一次性)
- 应用凭证:申请应用并领取
app_secret。仅回显一次,请妥善保存;如泄漏可申请轮换。 - 接口订阅:平台为你的应用开通
POST /openapi/v3/consultas/nf-e/xml与GET /openapi/v3/consultas/nf-e/{chave}。 - 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} | 按访问密钥查询;返回票面数据,提交过验证的 chave 另附当前的 validation 块 |
接收最终结论
二级 SEFAZ 核验落定后,平台向已注册的 webhook URL 发起 POST。这是最终结论的唯一推送通道;推送未送达时,chave 查询 返回当前的 validationStatus,可作轮询兜底。
请求头
| 请求头 | 说明 |
|---|---|
X-Tffiscal-Event | invoice.verify.completed |
X-Tffiscal-Event-Id | 事件 id,即幂等键:重试间不变,按此去重 |
X-Tffiscal-Delivery-Id | 投递 id,每个目标地址一个,对该目标的重试间不变(去重请用事件 id,不要用它) |
X-Tffiscal-Timestamp | Unix 秒,每次尝试重新生成 |
X-Tffiscal-Signature | hex( HMAC-SHA256( secret, timestamp + "." + body ) ) 小写;应用级 webhook 地址的 secret 即你的 app_secret(回落到租户级地址时,用该地址配置的 secret) |
载荷
{"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"}}
统一事件体字段:
| 字段 | 类型 | 出现性 | 说明 |
|---|---|---|---|
| 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(见 验证生命周期) |
| status | string | null | 可空 | SEFAZ 税务状态:Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida;VALIDATION_ERROR 时为 null(未拿到 SEFAZ 结论) |
| cStat | string | null | 可空 | SEFAZ 原始返回码(如 100 = 已授权、101 = 已取消);未触达 SEFAZ 时为 null |
| xMotivo | string | null | 可空 | SEFAZ 原始返回说明(葡语原文) |
| protocolo | object | null | 可空 | Protocol 对象(numero、digestValue),结构同 XML 验证 响应;取自官方记录 |
| dataAutorizacao | string | null | 可空 | SEFAZ 授权时间,ISO-8601 UTC |
| eventos[] | array | 恒有(可为空) | 该发票已登记的税务事件(取消、更正函);无则为空数组 |
| verifiedAt | string | 恒有 | 二级核验完成时间,ISO-8601 UTC |
| reason | string | 仅失败时 | 仅 REJECTED / VALIDATION_ERROR 出现;解释结论的英文稳定文案 |
注意: webhook 载荷只含语言无关的枚举值,无
*Description字段。展示文案由接收方自理。
接收端要求
- 验签:复算
HMAC-SHA256(secret, timestamp + "." + 原始body)与签名头比对。必须用收到的原始字节,不要先反序列化再重序列化(字段顺序变化会破坏签名)。 - 10 秒内返回 2xx。 其他情况(含超时)计为投递失败。
- 失败按 1m / 5m / 30m / 2h / 6h 退避重试 5 次,仍失败可联系平台人工重推。
- 按
event_id幂等去重(重试与多目标扇出共用同一 event_id)。 - 同一个 chave 可能先后产生多条事件:带
forceRevalidate: true的重验、超过 24 小时后的重复提交、chave 查询触发的后台复核,每一次都会以新的event_id推送一条新事件。请以occurred_at最新的一条为准。
结论处置
| validationStatus | 终态? | 处置 |
|---|---|---|
VALIDATED | 是 | 可放行(发货、结算等) |
REJECTED | 是 | 不可放行;reason 说明 SEFAZ 判定(已取消 / 否决 / 作废 / 查无 / 协议不符) |
VALIDATION_ERROR | 是,可重试 | 平台侧核验失败,非发票判定。与其他结论一样 24 小时内复用,请稍后带 forceRevalidate: true 重新提交 |
幂等与重验
- XML 验证以 chave(提交的 XML 里的访问密钥)为幂等键:
- 二级进行中(
PENDING_SEFAZ/VALIDATING)重复提交,返回当前进度,不会再发起一次核验。此时带forceRevalidate: true也不会让进行中的核验重新开始或提前完成。 - 终态结论(
VALIDATED/REJECTED/VALIDATION_ERROR)自得出之时起 24 小时内复用:窗口内重复提交直接返回该结论,不再访问 SEFAZ。超过 24 小时后,普通的重复提交会重新完整验证一遍(一级 + 二级)。 - 复用的响应反映的是当初提交的那份 XML(票面解析数据、一级检查项、errors 与 warnings),
validationStatus则是当前值。窗口内为同一 chave 提交修正后的 XML,请带forceRevalidate: true,否则拿到的仍是上一次的结果。
- 二级进行中(
- 强制重验:在 XML 验证 端点加请求头
forceRevalidate: true(不参与签名),跳过复用、重新执行一级和二级,新结论作为一条新事件经 webhook 推送。 - chave 查询:无法强制复核;只在最近一次 SEFAZ 答复超过 24 小时时自动安排复核(见 chave 查询)。
- 一级阻断的
REJECTED无二级对象,重验须重新提交 XML(24 小时窗口内需带forceRevalidate: true)。 - 验证记录归属于你的集成商账户:复用、chave 查询与 webhook 只看本账户的提交记录。其他集成商验证同一张票互不影响,各自独立核验、各自出结论。
错误模型
两种错误格式
业务与请求级错误(HTTP 400)为无包装的 JSON 对象:
{ "code": 10015004, "message": "未查询到该发票:35260764962869000108550990001366171195929648" }
| 字段 | 类型 | 说明 |
|---|---|---|
| code | integer | 平台错误码 |
| message | string | 人类可读说明,本地化(见 响应语言) |
鉴权层错误(HTTP 401 / 403 / 429)由平台网关在请求到达接口前产出,为平台统一响应体格式:
{ "success": false, "errorType": 1, "code": 10009003, "message": "签名错误" }
| 字段 | 类型 | 说明 |
|---|---|---|
| success | boolean | 所有错误恒为 false |
| errorType | integer | 错误分类:1 API 错误、2 SEFAZ 驳回、3 系统异常、4 字段校验失败 |
| code | integer | 平台错误码 |
| message | string | 人类可读说明,本地化 |
| data | object | null | 错误时不填充 |
错误处理须兼容两种格式。稳妥做法:把响应体按 JSON 解析,从任一格式读取 code,以 success=false 或 HTTP >= 400 判定失败。
鉴权与授权错误
| HTTP | 错误码 | 含义 | 处置 |
|---|---|---|---|
| 401 | 10009000 | 缺少签名请求头(token / sign / timestamp) | 修客户端:每次请求带全三个头 |
| 401 | 10009001 | 时间戳非法或时钟偏差超 ±300 秒 | 校准时钟(NTP);每次请求重新生成时间戳,勿复用 |
| 401 | 10009002 | token 无效 | 核对 app_secret;如已轮换请更新配置 |
| 401 | 10009003 | 签名不匹配 | 重新推导签名;见 排障 的清单 |
| 403 | 10009004 | 应用已停用 | 联系平台 |
| 403 | 10009015 | 应用未生效(待审批或已驳回) | 等待审批 / 联系平台 |
| 403 | 10009014 | 集成商账户已停用 | 联系平台 |
| 403 | 10009005 | 接口未订阅 | 为所调端点申请订阅 |
| 429 | 10009006 | 超出限流额度 | 退避重试;平滑调用频率。限流按应用与接口组两级实施 |
| 413 | 10009027 | 请求体超过网关大小上限 | XML 验证最多接受 1 MB;确认每次只发送一张 NF-e |
重试指引:401 与 403 属配置错误,不修复就重试没有意义,还可能触发限流。429 可重试,用指数退避(初始 1 秒,倍增至 30 秒上限,加抖动)。
请求级与校验错误
详见各端点页:
- XML 验证:请求级错误 10015000 到 10015003(HTTP 400,无包装格式,请求未进入校验流程),以及一级校验错误目录
XML_MALFORMED到XML_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) | 平台统一响应体 |
| 413 | 请求体超过网关大小上限 | 平台统一响应体 |
| 403 | 应用停用 / 未生效 / 集成商停用 / 未订阅 | 平台统一响应体 |
| 429 | 超出限流额度 | 平台统一响应体 |
| 5xx | 平台侧故障 | 退避重试;持续失败请携带失败请求的 timestamp 与路径联系平台 |
响应语言
message 与 *Description 字段随请求语言返回:Language 头(zh / en / pt / es)优先,其次 Accept-Language(支持 pt-BR 与 q 值)。未提供语言头时默认葡语(pt)。
程序判断一律使用语言无关的枚举字段(validationStatus、status、errors[].code),永远不要匹配描述文案。
联调清单
- 领取
app_secret;用 sign-helper 与本地签名实现对拍(POST 带 XML body 与 GET 空 body 各一次)。 POST /openapi/v3/consultas/nf-e/xml提交一张真实授权 nfeProc:HTTP 200,五项校验全 true,validationStatus=PENDING_SEFAZ。- 接收
invoice.verify.completedwebhook:验签通过、按event_id去重、拿到VALIDATED。 - chave 查询:查已提交过的 chave 得到 200 票面数据与
validation块(validationStatus与 webhook 结论一致);查不存在的 chave 得到 400 + 10015004;校验位错误的 chave 得到 400 + 10015104。 - 逆向用例:提交内容被篡改的 XML(
SIGNATURE_INVALID);提交无 protNFe 的 XML(仅 warning,照常受理)。 - 幂等验证:同 chave 重复提交得到复用结论;带
forceRevalidate: true触发重验,新结论经 webhook 推送。 - 失败路径:故意用错误
sign调用确认 401(code 10009003);调用未订阅端点确认 403(code 10009005)。
排障
签名始终对不上(401,code 10009003)?
按出现频率依次检查:
- body 拼接前未去除 CR/LF;
- GET 请求把 body 拼成了
"null"(应为空字符串); path缺/openapi前缀,或带上了查询串;sign用了大写(必须小写十六进制);- 拼接用的
timestamp与请求头不是同一个字符串(两次生成); - body 字节被重新编码(必须对线上实际发送的字节做哈希,UTF-8)。
HTTP 200 但发票是假的?
一级只判断 XML 本身是否自洽。真伪由二级对 SEFAZ 核验裁定并经 webhook 推送;业务放行动作(发货、结算)应挂在 webhook 的 VALIDATED 上,绝不能只看同步响应。
同步响应里的 status 是 SEFAZ 的状态吗?
不是。首次响应的 status 取自你提交的 XML 自带的授权协议,此时还没有访问 SEFAZ。SEFAZ 核实过的状态随二级结论给出:webhook 的 status,或二级拿到答复后的 chave 查询。
REJECTED 且 status 为 NaoEncontrada,发票是假的吗?
不一定。这表示 SEFAZ 答复「查无此票」(cStat 217)。平台不会凭第一次 217 下结论:刚授权的发票需要一段时间才能在 SEFAZ 查到,平台会在几分钟内自动多次重查,仍是 217 才给出 REJECTED + NaoEncontrada。常见原因:
- 发票刚授权,SEFAZ 尚未同步完成:稍后带
forceRevalidate: true重新验证; - 发票是在 SEFAZ 测试环境开具的(
ambienteEmissao=Homologacao):二级查询的是生产环境,生产环境里没有测试票; - 该 chave 从未获得授权。
这与 chave 查询返回的 10015004 不同,后者表示平台的各数据源都不认识这个 chave。
webhook 一直验签失败?
最常见原因是先反序列化载荷再重新序列化后参与 HMAC 计算,字段顺序或空白变了。必须对收到的原始字节做哈希。
VALIDATION_ERROR 是发票有问题吗?
不是。它表示平台核验通道故障(如 SEFAZ 限流),发票本身未被判定。稍后带 forceRevalidate: true 重新提交即可。
时钟偏差(401,code 10009001)?
你的服务器时钟与平台相差超过 300 秒。请用 NTP。永远不要跨请求缓存或复用时间戳。
