訂閱
使用 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 或完成方案升級。
續費、付款視窗與逾期
自動開票不等於自動扣款。 每期帳單仍需要使用者透過錢包主動付款。請在收到 end_user_invoice.open 後,透過你自己的通知管道引導使用者進入帳單支付流程,不要假設平台能從使用者錢包自動劃款。
商戶訂閱設定按組織與環境隔離,可透過查詢設定和更新設定管理:
| 設定 | 預設值 | 含義 |
|---|---|---|
renewal_lead_days | 3 天 | 活躍且未安排期末取消的訂閱,在週期結束前進入續費開票視窗 |
pay_window_days | 7 天 | 從開票時刻計算帳單的 due_at |
grace_days | 3 天 | 目前週期結束後允許續費的寬限期 |
帳單付款視窗、訂閱寬限期和付款訂單有效期是三個不同概念。付款視窗不保證服務延長相同天數;應同時讀取帳單到期時間、訂閱目前週期和狀態。後台按週期檢查,不保證在截止時間所在的那一秒立即變更狀態。
- 首次付款:未付款訂閱處於
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 还没有任何收款地址的场景。若只想使用自己管理的地址,请关闭。
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,
})這篇文件怎麼樣?
最後更新