Skip to content

回调通知

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 字段

字段类型说明
orderNostringCXH 主订单号
externalOrderNostring渠道主订单号
createTimestring订单创建时间
orderStatusstring当前主订单状态
orderSuccessTimestring | null主订单首次激活时刻
periodTypestringDAY / MONTH
periodint周期单位倍率
totalCyclesint总期数
totalPaidAmountCentlong累计用户侧支付金额(分);B 模式为已 BILLED 期次对应的用户侧 SPU 金额累计
totalPaidCyclesint累计已付期数
totalRefundAmountCentlong累计退款金额(分);B 模式 0
latestPaymentStatusstring | null最新一期子订单状态
nextPayTimestring | null下次扣款 / 开账时间;完结或解约后为 null

子订单状态事件 data 字段

字段类型说明
orderNostringCXH 主订单号
externalOrderNostring渠道主订单号
paymentOrderNostring子订单号
channelUserIdstring渠道侧用户唯一 ID
mobileEncryptedstring | null用户登录手机号(AES 加密);agreementShareFlag=true(A.2 协议共享)时下发。A.2 时 CXH 以共享协议号代扣,需借此字段帮助渠道关联用户身份;A.1 / B 模式下渠道自身已知用户身份,不重复下发
cycleNoint期次序号
expectPayTimestring计划扣款 / 开账时刻
paidAtstring | null实际扣款 / 开账成功时刻
amountCentlong本期用户侧支付金额(分);B 模式为渠道向用户收取的 SPU 单期金额
refundTimestring | null退款时刻
refundAmountCentlong累计退款金额(分);B 模式 0
paymentStatusstring当前子订单状态
agreementShareFlagbooleantrue 表示该订阅走 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 按下表重试:

重试序号距上次
130 秒
260 秒
3120 秒
4240 秒
5480 秒
6960 秒
7-101800 秒

10 次仍失败标记为 GIVE_UP,CXH 侧介入处理。

渠道侧实现要求

  1. 验签:用 callbackSecret 计算 expectedSignature 并与 X-CXH-Signature 比对,严禁跳过
  2. 幂等:按 eventId 去重(建议保留 30 天),相同 eventId 的重复推送不得重复变更业务状态
  3. 超时控制:CXH 默认 5 秒超时;若业务处理较慢,请使用异步 worker,主线程立即返回 {"code":"0"}
  4. 响应字段: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
  • 验签失败
    • 计算 signText 必须使用收到的 body 原始字节,不得重新格式化 JSON
    • eventId 取自 X-CXH-Event-Id,而非 X-CXH-Request-Id
    • callbackSecret 需先 base64 解码,以 raw bytes 作为 HMAC 密钥
  • 收到重复回调
    • 属预期行为,务必按 eventId 实现幂等

对接咨询 · bd@cxh.me / tech@cxh.me