Appearance
回调通知
CXH 对外的统一异步推送通道,覆盖订单、支付、协议三类业务事件。 主订单状态变更推
order.status.changed,子订单状态变更推payment.status.changed(状态机两层分离,详见 字段约定 § 状态枚举)。一次业务动作可能触发多个事件(例如首扣成功会同时改写主订单与子订单)。 渠道侧需在接入时通过 BD 登记callback_url,并实现验签与幂等。
事件类型
| eventType | 触发条件 | data 字段集 |
|---|---|---|
order.status.changed | 主订单 orderStatus 变更:INITIALIZING → ACTIVE(首扣成功)/ INITIALIZING → TERMINATED(首扣彻底失败)/ ACTIVE → COMPLETED(末期完结)/ 任意 → UNSUBSCRIBED(解约);B 模式同样覆盖 ACTIVE → COMPLETED 与 → UNSUBSCRIBED | 订单状态事件载荷(详见下文) |
payment.status.changed | 子订单 paymentStatus 变更:A 模式首扣 PROCESSING → SUCCESS / FAIL,续费期可出现 FAIL_RETRY,SUCCESS → REFUND_PART / REFUND_FULL(CXH 内部退款时触发);B 模式 → BILLED(每期开账);两类均含 → CANCELED(主订单解约时未来期作废) | 子订单状态事件载荷(详见下文) |
B 模式不推送退款类事件;渠道侧退款由渠道自管,CXH 不感知。
订单状态事件 data 字段
| 字段 | 类型 | 说明 |
|---|---|---|
orderNo | string | CXH 主订单号 |
externalOrderNo | string | 渠道主订单号 |
createTime | string | 订单创建时间 |
orderStatus | string | 当前主订单状态 |
orderSuccessTime | string | null | 主订单首次激活时刻 |
periodType | string | DAY / MONTH |
period | int | 周期单位倍率 |
totalCycles | int | 总期数 |
totalPaidAmountCent | long | 累计用户侧支付金额(分);B 模式为已 BILLED 期次对应的用户侧 SPU 金额累计 |
totalPaidCycles | int | 累计已付期数 |
totalRefundAmountCent | long | 累计退款金额(分);B 模式 0 |
latestPaymentStatus | string | null | 最新一期子订单状态 |
nextPayTime | string | null | 下次扣款 / 开账时间;完结或解约后为 null |
子订单状态事件 data 字段
| 字段 | 类型 | 说明 |
|---|---|---|
orderNo | string | CXH 主订单号 |
externalOrderNo | string | 渠道主订单号 |
paymentOrderNo | string | 子订单号 |
channelUserId | string | 渠道侧用户唯一 ID |
mobileEncrypted | string | null | 用户登录手机号(AES 加密);仅 agreementShareFlag=true(A.2 协议共享)时下发。A.2 时 CXH 以共享协议号代扣,需借此字段帮助渠道关联用户身份;A.1 / B 模式下渠道自身已知用户身份,不重复下发 |
cycleNo | int | 期次序号 |
expectPayTime | string | 计划扣款 / 开账时刻 |
paidAt | string | null | 实际扣款 / 开账成功时刻 |
amountCent | long | 本期用户侧支付金额(分);B 模式为渠道向用户收取的 SPU 单期金额 |
refundTime | string | null | 退款时刻 |
refundAmountCent | long | 累计退款金额(分);B 模式 0 |
paymentStatus | string | 当前子订单状态 |
agreementShareFlag | boolean | true 表示该订阅走 A.2 共享协议;false 否则 |
典型场景的事件序列
每张图聚焦"一次业务动作触发哪些 webhook、按什么顺序"。同步接口响应单独标 200,webhook 推送另算。
A · 首扣同步 SUCCESS(2 次推送)
渠道侧:同步响应已包含 redeemUrl,无需等待 webhook。两条 webhook 主要用于业务系统二次入账与对账,按 eventId 各自幂等处理即可。
A · 首扣 PROCESSING 异步(2-3 次推送)
A · 首扣终极失败(2 次推送)
同步 / 异步扣款终态失败走该流程:
A · 续扣中间期 SUCCESS(1 次推送)
A · 续扣末期 SUCCESS(2 次推送)
A · 退款(1 次推送)
A · 解约(1 次推送)
B · 下单(2 次推送)
B · 续期(中间期 1 次 / 末期 2 次)
B · 解约(1 次推送)
请求格式
http
POST /your/registered/callback/path HTTP/1.1
Content-Type: application/json
X-CXH-App-Id: test_xxxxxxx
X-CXH-Timestamp: 1714003200123
X-CXH-Nonce: a1b2c3...
X-CXH-Event-Id: evt_20260425_xxx
X-CXH-Signature: base64(HMAC-SHA256(signText, callbackSecret))
{
"eventId": "evt_20260425_xxx",
"eventType": "payment.status.changed",
"occurredAt": "2026-04-25 21:34:56",
"retryNo": 0,
"data": {
"orderNo": "SUB...",
"externalOrderNo": "ext_xxx",
"paymentOrderNo": "PAY...",
"channelUserId": "u_001",
"cycleNo": 1,
"expectPayTime": "2026-04-25 21:00:00",
"paidAt": "2026-04-25 21:34:56",
"amountCent": 1990,
"refundTime": null,
"refundAmountCent": 0,
"paymentStatus": "SUCCESS",
"agreementShareFlag": false
}
}签名规则与请求侧一致,密钥替换为 callbackSecret,signText 拼装如下:
POST
/your/callback/path
// query 为空
sha256(body)
1714003200123
a1b2c3...
evt_20260425_xxx // 使用 X-CXH-Event-Id,而非 Request-Id响应规范
http
HTTP/1.1 200 OK
Content-Type: application/json
{"code": "0"}非 200 + body 含 "code":"0" 一律视为失败,CXH 按下表重试:
| 重试序号 | 距上次 |
|---|---|
| 1 | 30 秒 |
| 2 | 60 秒 |
| 3 | 120 秒 |
| 4 | 240 秒 |
| 5 | 480 秒 |
| 6 | 960 秒 |
| 7-10 | 1800 秒 |
10 次仍失败标记为 GIVE_UP,CXH 侧介入处理。
渠道侧实现要求
- 验签:用 callbackSecret 计算 expectedSignature 并与 X-CXH-Signature 比对,严禁跳过
- 幂等:按
eventId去重(建议保留 30 天),相同 eventId 的重复推送不得重复变更业务状态 - 超时控制:CXH 默认 5 秒超时;若业务处理较慢,请使用异步 worker,主线程立即返回
{"code":"0"} - 响应字段:CXH 仅读取 code 字段,其余字段忽略,无需在响应中携带业务详情
验签示例(Python)
python
import base64, hmac, hashlib
def verify(body_raw: bytes, headers: dict, callback_secret_b64: str) -> bool:
sign_text = "\n".join([
"POST",
"/your/callback/path",
"", # query 为空
hashlib.sha256(body_raw).hexdigest(),
headers["X-CXH-Timestamp"],
headers["X-CXH-Nonce"],
headers["X-CXH-Event-Id"],
])
key = base64.b64decode(callback_secret_b64)
expected = base64.b64encode(
hmac.new(key, sign_text.encode(), hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, headers["X-CXH-Signature"])排查清单
- 未收到回调
- 确认已通过 BD 登记
callback_url(支持按事件类型分别登记) - 检查 callback_url 公网可达性、DNS 解析与 SSL 证书有效性(CXH 仅走 https)
- 检查 nginx / 网关日志是否出现 5xx
- 确认已通过 BD 登记
- 验签失败
- 计算 signText 必须使用收到的 body 原始字节,不得重新格式化 JSON
- eventId 取自
X-CXH-Event-Id,而非X-CXH-Request-Id - callbackSecret 需先 base64 解码,以 raw bytes 作为 HMAC 密钥
- 收到重复回调
- 属预期行为,务必按 eventId 实现幂等