Skip to content

订阅下单

一个主订单对应 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_BINDINGCXH 端(走 支付绑卡)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 扣款过程。

请求

字段必填说明
settlementModeCXH_DEDUCTCHANNEL_COLLECT
externalOrderNo渠道主订单号,CXH 全局唯一
productCodeSPU 编码,必须在 products/list 已授权列表内,否则 420005420006
bindOrderNoA.1 必填;A.2 / B 模式不传
sharedAgreementA.2 必填子对象;A.1 / B 模式不传(详见下文)
channelAgreementRefCHANNEL_COLLECT 选填:渠道自有协议号,任意字符串(CXH 仅存储不解析,不限定字符集)
channelUserIdA.2 / B 必填;A.1 由 bindOrderNo 关联的 agreement 提供
mobileEncryptedA.2 协议共享 / B 渠道收单必填(用户登录手机号,AES 加密)。A.1 由 bind-sms 阶段已绑定,此处可不传;若传则做一致性校验。首次提交后,该手机号与 (channelId, channelUserId) 强绑定;后续同 (channelId, channelUserId) 再次调用 mobile 必须一致,否则 410010 CHANNEL_USER_MOBILE_MISMATCH
periodTypeDAY(自然日)/ 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_MISMATCH
  • mobileEncrypted 已被同渠道下其他 channelUserId 占用 → 410011 CHANNEL_MOBILE_OCCUPIED

沙箱常见错误:测试时使用同一手机号轮换不同 channelUserId 构造数据,第二次起即返回 410011。建议为每个测试 channelUserId 分配独立手机号(如 139000xxxxx 序号递增)。详见 字段约定 § 错误码

周期与 SPU 一致性

  • 一个 SPU 对应一组固定的周期定价;periodType / period / totalCycles 三字段可省略,CXH 按 SPU 配置自动填充
  • 若传值,必须与 SPU 配置完全一致,否则 420007
  • 不同期数 / 价格 / 结算模式请使用对应的独立 productCode

bindOrderNosharedAgreement 互斥

同一请求内只能传一个;两个都传或两个都不传(且 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 内部处理:

  1. 校验该通道 shareEnabled=true,否则 403004
  2. 创建 agreement(agreementSource=SHARED_FROM_CHANNEL, agreementStatus=BIND_SUCCESS),记录卡 hash / tail / 共享协议号
  3. 用 CXH 自有入网身份 + sharedAgreementNo 调用支付机构扣款
  4. 后续 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_DEDUCTSUCCESS通道同步扣款成功,权益已发
CXH_DEDUCTPROCESSING支付确认中,通道尚未返回终态;以 回调通知 为准
CXH_DEDUCTFAIL扣款失败终态,权益不发,主订单 TERMINATED
CHANNEL_COLLECTBILLED已开账(权益已发);渠道侧实际扣款结果未知,扣款风险由渠道自担

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.1ACTIVE,首期 BILLED,权益已发
首期权益发放首扣 SUCCESS同 A.1orders/create 同步返回前
兑换入口可用首扣 SUCCESS同 A.1接口返回业务成功响应(code=0)后立即
CXH 计费首扣 SUCCESS = 一期收入同 A.1下单成功 = 一期开账
续扣 / 续期CXH 到期触发通道扣款(自签协议)CXH 到期触发通道扣款(共享协议)CXH 到期开账并发放权益
退款CXH 内部审核处理(联系 BD)同 A.1CXH 不参与,渠道自管
已发权益是否会 REVOKE退款时 REVOKE 未消耗部分同 A.1

POST /openapi/v1/agreements/replace(替换 A.2 共享协议号)

仅 A.2 协议共享子流程(agreementSource = SHARED_FROM_CHANNEL)的订阅可调用。 用途:用户换卡 / 原协议号在支付机构失效 / 主动迁移至另一开通了协议共享的通道。

请求

字段必填说明
orderNoCXH 主订单号;与 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
}

行为

替换在接口同步返回前原子完成:

  1. 创建新 agreement(agreementSource=SHARED_FROM_CHANNEL, agreementStatus=BIND_SUCCESS),关联到原订阅
  2. agreement 状态置 REPLACED(保留历史,标记不再使用)
  3. 主订单 agreement_id 切换到新 agreement
  4. 未扣款成功的子订单批量切换协议:
    • WAIT:切到新 agreement,到期由 cycle job 用新协议扣款
    • PROCESSING:等当前一次回单后再切(避免双扣)
    • FAIL_RETRY:切到新 agreement;retryFailedNow=true 时立即触发一次重试,无需等待下次 retry 窗口
  5. 已扣款成功的子订单(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_DEDUCTagreementSource != SHARED_FROM_CHANNEL410008 AGREEMENT_REPLACE_NOT_ALLOWED
订阅已 UNSUBSCRIBED / COMPLETED420003 / 420004
newSharedAgreement.paymentChannelCode 不在该渠道授权列表403003
该通道未开通协议共享(shareEnabled=false)403004

后续接口

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