主题
回调通知
平台向创建授权时提供的 callback_url 发送 JSON POST。回调是获取结果的主要方式, 状态查询只是没收到回调时的兜底。
HTTP 回调仅用于平台特批机构
普通机构的 callback_url 必须使用公网 HTTPS。只有平台超级管理员为所属机构开启安全例外后才允许公网 HTTP;该例外仍禁止本机和内网目标。HTTP 不提供 TLS 保护,接收端仍必须验签,并建议在创建授权时选择 output_encryption=SM4。
请求头
| 头 | 说明 |
|---|---|
X-RPA-Event | 事件类型 |
X-RPA-Delivery | 投递记录 ID;重试时保持不变 |
X-RPA-Signature | Callback 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 |
| 整单结果回调 | 仅 AGGREGATE | order.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。
| 字段 | 值 | 含义 | 进一步判断 |
|---|---|---|---|
notifyType | 1 | 授权阶段通知 | event 为 authorization.completed 或 authorization.failed |
notifyType | 2 | 采集或产品结果通知 | event 为 order.* 或 collection.* |
status | 1 | 本次通知成功 | 成功结果仍可能带质量警告,须继续读取 data |
status | 2 | 本次通知未成功 | 可能是 failed、skipped 或 PDF partial,以 event、errorMsg 和 data 为准 |
dataType | 0 | 仅发票 | PER_PRODUCT 的发票通知也使用该值 |
dataType | 1 | 仅税务 | PER_PRODUCT 的税务通知也使用该值 |
dataType | 2 | 税务与发票 | 采集范围同时包含税务与发票时使用;PDF 产品通知也使用该值 |
例如,上面的 notifyType=2、status=1 表示“采集结果通知且本次通知成功”; 具体是整单完成还是某个产品完成,仍由 event 判断。
PER_PRODUCT 报文中的 sequence 才是本轮采集内的平台通知序号,但回调可能乱序到达, 因此不能按 sequence 推断实际送达顺序,也不能用它代替 event_id 做幂等去重。
- 采集或产品结果回调包含顶层
attempt_id;纯授权阶段回调可以没有该字段。 - 创建授权时传入的
extras_data对象以顶层extrasDataJSON 字符串回传;需要业务对象时再解析该字符串。 - 非阻断部分采集可以发送成功终态,同时在数据中带
qualityStatus=WARNING、collectionResult=COMPLETED_WITH_WARNINGS与质量明细;接收端不能只保存事件名。 - 开启详版、简版在线报告,或申请
TAX_RISK时,结果回调顶层会额外包含generateHtmlReports=true与同一份htmlReports。 - 订单启用
output_encryption=SM4时,整个 Body 被替换为 SM4-GCM 信封。
正确的处理顺序
顺序不能变
- 先验证签名(基于原始字节,即下文的「验签」)
- 解析外层 JSON;如为 SM4 信封,验证 GCM Tag 并解密
- 读取明文业务 JSON
- 按
event_id去重(同一条通知重复送达,只处理第一次) - 在同一事务中保存回调记录与业务状态
- 事务提交后返回 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 / Starlette | await request.body() |
| Flask | request.get_data() |
| Spring Boot | ContentCachingRequestWrapper 或 HttpServletRequest.getInputStream() |
| Express | express.raw({ type: 'application/json' }) |
| Gin | io.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;旧批次通知不能使最新状态倒退。
联调要点
- 回调地址使用公网 HTTPS;仅已特批机构可按本页规则使用公网 HTTP。
- 接收端应在 10 秒内返回,耗时操作放进异步队列。
- 主动构造重复投递、错误签名、超时不响应三种场景验证客户端行为。
- 测试环境与生产环境的回调地址分别配置,不要复用。