Appearance
订阅下单
一个主订单对应 N 期子订单。 下单时必须显式选定清结算模式:
CXH_DEDUCT(CXH 代扣) 或CHANNEL_COLLECT(渠道收单);两种模式的语义、计费时机、权益发放时机不同。
清结算模式
settlementMode | 适用场景 | 收款方 | 兑换入口可用时机 |
|---|---|---|---|
CXH_DEDUCT | 用户在 CXH 完成绑卡,或渠道将已绑的支付协议号共享给 CXH;由 CXH 调通道扣款 | CXH 调用支付通道 | 首扣 SUCCESS 后 |
CHANNEL_COLLECT | 渠道自有支付体系收款 | 渠道自有支付 | orders/create 返回业务成功响应(HTTP 200 + code=0)后 |
两种模式可在同一渠道内共存(不同 SPU 走不同模式),但单个订阅必须二选一,事后不可修改。
B 模式退款:退款由渠道自行处理,CXH 不感知,已发权益不会因渠道侧退款而 REVOKE。
CXH_DEDUCT 的两个子流程
A 模式扣款方均为 CXH,但协议来源有两种:
| 子流程 | agreementSource | 绑卡发生 | 协议获取方式 | 入参 |
|---|---|---|---|---|
| A.1 自绑 | SELF_BINDING | CXH 端(走 支付绑卡) | CXH 自行签出 | bindOrderNo |
| A.2 协议共享 | SHARED_FROM_CHANNEL | 渠道端 | 渠道在本接口入参中传入 | sharedAgreement 子对象 |
A.2 硬约束:CXH 与渠道在同一家支付机构入网,且双方在该机构开通协议共享权限(需提前对接,授权后生效)。可走 A.2 的通道由 BD 在渠道开通时确认。
POST /openapi/v1/orders/create
强烈建议带 Idempotency-Key 防止网络抖动导致重复下单。
A 模式下单 = 同步触发首扣:接口同步返回时首扣已发起;响应中
paymentRecords[0].paymentStatus即首扣结果(SUCCESS/PROCESSING/FAIL),不会包含WAIT/FAIL_RETRY(WAIT仅用于续扣排队场景,FAIL_RETRY仅用于续费期可重试失败)。B 模式下单 = 立即开账 + 立即发权益:首期
paymentStatus = BILLED,无 CXH 扣款过程。
请求
| 字段 | 必填 | 说明 |
|---|---|---|
settlementMode | ✓ | CXH_DEDUCT 或 CHANNEL_COLLECT |
externalOrderNo | ✓ | 渠道主订单号,CXH 全局唯一 |
productCode | ✓ | SPU 编码,必须在 products/list 已授权列表内,否则 420005 或 420006 |
bindOrderNo | △ | A.1 必填;A.2 / B 模式不传 |
sharedAgreement | △ | A.2 必填子对象;A.1 / B 模式不传(详见下文) |
channelAgreementRef | — | CHANNEL_COLLECT 选填:渠道自有协议号,任意字符串(CXH 仅存储不解析,不限定字符集) |
channelUserId | △ | A.2 / B 必填;A.1 由 bindOrderNo 关联的 agreement 提供 |
mobileEncrypted | △ | A.2 协议共享 / B 渠道收单必填(用户登录手机号,AES 加密)。A.1 由 bind-sms 阶段已绑定,此处可不传;若传则做一致性校验。首次提交后,该手机号与 (channelId, channelUserId) 强绑定;后续同 (channelId, channelUserId) 再次调用 mobile 必须一致,否则 410010 CHANNEL_USER_MOBILE_MISMATCH |
periodType | — | DAY(自然日)/ MONTH(自然月,非 30 天;详见 字段约定 § 周期日期推进规则)。可不传,CXH 按 SPU 配置自动填充;若传则必须与 SPU 一致,否则 420007 |
period | — | 同上 |
totalCycles | — | 同上 |
⚠️ 渠道内手机号全局唯一(强约束,不可逆)
适用模式:A.2 协议共享 / B 渠道收单(即任何需要传入
mobileEncrypted的下单场景)。A.1 自绑订阅由bind-sms阶段完成手机号绑定,本节不适用。同一渠道(
channelId)下,一个手机号只能绑定到一个channelUserId;CXH 对(channelId, 手机号)做全局唯一性校验,不允许多个channelUserId共享同一手机号。
channelUserId已存在但本次提交的mobileEncrypted与首次绑定不一致 →410010 CHANNEL_USER_MOBILE_MISMATCHmobileEncrypted已被同渠道下其他channelUserId占用 →410011 CHANNEL_MOBILE_OCCUPIED沙箱常见错误:测试时使用同一手机号轮换不同
channelUserId构造数据,第二次起即返回410011。建议为每个测试channelUserId分配独立手机号(如139000xxxxx序号递增)。详见 字段约定 § 错误码。
周期与 SPU 一致性
- 一个 SPU 对应一组固定的周期定价;
periodType/period/totalCycles三字段可省略,CXH 按 SPU 配置自动填充 - 若传值,必须与 SPU 配置完全一致,否则
420007 - 不同期数 / 价格 / 结算模式请使用对应的独立
productCode
bindOrderNo 与 sharedAgreement 互斥
同一请求内只能传一个;两个都传或两个都不传(且 settlementMode=CXH_DEDUCT)→ 400001。
sharedAgreement 子对象(A.2 专用)
| 字段 | 必填 | 说明 |
|---|---|---|
paymentChannelCode | ✓ | 共享协议所在通道编码;必须在 BD 提供的授权列表内且支持协议共享,否则 403004 SHARED_AGREEMENT_NOT_ALLOWED |
sharedAgreementNo | ✓ | 渠道在该支付机构的协议号(由渠道侧绑卡产生),CXH 用此号 + 自有入网身份调用同一机构扣款 |
bankCardNoEncrypted | ✓ | 银行卡号(AES 加密) |
bankMobileEncrypted | ✓ | 银行预留手机号(AES 加密) |
certificateNoEncrypted | ✓ | 持卡人证件号(AES 加密) |
realNameEncrypted | ✓ | 持卡人真实姓名(AES 加密) |
certType | — | 证件类型,默认 ID_CARD |
channelUserId | ✓ | 渠道侧用户唯一 ID |
A.2 内部处理:
- 校验该通道
shareEnabled=true,否则403004 - 创建
agreement(agreementSource=SHARED_FROM_CHANNEL, agreementStatus=BIND_SUCCESS),记录卡 hash / tail / 共享协议号 - 用 CXH 自有入网身份 +
sharedAgreementNo调用支付机构扣款 - 后续 cycle / 退款逻辑与 A.1 一致
请求示例
A.1 自绑
json
{
"settlementMode": "CXH_DEDUCT",
"externalOrderNo": "ext_xxx",
"productCode": "TEST_SPU_001",
"bindOrderNo": "BIND...",
"periodType": "MONTH",
"period": 1,
"totalCycles": 12
}三个 cycle 字段省略时,CXH 按 SPU 配置自动填充。
A.2 协议共享
json
{
"settlementMode": "CXH_DEDUCT",
"externalOrderNo": "ext_xxx",
"productCode": "TEST_SPU_001",
"channelUserId": "u_001",
"mobileEncrypted": "cxh_aes_v1:...",
"sharedAgreement": {
"paymentChannelCode": "CHANNEL_A",
"sharedAgreementNo": "AGT_xxx_from_channel",
"channelUserId": "u_001",
"bankCardNoEncrypted": "cxh_aes_v1:...",
"bankMobileEncrypted": "cxh_aes_v1:...",
"certificateNoEncrypted": "cxh_aes_v1:...",
"realNameEncrypted": "cxh_aes_v1:...",
"certType": "ID_CARD"
},
"periodType": "MONTH",
"period": 1,
"totalCycles": 12
}CHANNEL_COLLECT
json
{
"settlementMode": "CHANNEL_COLLECT",
"externalOrderNo": "ext_xxx",
"productCode": "TEST_SPU_001",
"channelUserId": "u_001",
"mobileEncrypted": "cxh_aes_v1:...",
"channelAgreementRef": "渠道协议号(可选)",
"periodType": "MONTH",
"period": 1,
"totalCycles": 12
}响应 data
json
{
"orderNo": "SUB...",
"externalOrderNo": "ext_xxx",
"createTime": "2026-04-28 12:00:00",
"orderStatus": "ACTIVE",
"orderSuccessTime": "2026-04-28 12:00:01",
"periodType": "MONTH",
"period": 1,
"totalCycles": 12,
"totalPaidAmountCent": 1990,
"totalRefundAmountCent": 0,
"nextPayTime": "2026-05-28 12:00:00",
"unsubscribeTime": null,
"redeemUrl": "https://<cxh-h5>/redeem?orderNo=SUB...&appId=test_xxx&channelUserId=u_001&exp=1714003200&sig=xxx",
"paymentRecords": [
{
"paymentOrderNo": "PAY...",
"cycleNo": 1,
"expectPayTime": "2026-04-28 12:00:00",
"paidAt": "2026-04-28 12:00:01",
"amountCent": 1990,
"refundAmountCent": 0,
"paymentStatus": "SUCCESS"
}
]
}paymentRecords[0].paymentStatus 含义
settlementMode | 取值 | 含义 |
|---|---|---|
CXH_DEDUCT | SUCCESS | 通道同步扣款成功,权益已发 |
CXH_DEDUCT | PROCESSING | 支付确认中,通道尚未返回终态;以 回调通知 为准 |
CXH_DEDUCT | FAIL | 扣款失败终态,权益不发,主订单 TERMINATED |
CHANNEL_COLLECT | BILLED | 已开账(权益已发);渠道侧实际扣款结果未知,扣款风险由渠道自担 |
A 模式
orders/create同步响应不会出现WAIT/FAIL_RETRY(首扣已触发且终态失败直接失败)。WAIT仅在续扣排队、retry job 尚未轮到时出现;FAIL_RETRY仅可能在续费期通过orders/query/ webhook 可见。
redeemUrl 兑换入口
订阅下单成功后,CXH 同步生成一条绑定渠道 + 用户 + 订单的兑换入口链接,渠道分发给用户(短信 / 站内 push / 客户端跳转),用户访问即进入 CXH H5 兑换权益页。
域名由 CXH 返回:沙箱环境返回测试 H5 域名,生产环境返回生产 H5 域名。渠道侧不应硬编码域名,直接将
redeemUrl整串透传给用户。
https://<cxh-h5-host>/redeem
?orderNo=SUB... # CXH 主订单号
&appId=test_xxx # 渠道 appId
&channelUserId=u_001 # 渠道侧用户 ID
&exp=1714003200 # 过期时间戳(秒,默认下单后 +7d)
&sig=base64url(...) # HMAC-SHA256 签名签名规则:与开放 API 入站签名同算法,密钥换为 callbackSecret(B 模式渠道亦持有 callbackSecret)。signText 7 行:
GET
/redeem
# query 不参与
sha256("") # body 为空,sha256("") 的 hex 小写
{exp} # 整数秒
{appId} # 渠道 appId
{channelUserId} # 渠道侧用户 ID
{orderNo} # CXH 主订单号sig = base64url(HMAC-SHA256(signText, callbackSecret))
| 场景 | redeemUrl |
|---|---|
| A 模式 | 仅首扣 SUCCESS 时返回;PROCESSING / FAIL 均为 null(权益未发,链接无意义);异步路径在收到首扣 SUCCESS webhook 后调 orders/query 获取 |
| B 模式 | 必返回(BILLED 已立即发权益) |
orders/query | 同上规则;订阅存在已发未消耗权益时返回新 URL(exp 重新计算 +7d),否则 null |
链接安全约束
- 任何持有正确签名的 URL 均可访问该订单兑换页;渠道分发时建议使用短链 + 用户身份二次校验
exp过期后失效;过期可重新调用orders/query获取新 URL- 同一订单可下发多个 URL,共用同一签名密钥,互不影响
模式差异速查
| 时序事件 | A.1 自绑 | A.2 协议共享 | CHANNEL_COLLECT |
|---|---|---|---|
| 绑卡执行方 | CXH(走 binding 接口) | 渠道(共享协议号给 CXH) | 渠道(完全自管) |
orders/create 返回时主订单状态 | INITIALIZING / ACTIVE / TERMINATED | 同 A.1 | ACTIVE,首期 BILLED,权益已发 |
| 首期权益发放 | 首扣 SUCCESS 时 | 同 A.1 | orders/create 同步返回前 |
| 兑换入口可用 | 首扣 SUCCESS 后 | 同 A.1 | 接口返回业务成功响应(code=0)后立即 |
| CXH 计费 | 首扣 SUCCESS = 一期收入 | 同 A.1 | 下单成功 = 一期开账 |
| 续扣 / 续期 | CXH 到期触发通道扣款(自签协议) | CXH 到期触发通道扣款(共享协议) | CXH 到期开账并发放权益 |
| 退款 | CXH 内部审核处理(联系 BD) | 同 A.1 | CXH 不参与,渠道自管 |
| 已发权益是否会 REVOKE | 退款时 REVOKE 未消耗部分 | 同 A.1 | 否 |
POST /openapi/v1/agreements/replace(替换 A.2 共享协议号)
仅 A.2 协议共享子流程(
agreementSource = SHARED_FROM_CHANNEL)的订阅可调用。 用途:用户换卡 / 原协议号在支付机构失效 / 主动迁移至另一开通了协议共享的通道。
请求
| 字段 | 必填 | 说明 |
|---|---|---|
orderNo | △ | CXH 主订单号;与 externalOrderNo 二选一 |
externalOrderNo | △ | 渠道主订单号;与 orderNo 二选一 |
newSharedAgreement | ✓ | 新的共享协议子对象;结构与 orders/create § sharedAgreement 一致 |
retryFailedNow | — | 是否对当前 FAIL_RETRY 子订单立即触发重试,默认 true |
newSharedAgreement.paymentChannelCode 允许与原通道不同(支持跨通道迁移),但仍须在 payment-channels/list 中且 shareEnabled=true,否则 403004。
json
{
"orderNo": "SUB...",
"newSharedAgreement": {
"paymentChannelCode": "CHANNEL_A",
"sharedAgreementNo": "AGT_新协议号",
"channelUserId": "u_001",
"bankCardNoEncrypted": "cxh_aes_v1:...",
"bankMobileEncrypted": "cxh_aes_v1:...",
"certificateNoEncrypted": "cxh_aes_v1:...",
"realNameEncrypted": "cxh_aes_v1:...",
"certType": "ID_CARD"
},
"retryFailedNow": true
}行为
替换在接口同步返回前原子完成:
- 创建新
agreement(agreementSource=SHARED_FROM_CHANNEL, agreementStatus=BIND_SUCCESS),关联到原订阅 - 旧
agreement状态置REPLACED(保留历史,标记不再使用) - 主订单
agreement_id切换到新 agreement - 未扣款成功的子订单批量切换协议:
WAIT:切到新 agreement,到期由 cycle job 用新协议扣款PROCESSING:等当前一次回单后再切(避免双扣)FAIL_RETRY:切到新 agreement;retryFailedNow=true时立即触发一次重试,无需等待下次 retry 窗口
- 已扣款成功的子订单(
SUCCESS/REFUND_*)保持不变,可追溯历史
替换结果通过本接口同步响应返回,渠道无需依赖额外 webhook 感知;后续期次状态变更仍走 payment.status.changed。
响应 data
json
{
"orderNo": "SUB...",
"oldAgreementNo": "AGT_old",
"newAgreementNo": "AGT_new",
"paymentChannelCode": "CHANNEL_A",
"affectedPayments": [
{ "paymentOrderNo": "PAY_2", "cycleNo": 2, "previousStatus": "WAIT" },
{ "paymentOrderNo": "PAY_3", "cycleNo": 3, "previousStatus": "FAIL_RETRY" }
],
"retriedNow": [
{ "paymentOrderNo": "PAY_3", "cycleNo": 3, "newStatus": "PROCESSING" }
]
}拒绝场景
| 场景 | 错误码 |
|---|---|
| 订阅不存在或不属于当前渠道 | 420001 |
订阅 settlementMode != CXH_DEDUCT 或 agreementSource != SHARED_FROM_CHANNEL | 410008 AGREEMENT_REPLACE_NOT_ALLOWED |
订阅已 UNSUBSCRIBED / COMPLETED | 420003 / 420004 |
newSharedAgreement.paymentChannelCode 不在该渠道授权列表 | 403003 |
该通道未开通协议共享(shareEnabled=false) | 403004 |