- 商户在服务端创建收银台会话
- 将
checkoutUrl返回给前端 - Customer 点击链接进入托管收银台页面
- Customer 填写账单信息、预览税费并完成订单
请求体(API Key)
请求体(Store Slug)
Store Slug 认证不支持
priceSnapshot、expiresInSeconds、metadata、orderMerchantExternalId、withTrial、changeAmount、changeCreditAmount、includePaymentMethods 和 excludePaymentMethods。这些字段会被静默忽略,以防止客户端篡改价格和操纵会话。支付方式属于商户侧的商业决策(渠道费率、结算周期),浏览器不得收窄或放宽——Store Slug 创建的 session 一律开放该货币支持的全部方式。ID 命名约定:同一组扁平双 key 名(checkout 端的
orderMerchantExternalId、退款工单端的 refundTicketMerchantExternalId)贯穿请求体、webhook 载荷以及承载这些值的每个 GraphQL 类型 — 在 checkout 创建时写入的值可在 Order、Payment、Refund 及 webhook 载荷中以相同的字段名读回。本接口没有
cancelUrl 参数。 买家关闭收银台或放弃支付时,不会跳回你的站点。会话到期(默认 45 分钟)后订单以 canceled 结束,不产生扣款,也不发 webhook —— 你这边无需处理。传入 cancelUrl、returnUrl 之类的字段会被静默忽略,不报错。请在打开收银台的那个页面上保留自己的「返回商店」入口。价格快照对象
对一次性商品,
amount 直接替换商品价格。对订阅商品,它只替换常规周期价格 —— 试用期价格取自 session 锁定的商品版本,此处无法覆盖。
账单详情对象
billingDetail 只做预填。你传的国家会成为收银台国家下拉框的初始值(商家预填优先于 IP 探测),买家可以改,订单最终记录的是买家自己选定的国家。没有任何会话参数或 URL 参数能锁定或隐藏账单字段。支付方式列表不由国家驱动:它按产品类型 × 币种再加 includePaymentMethods / excludePaymentMethods 解析;国家只用于税费预览和决定展示哪些地址字段。换方案模式
传入originOrderId 即把本端点切换到换方案模式:返回的 checkoutUrl 不再指向新购链接,而是指向一笔已有订阅的变更确认页 —— 门店 slug 之后的路径段是 change 而不是 checkout(…/store/{storeSlug}/change/{sessionId})。换方案分两步 —— 本端点签发链接,customer 在该页面确认。
换方案被建模为一次取消加一次新建:旧订单进入取消终态,新订单走正常的创建生命周期。它不是对现有订阅的原地修改。
谁能签发链接:API Key(你的服务端)或 customer 会话令牌。Store Slug 是匿名身份,没有可归属的订阅 —— 用 Store Slug 认证传 originOrderId 一律返回 403。
用 SDK 签发链接
每个 SDK 都给换方案单独一个方法,originOrderId 必填,因此上面那些「必须与 originOrderId 同时出现」的拒绝在 SDK 里根本表达不出来:
withTrial 都会生效。用 checkout.authenticated.createPlanChange()(TypeScript / Next.js 的 authenticatedPlanChange)或 Checkout.Authenticated.CreatePlanChange(Go)可以让 SDK 替你把 customer 会话令牌拼到链接上 —— 那正是确认页需要的令牌,见下一节。
买家自行签发的那一半用 customer.createPlanChangeSession(),在 customer 会话上调用(TypeScript 的 client.customer(token)、Go 的 client.Customer(token)、Next.js 的 customerAction(token, "createPlanChangeSession", …))。它以买家自己的凭证发起,因此上面那三条 403 判据全部适用,且仅 API Key 可用的字段不在其入参里 —— 后端会静默丢弃它们。
构造给买家打开的链接
返回的checkoutUrl 既不带买家凭证、也不带环境参数 —— 会话是用你的服务端凭证创建的,平台此时无法代表买家授权。直接打开裸链接能看到方案对比页,但确认那一步会报「缺少授权信息」。两部分都要你自己拼:
?test=true。
不带令牌时页面回退到买家邮箱验证 —— 可用但更慢。环境拼错的链接过去会在确认那一步报
Environment mismatch between request and session。
changeAmount、changeCreditAmount 与 withTrial 仅 API Key 可用。用其他凭证传入会被静默忽略,与新购时的 priceSnapshot 同一口径 —— 不会有错误告诉你它们被丢弃了。哪些值取自原订阅
换方案模式下,这些值来自你要变更的那笔订阅,而非请求体:
货币也必须与原订阅一致 —— 跨币种换方案会被拒绝。
为这次变更定价
两个字段,两种为同一次变更定价的方式,二者互斥 —— 同时传入返回 400。
两者都是主单位小数串、同量纲同口径(含税),且都要求结果不为负:实收高于目标方案本期应收会被拒绝,抵扣高于它同样会被拒绝。两条拒绝是不同的文案,因此你能看出该调低哪个字段。
同一个数字在两个字段下含义相反,所以平台不会替你选一个。按你定价的方式挑:知道最终金额用
changeAmount,知道优惠额度用 changeCreditAmount。对下面的生效档位规则而言,传了任意一个都算「商户已为这次变更定价」。
会话内部只存一个口径 —— 本期实收,因此你指定的抵扣会在签发时刻按目标方案当时的应收折算成实收。
新方案何时生效
changeTiming 接受 immediate 或 next_period。不传则由平台按变更方向推导,方向按税前定价比较得出:升级默认 immediate,降级与同价切换默认 next_period。
同时只能有一笔已确认的变更
customer 还在犹豫时,同一笔订阅可以连续签发并提交多笔候选变更。只有当某笔变更被支付渠道确认之后,该订阅才被锁定到这一笔;此后再发起会返回 409。响应
响应体与新购模式完全一致 ——sessionId、checkoutUrl、expiresAt,不新增任何字段。变更方向、推导出的生效档位与折抵预览只写进会话供确认页读取,不回显。两个后果:
- 不传
changeTiming时你不会得知平台推导成了哪一档。需要确定就显式传。 - 折抵预览是签发时刻的估算,提交时会按当时实际剩余天数重算。不要把它当作最终金额呈现给 customer。
next_period换方案会产生一笔$0验卡记录。Dashboard 交易列表里这一行显示方案标价,下方标出 Charged $0.00,且不计入订阅的扣款次数。
subscription.plan_changed 与另两个换方案事件。升降级对 customer 的行为口径,见 订阅。
会话锁定
创建收银台会话时,以下值被锁定,在会话有效期内无法更改:productVersionId、productName、priceInfo、storeName、billingPeriod、withTrial、theme、buyerEmail、billingDetail
换方案模式下,会话还会额外锁定变更上下文:原订阅的快照、变更方向、推导出的生效档位与生效时刻、折抵预览,以及你指定的金额(传 changeCreditAmount 时存的是它折算出的实收)。确认页全部从会话读取,因此这些值在提交时都无法被覆盖 —— 目标方案、生效档位与金额在链接签发的那一刻就已定下。
邮箱规范化:所有邮箱字段(email / buyerEmail / contactEmail)在服务端会被规范化(trim().toLowerCase())后再用于存储、cache、下游调用。Foo@Bar.COM 与 foo@bar.com 视为同一账户。
请求示例
成功响应 (200)
响应字段
错误响应
重试策略:4xx 一律不要重试 — 修正请求后重发。下方的 409 是种类上的例外而非策略上的例外:它们报告的是订阅当前的状态,不是请求体写错了,因此原样重发只会一直失败,直到该状态发生变化。5xx 指数退避重试(起步 5s,最多 3 次)。
收款开关与生产环境审批是两个相互独立的条件:前者是平台侧控制的收款开关(可暂停整个门店,也可只暂停单个支付方式),后者是生产环境审批(KYB)闸门。在
prod 环境下,任一条件单独不满足即返回 403。两者在 test 环境均不适用。