跳转到正文
生产环境文档契约 1.5.169
正在核对线上版本…

回调通知 ​

平台向创建授权时提供的 callback_url 发送 JSON POST。回调是获取结果的主要方式, 状态查询只是没收到回调时的兜底。

HTTP 回调仅用于平台特批机构

普通机构的 callback_url 必须使用公网 HTTPS。只有平台超级管理员为所属机构开启安全例外后才允许公网 HTTP;该例外仍禁止本机和内网目标。HTTP 不提供 TLS 保护,接收端仍必须验签,并建议在创建授权时选择 output_encryption=SM4。

请求头 ​

头说明
X-RPA-Event事件类型
X-RPA-Delivery投递记录 ID;重试时保持不变
X-RPA-SignatureCallback Secret 对原始 Body 字节的 HMAC-SHA256

事件 ​

授权回调不受 callback_mode 影响

“授权阶段”不是第三种 callback_mode。只要本单配置了 callback_url, 每轮授权达到结果状态时,无论选择 AGGREGATE 还是 PER_PRODUCT, 都会发送 authorization.completed 或 authorization.failed。 callback_mode 只决定后续采集结果怎么通知。

回调类别发送条件事件
授权回调AGGREGATE 和 PER_PRODUCT 都发送authorization.completed、authorization.failed
整单结果回调仅 AGGREGATEorder.completed、order.failed
财税(TAX)回调PER_PRODUCT 且本单选择财税collection.tax.completed、collection.tax.failed、collection.tax.skipped
发票(INVOICE)回调PER_PRODUCT 且本单选择发票collection.invoice.completed、collection.invoice.failed、collection.invoice.skipped
PDF 回调PER_PRODUCT 且本单选择 PDF 报告collection.pdf.completed、collection.pdf.partial、collection.pdf.failed

callback_mode 在创建授权时选择:AGGREGATE 整单一次通知, PER_PRODUCT 按产品分别通知,适合把财税和发票分开保存、各自触发后续流程的系统。 因此,客户实际收到的业务回调为:

  • AGGREGATE:授权回调 + 整单结果回调。
  • PER_PRODUCT:授权回调 + 本单已选财税、发票、PDF 的各自结果回调。

从 API 1.5.168 起,整单结果回调等待本单所选交付结果完成;其中报告成功时应已可下载,失败时按失败结果通知。分产品模式中,税务和发票按各自数据就绪通知,不能据此判断报告已就绪;报告通知等待对应报告完成。回调重试会刷新可用下载链接,收到后仍应及时下载,链接过期可通过订单查询刷新。

分产品回调反映对应产品的结果,不代替整单核验结论。采用 PER_PRODUCT 且渠道开启收入核验时,收到产品结果后还应查询订单的 state 与 revenueCheck;平台不会因此额外切换成整单模式发送 order.failed。

平台按业务阶段产生这些事件,但网络重试可能造成乱序送达; 客户不能依赖实际到达顺序。

报文 ​

授权回调示例 ​

authorization.completed 表示税局身份验证与授权阶段成功,不表示财税或发票数据已可用。纯授权回调可以不包含顶层 attempt_id,但其 event_id 在已有执行批次时仍可含批次后缀。以下两个 JSON 是字段节选,完整报文见创建授权接口中的回调契约。

json
{
  "channelOrderNo": "BANK-20260812-0001",
  "orderNo": "1e3f2f55-8da4-4ba9-b941-84860eb1d243",
  "notifyType": 1,
  "event": "authorization.completed",
  "event_id": "550e8400-e29b-41d4-a716-446655440000:authorization.completed:a2ad4ec4-b878-42c5-b623-4d962f350e72",
  "status": 1,
  "data": {
    "external_order_id": "BANK-20260812-0001",
    "authorization_code": "550e8400-e29b-41d4-a716-446655440000",
    "job_id": "1e3f2f55-8da4-4ba9-b941-84860eb1d243",
    "state": "SUCCESS"
  }
}

采集结果回调示例 ​

下例为 AGGREGATE 整单成功回调。PER_PRODUCT 时, event 按上表分别为 collection.tax.*、collection.invoice.* 或 collection.pdf.*,并在顶层增加 callbackMode=PER_PRODUCT 和 product。

json
{
  "channelOrderNo": "BANK-20260812-0001",
  "orderNo": "1e3f2f55-8da4-4ba9-b941-84860eb1d243",
  "notifyType": 2,
  "event": "order.completed",
  "event_id": "550e8400-e29b-41d4-a716-446655440000:order.completed:a2ad4ec4-b878-42c5-b623-4d962f350e72",
  "attempt_id": "a2ad4ec4-b878-42c5-b623-4d962f350e72",
  "status": 1,
  "data": {}
}

数字字段对照 ​

notifyType、status 与 dataType 是为兼容既有接收端保留的固定数字枚举, 不表示回调的先后顺序。判断具体通知类型时,应优先读取语义更明确的 event。

字段值含义进一步判断
notifyType1授权阶段通知event 为 authorization.completed 或 authorization.failed
notifyType2采集或产品结果通知event 为 order.* 或 collection.*
status1本次通知成功成功结果仍可能带质量警告,须继续读取 data
status2本次通知未成功可能是 failed、skipped 或 PDF partial,以 event、errorMsg 和 data 为准
dataType0仅发票PER_PRODUCT 的发票通知也使用该值
dataType1仅税务PER_PRODUCT 的税务通知也使用该值
dataType2税务与发票采集范围同时包含税务与发票时使用;PDF 产品通知也使用该值

例如,上面的 notifyType=2、status=1 表示“采集结果通知且本次通知成功”; 具体是整单完成还是某个产品完成,仍由 event 判断。

PER_PRODUCT 报文中的 sequence 才是本轮采集内的平台通知序号,但回调可能乱序到达, 因此不能按 sequence 推断实际送达顺序,也不能用它代替 event_id 做幂等去重。

  • 采集或产品结果回调包含顶层 attempt_id;纯授权阶段回调可以没有该字段。
  • 创建授权时传入的 extras_data 对象以顶层 extrasData JSON 字符串回传;需要业务对象时再解析该字符串。
  • 非阻断部分采集可以发送成功终态,同时在数据中带 qualityStatus=WARNING、collectionResult=COMPLETED_WITH_WARNINGS 与质量明细;接收端不能只保存事件名。
  • 开启详版、简版在线报告,或申请 TAX_RISK 时,结果回调顶层会额外包含 generateHtmlReports=true 与同一份 htmlReports。
  • 订单启用 output_encryption=SM4 时,整个 Body 被替换为 SM4-GCM 信封。

正确的处理顺序 ​

顺序不能变

  1. 先验证签名(基于原始字节,即下文的「验签」)
  2. 解析外层 JSON;如为 SM4 信封,验证 GCM Tag 并解密
  3. 读取明文业务 JSON
  4. 按 event_id 去重(同一条通知重复送达,只处理第一次)
  5. 在同一事务中保存回调记录与业务状态
  6. 事务提交后返回 2xx

平台默认单次投递超时 10 秒,最多尝试 8 次(含首次);具体可由平台配置。非 2xx、超时及网络错误会重试,3xx 不跟随跳转。默认重试间隔为 5、10、20、40、80、160、320 秒,从前次失败后计时。 所以同一条通知可能送达多次,也可能后发的先到 —— 客户端必须能正确处理重复送达与乱序到达。

验证签名 ​

python
import hashlib
import hmac

def verify(raw_body: bytes, signature_header: str, callback_secret: str) -> bool:
    expected = hmac.new(
        callback_secret.encode("utf-8"), raw_body, hashlib.sha256
    ).hexdigest()
    received = signature_header.removeprefix("sha256=").lower()
    return hmac.compare_digest(expected, received)

必须用原始字节

不得对重新序列化后的 JSON 验签。框架自动解析过的对象再 json.dumps() 得到的字节与原始报文不同,签名必然对不上。

各框架获取原始字节的方式:

框架取法
FastAPI / Starletteawait request.body()
Flaskrequest.get_data()
Spring BootContentCachingRequestWrapper 或 HttpServletRequest.getInputStream()
Expressexpress.raw({ type: 'application/json' })
Ginio.ReadAll(c.Request.Body)

接收端处理示例 ​

下面展示事务处理顺序;begin_transaction、save_callback、apply_business_state 需接入客户自己的数据库,decrypt_if_needed 需按加密指南实现,不能将这段结构示例直接当作可运行服务。数据库应对完整 event_id 设置唯一约束,防止并发重复投递。

python
import hashlib
import hmac
import os
from fastapi import APIRouter, Header, HTTPException, Request

router = APIRouter()
CALLBACK_SECRET = os.environ["RPA_CALLBACK_SECRET"]


@router.post("/rpa/callback")
async def receive(
    request: Request,
    x_rpa_signature: str = Header(default=""),
    x_rpa_event: str = Header(default=""),
):
    raw = await request.body()

    # 1. 验签(原始字节)
    expected = hmac.new(CALLBACK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, x_rpa_signature.removeprefix("sha256=").lower()):
        raise HTTPException(status_code=401, detail="bad signature")

    # 2. 解密(仅当订单启用 SM4)
    payload = decrypt_if_needed(raw)  # 见「SM4 输出加密」

    # 3. 去重 + 保存,放在同一事务里
    event_id = payload.get("event_id")
    with begin_transaction() as tx:
        if tx.callback_exists(event_id):
            return {"ok": True}          # 重复投递,直接确认
        tx.save_callback(event_id, x_rpa_event, payload)
        tx.apply_business_state(payload)

    # 4. 提交后返回 2xx
    return {"ok": True}

用哪个字段去重 ​

把平台返回的完整 event_id 当作不透明的幂等键保存,不自行拼接、截断或拆分后重建。当前有执行批次时格式为 <authorization_code>:<event>:<attempt_id>,无批次的事件及历史报文可以只有前两段。

场景是否会重复去重做法
平台重试同一次投递event_id 与 X-RPA-Delivery 都不变按 event_id 去重
平台系统重试或原授权页重新录入凭据,进入新执行批次attempt_id 变化,新批次事件的 event_id 也变化新事件独立处理;保留旧批次明细,不用旧批次结果覆盖当前结果
平台创建新授权代次新的 authorization_code → 新的 event_id当作新一轮授权处理

批次 ID 是标识,不保证可按字符串排序。收到延迟通知时,保留通知原文,并查询当前订单/产品确认 authorization_code 和 attempt_id;旧批次通知不能使最新状态倒退。

回调只是信号

回调 data 不保证携带完整业务数据。收到终态回调后,仍应走 产品查询确认 available, 再调用数据接口取数。

联调要点 ​

  • 回调地址使用公网 HTTPS;仅已特批机构可按本页规则使用公网 HTTP。
  • 接收端应在 10 秒内返回,耗时操作放进异步队列。
  • 主动构造重复投递、错误签名、超时不响应三种场景验证客户端行为。
  • 测试环境与生产环境的回调地址分别配置,不要复用。

文档契约 API 1.5.169