StableOps

訂閱

使用 StableOps 建立穩定幣訂閱方案、為商戶使用者開通和管理訂閱、自動生成帳單,並讓終端使用者透過託管結帳頁或自有錢包流程付款;再根據已驗證的訂閱事件更新服務狀態。完整覆蓋方案變更、取消、恢復、帳單支付和付款狀態查詢,幫助業務可靠維護訂閱生命週期。

StableOps 訂閱把商戶側管理和終端使用者支付分開:你的後端用金鑰管理方案、訂閱、帳單設定和 Portal 會話;終端使用者拿 Portal 令牌檢視自己的帳單,並透過託管結帳頁或你的自有錢包支付流程完成付款。

生命週期

  1. 建立一個或多個方案,指定週期和金額,方案金額以美元計價。
  2. 為你的 merchantUserId 建立訂閱。
  3. 讀取首期未支付帳單,併為該使用者建立 Portal 會話。
  4. 在 Portal 上下文為帳單建立結帳頁會話,跳轉到結帳頁支付,或拿到支付指令,交給你自己的錢包支付。
  5. 以 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 或完成方案升級。

續費、付款視窗與逾期

自動開票不等於自動扣款。 每期帳單仍需要使用者透過錢包主動付款。請在收到 end_user_invoice.open 後,透過你自己的通知管道引導使用者進入帳單支付流程,不要假設平台能從使用者錢包自動劃款。

商戶訂閱設定按組織與環境隔離,可透過查詢設定和更新設定管理:

設定預設值含義
renewal_lead_days3 天活躍且未安排期末取消的訂閱,在週期結束前進入續費開票視窗
pay_window_days7 天從開票時刻計算帳單的 due_at
grace_days3 天目前週期結束後允許續費的寬限期

帳單付款視窗、訂閱寬限期和付款訂單有效期是三個不同概念。付款視窗不保證服務延長相同天數;應同時讀取帳單到期時間、訂閱目前週期和狀態。後台按週期檢查,不保證在截止時間所在的那一秒立即變更狀態。

  • 首次付款:未付款訂閱處於 incomplete;首期帳單到期且沒有在途付款訂單時,訂閱轉為 expired,未支付帳單轉為 uncollectible。
  • 試用結束:後台為到期試用開出首期帳單,並將訂閱推進到 past_due;後續按帳期與寬限規則處理。
  • 續費未付:目前週期結束後,訂閱在寬限期內進入 past_due;超過寬限期且沒有在途付款時轉為 expired。
  • 在途付款:正常逾期檢查會暫緩處理關聯 created、detected 或 confirmed 付款訂單的帳單,等待支付結果;這不保證付款最終成功,也不覆蓋主動取消規則。

以查詢介面返回的狀態決定是否保留或暫停權益,並結合經過驗籤的訂閱事件同步更新;不要僅靠瀏覽器返回或本地倒計時。

取消與恢復

取消訂閱有兩種行為,門戶介面遵循相同規則:

  • immediate: false(預設):設定 cancel_at_period_end,不立即結束目前週期;後台期末處理時將適用訂閱置為 expired,並將未支付帳單置為 uncollectible。
  • immediate: true:立即置為 canceled,同時將未支付帳單置為 uncollectible。已在鏈上發出的轉帳無法因此撤銷,取消也不會自動退款。

恢復訂閱只是清除非終態訂閱的期末取消標記,不會替使用者支付欠款,也不會復活已經 canceled 或 expired 的訂閱。需要重新訂閱時建立新訂閱,並核對原帳單及付款記錄。

方案變更

呼叫更改方案後,請讀取返回的訂閱、帳單及待生效方案,而不是立即在業務系統切換權益。

  • 目標價格更高:建立 upgrade_proration 差額帳單,目前實現收取目標方案金額減去目前方案金額,不按本週期剩餘天數折算。差額付款達到 finalized 並結清帳單後才切換方案,目前週期起止時間不變。建立升級帳單會清除期末取消標記;同一週期已有未付升級帳單時,再次升級返回衝突。
  • 目標價格更低或相同:記錄待生效方案,用於後續續費開票,續費帳單結清後切換;不會自動退還目前週期費用。已開出的續費帳單不會因之後修改待生效方案而自動重寫,付款前應核對帳單金額及目標方案。

遲到付款與業務對帳

帳單已變為 uncollectible 後,關聯付款訂單仍可能完成。結算檢查會產生 end_user_invoice.payment_late,不會把該帳單直接結清或恢復訂閱。商戶應核實鏈上實際到帳、帳單和訂閱狀態,再決定人工補償、新訂閱或退款,避免重複開通權益。

訂閱帳單的底層付款訂單會產生普通支付事件。不要單憑 payment.finalized 開通或升級訂閱;查詢帳單是否為 paid,以及訂閱是否進入預期狀態。重複和亂序事件的處理見 Webhook。

角色

  • 後端或沙箱工具用金鑰建立方案、訂閱和 Portal 會話。
  • 終端使用者瀏覽器用 Portal 令牌讀取自己的訂閱和帳單。
  • 控制台用於管理方案、訂閱、帳單以及商戶級訂閱設定。

線上測試

下面的面板會在瀏覽器裡使用你的沙箱 API 金鑰,準備演示方案、建立演示訂閱、建立 Portal 會話,並開啟帳單對應的託管結帳頁。

开启时会在创建账单收银台会话前导入一个确定性 burner 地址,适合 org 还没有任何收款地址的场景。若只想使用自己管理的地址,请关闭。

訂閱元件原始碼託管在 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 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,
})

這篇文件怎麼樣?

最後更新

本頁內容