StableOps

订阅

创建套餐、为商户用户开通订阅、生成账单,并让终端用户通过托管收银台支付。

StableOps 订阅把商户侧管理和终端用户支付分开:你的后端用密钥管理套餐、订阅、账单设置和 Portal 会话;终端用户拿 Portal 令牌查看自己的账单,并通过托管收银台支付。

生命周期

  1. 创建一个或多个套餐,指定周期和金额,套餐金额以美元计价。
  2. 为你的 merchantUserId 创建订阅。
  3. 读取首期未支付账单,并为该用户创建 Portal 会话。
  4. 在 Portal 上下文为账单创建收银台会话,把用户带到 Checkout。
  5. 以 Webhook 和账单状态为准完成业务逻辑。successUrl 只是浏览器返回路径。

结算币种

套餐和账单不绑定某一种稳定币。账单发起支付时,StableOps 会按账单金额生成一张收款单;这张收款单会在订阅账单可用的链上接受该链支持的稳定币(USDC 或 USDT)。付款人在收银台里选择一条链和一种币,实际打出的那种币就成为账单的结算币种。在此之前,账单的 assetnull,表示还没有已结算币种。

付款人能看到的链来自商户级订阅账单设置。现在可以在控制台的 商户订阅 → 设置 页面配置,用来控制当前环境下所有订阅付款可选的链。如果 accepted_chains 为空,StableOps 会从你已导入的收款地址推导可用链。每条链只展示 StableOps 在该链支持的稳定币,因此不会出现某条链展示了实际不能收的币。

结算模型

账单收银台会话包住的是账单自己的收款单。收款单元数据里有 invoice_id,账单记录也回写了 paymentOrderId。收款单到达 FINALIZED 后,StableOps 通过这对字段结算账单,把付款人实际打出的稳定币回填到账单的 asset,再把订阅状态推进到 active 或完成套餐升级。

角色

  • 后端或沙箱工具用密钥创建套餐、订阅和 Portal 会话。
  • 终端用户浏览器用 Portal 令牌读取自己的订阅和账单。
  • 控制台用于管理套餐、订阅、账单以及商户级订阅设置。

在线测试

下面的面板会在浏览器里使用你的沙箱 API 密钥,准备演示套餐、创建演示订阅、创建 Portal 会话,并打开账单对应的托管 Checkout。

订阅组件源码托管在 GitHub:github.com/StableOps/stableops-playground

SDK 示例

import { StableOps } from '@stableops/api-sdk'

const stableops = new StableOps({
  apiKey: process.env.STABLEOPS_API_KEY!,
})

const plan = await stableops.merchantSubscriptions.plans.create(
  {
    code: 'starter',
    name: 'Starter',
    groupKey: 'starter',
    // 金额以美元计价;不指定币种,终端用户在收银台里选择 USDC 或 USDT。
    amount: '9.00',
    interval: 'month',
    intervalCount: 1,
  },
  { idempotencyKey: 'plan_starter' },
)

const created = await stableops.merchantSubscriptions.subscriptions.create(
  {
    planId: plan.id,
    merchantUserId: 'user_123',
  },
  { idempotencyKey: 'sub_user_123' },
)

const portalSession = await stableops.merchantSubscriptions.portalSessions.create({
  merchantUserId: 'user_123',
})

const portal = stableops.portal(portalSession.portalToken)
const invoice = created.invoice!
const checkout = await portal.invoices.checkoutSession(invoice.id, {
  successUrl: 'https://merchant.example/success',
  cancelUrl: 'https://merchant.example/cancel',
})

return Response.redirect(checkout.checkoutUrl, 303)

这篇文档怎么样?

最后更新

本页内容