订阅
使用 StableOps 创建稳定币订阅套餐、为商户用户开通和管理订阅、自动生成账单,并让终端用户通过托管收银台或自有钱包流程付款;再根据已验证的订阅事件更新服务状态。完整覆盖套餐变更、取消、恢复、账单支付和付款状态查询,帮助业务可靠维护订阅生命周期。
StableOps 订阅把商户侧管理和终端用户支付分开:你的后端用密钥管理套餐、订阅、账单设置和 Portal 会话;终端用户拿 Portal 令牌查看自己的账单,并通过托管收银台或你的自有钱包支付流程完成付款。
生命周期
- 创建一个或多个套餐,指定周期和金额,套餐金额以美元计价。
- 为你的
merchantUserId创建订阅。 - 读取首期未支付账单,并为该用户创建 Portal 会话。
- 在 Portal 上下文为账单创建收银台会话,跳转到收银台支付,或拿到支付指令,交给你自己的钱包支付。
- 以 Webhook 和账单状态为准完成业务逻辑。
successUrl只是浏览器返回路径。
结算币种
套餐和账单不绑定某一种稳定币。账单发起支付时,StableOps 会按账单金额生成一张收款单,发起支付请求传入 acceptedAssets,StableOps 会按这些链和资产组合分配收款地址。付款人在收银台里选择一条链和一种币,或你的钱包从返回的支付指令里选择一项;实际打出的那种币就成为账单的结算币种。在此之前,账单的 asset 为 null,表示还没有已结算币种。
acceptedAssets 需要与你已导入的地址池可收款范围一致。如果请求的链/资产没有可用地址,创建收款单会像单独创建 payment order 一样失败。
支付方式选择
托管收银台是最快的集成方式:调用 portal.invoices.checkoutSession(invoice.id, { acceptedAssets, ... }),把用户重定向到返回的 checkoutUrl。
如果你已经有自己的钱包支付流程,可以改为调用 portal.invoices.pay(invoice.id, { acceptedAssets })。响应里包含 paymentOrder.paymentInstructions,字段形状和单独创建 payment order 一致。你选择其中一条指令,让用户钱包在对应链和资产上把账单金额转到该地址,然后以 StableOps webhook 和账单状态作为最终结算依据。
结算模型
账单收银台会话包住的是账单自己的收款单。收款单元数据里有 invoice_id,账单记录也回写了 paymentOrderId。收款单到达 FINALIZED 后,StableOps 通过这对字段结算账单,把付款人实际打出的稳定币回填到账单的 asset,再把订阅状态推进到 active 或完成套餐升级。
角色
- 后端或沙箱工具用密钥创建套餐、订阅和 Portal 会话。
- 终端用户浏览器用 Portal 令牌读取自己的订阅和账单。
- 控制台用于管理套餐、订阅、账单以及商户级订阅设置。
在线测试
下面的面板会在浏览器里使用你的沙箱 API 密钥,准备演示套餐、创建演示订阅、创建 Portal 会话,并打开账单对应的托管收银台。
开启时会在创建账单收银台会话前导入一个确定性 burner 地址,适合 org 还没有任何收款地址的场景。若只想使用自己管理的地址,请关闭。
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 acceptedAssets = [{ chain: 'base-sepolia', asset: 'USDC' }] as const
const checkout = await portal.invoices.checkoutSession(invoice.id, {
acceptedAssets,
// 可选。省略时默认 exact;auto 会在共享地址金额冲突时自动微调应付金额。
amountMode: 'auto',
successUrl: 'https://merchant.example/success',
cancelUrl: 'https://merchant.example/cancel',
})
return Response.redirect(checkout.checkoutUrl, 303)也可以使用你自己的钱包支付:
const payment = await portal.invoices.pay(invoice.id, {
acceptedAssets,
// 可选。省略时默认 exact;auto 时请始终使用返回的 paymentOrder.amount 发起转账。
amountMode: 'auto',
})
const instruction = payment.paymentOrder.paymentInstructions[0]
// 交给你的钱包层:发送 payment.paymentOrder.amount 数量的
// instruction.asset 到 instruction.chain 上的 instruction.address。
await payWithYourWallet({
chain: instruction.chain,
asset: instruction.asset,
amount: payment.paymentOrder.amount,
to: instruction.address,
})这篇文档怎么样?
最后更新