主题
回调通知
平台向创建授权时提供的 callback_url 发送 JSON POST。回调是主链路, 状态查询只是兜底。
请求头
| 头 | 说明 |
|---|---|
X-RPA-Event | 事件类型 |
X-RPA-Delivery | 投递记录 ID;重试时保持不变 |
X-RPA-Signature | Callback Secret 对原始 Body 字节的 HMAC-SHA256 |
事件
| 模式 | 事件 |
|---|---|
| 授权阶段 | authorization.completed、authorization.failed |
AGGREGATE(默认) | order.completed、order.failed |
PER_PRODUCT | collection.tax.*、collection.invoice.*、collection.pdf.* |
callback_mode 在创建授权时选择:AGGREGATE 整单一次通知, PER_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",
"attempt_id": "a2ad4ec4-b878-42c5-b623-4d962f350e72",
"status": 1,
"data": {}
}- 采集或产品结果回调包含顶层
attempt_id;纯授权阶段回调可以没有该字段。 - 创建授权时传入的
extras_data会原样回传。 - 1.5.3 的非阻断部分采集可以发送成功终态,同时在数据中带
qualityStatus=WARNING、collectionResult=COMPLETED_WITH_WARNINGS与质量明细;接收端不能只保存事件名。 - 开启 HTML 报告时,终态回调顶层增量包含
generateHtmlReports=true与同一份htmlReports。 - 订单启用
output_encryption=SM4时,整个 Body 被替换为 SM4-GCM 信封。
正确的处理顺序
顺序不能变
- 先验签(基于原始字节)
- 再解密(如启用 SM4)
- 再解析 JSON
- 按
event_id去重 - 在同一事务中保存回调记录与业务状态
- 事务提交后返回 2xx
平台单次投递超时 10 秒,最多尝试 8 次。 客户端必须容忍重复投递与非顺序到达。
验签
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) |
一个完整的接收端
python
import hashlib
import hmac
from fastapi import APIRouter, Header, HTTPException, Request
router = APIRouter()
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>, 在同一授权代次内对同一事件是稳定的。
| 场景 | 是否会重复 | 幂等策略 |
|---|---|---|
| 平台重试同一次投递 | event_id 与 X-RPA-Delivery 都不变 | 按 event_id 去重 |
| 平台系统重试或原授权页重新录入凭据 | event_id 不变、attempt_id 变化 | 按 event_id 去重,用 attempt_id 更新明细 |
| 用户重新授权 | 新的 authorization_code → 新的 event_id | 视为新代次处理 |
联调要点
- 回调地址必须是公网可达的 HTTPS 地址,且已由平台配置。
- 接收端应在 10 秒内返回,耗时操作放进异步队列。
- 主动构造重复投递、错误签名、超时不响应三种场景验证客户端行为。
- 测试环境与生产环境的回调地址分别配置,不要复用。