关于 subscription-integration
面向使用 Dodo Payments 构建循环计费产品的开发者,指导订阅创建、生命周期管理和支付恢复。内容还涵盖计划变更、按需扣款与客户自助门户。
主要功能
- 01通过 Checkout Sessions 创建订阅
- 02管理订阅状态与取消
- 03处理升级、降级和按比例计费
- 04执行按需扣款
- 05恢复失败付款
- 06配置客户自助门户
适用场景
- 01构建含试用期的订阅产品
- 02实现套餐升级或降级
- 03为用量计费客户发起按需扣款
- 04让客户自行更新付款方式或取消
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dodopayments/ skills --skill subscription-integration安装后在 Claude Code 中输入/subscription-integration确认已加载。
附带文件references/webhook-events.md
详细介绍
Dodo Payments 订阅集成
实现包含试用、套餐变更和按需扣款的周期性计费。订阅通过 Checkout Sessions 创建,通过订阅 API 管理,并通过 Webhook 监控。
何时使用此技能
- 构建包含周期性计费和试用的订阅产品
- 处理中途升级、降级和迁移套餐
- 实现使用授权的按需/非会话扣款
- 管理支付失败和催款恢复
- 构建供客户自助管理订阅的门户
核心概念
订阅生命周期: 订阅有八种状态:pending、active、past_due、on_hold、paused、cancelled、failed 和 expired。处于试用期的订阅状态为 active;subscription.renewed 是一个事件,而非状态。续订失败后,订阅会变为 past_due(仅当你配置了宽限期时;客户仍可使用服务)或 on_hold(撤销访问权限);这两种状态都可以恢复。paused 表示你或客户主动暂停。failed 是终止状态,仅在初始创建失败时出现。取消后,订阅会变为 cancelled,或计划在周期结束时变为 expired。
Checkout Sessions: 创建订阅的推荐方式。这是一种仅可使用一次的托管式结账方式,可收集支付和客户数据,然后在服务端创建订阅。
按比例计费: 客户在周期中途变更套餐时,Dodo 会根据剩余时间计算抵扣额或费用。按比例计费模式决定是否立即向客户收费、给予抵扣,或两者皆不。
支付授权: 授权反复向客户的支付方式扣款(用于订阅)或按需扣款(用于基于用量的计费)。支付授权在结账过程中创建,支付失败时可以更新。
创建订阅
通过 Checkout Session(推荐)
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
});
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'pdt_monthly_plan', quantity: 1 }
],
subscription_data: {
trial_period_days: 14, // Optional
},
customer: {
email: '[email protected]',
name: 'Jane Doe',
},
return_url: 'https://yoursite.com/success',
});
// Redirect user to session.checkout_url
console.log('Redirect to:', session.checkout_url);
客户在托管式结账页面完成支付。支付成功后,Dodo 会创建订阅并触发 subscription.active Webhook。
添加附加项
const session = await client.checkoutSessions.create({
product_cart: [
{
product_id: 'pdt_pro_monthly',
quantity: 1,
addons: [
{ addon_id: 'adn_extra_seats', quantity: 3 }
]
}
],
subscription_data: {
trial_period_days: 7,
},
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/success',
});
订阅生命周期和状态
| 状态 | 含义 | 转换 |
|---|---|---|
pending | 正在创建 | → active、failed |
active | 正常续订 | → past_due、on_hold、paused、cancelled、expired |
past_due | 续订失败,且仍在宽限期内;客户在 past_due_ends_at 之前仍可使用服务 | → active(结清欠款)、on_hold 或 cancelled(宽限期结束后,取决于你的设置) |
on_hold | 续订/套餐变更支付失败;已停止续订,访问权限已撤销;可恢复 | → active(已更新支付方式/重试成功)、cancelled |
paused | 由你或客户主动暂停;计费冻结,访问权限已撤销 | → active(恢复)、cancelled |
cancelled | 不会续订 | → expired(周期结束时) |
failed | 创建时初始支付授权/支付失败;此状态不可恢复。不会因 on_hold 或重试次数耗尽而进入此状态 | (不可恢复) |
expired | 订阅期限已结束 | (终止状态) |
仅当你启用了宽限期时,订阅才会进入 past_due;否则,续订失败后会直接进入 on_hold。
试用期: 如果设置了 trial_period_days,订阅会立即进入 active,但在试用期结束前不会扣款。首次扣款发生在试用期结束日。
订阅方法
查询
const subscription = await client.subscriptions.retrieve('sub_xxxxx');
console.log(subscription.status, subscription.next_billing_date);
暂停和恢复
暂停和恢复通过更新端点上的 status 执行。请单独发送此字段:将 paused/active 与任何其他字段一起发送会被拒绝,并返回 422。
// Pause: renewals stop, access is revoked, next_billing_date shifts by the pause length
await client.subscriptions.update('sub_xxxxx', { status: 'paused' });
// Resume
await client.subscriptions.update('sub_xxxxx', { status: 'active' });
暂停会触发 subscription.paused;恢复会触发 subscription.unpaused。客户在门户中自助暂停需要选择启用(Settings → Subscriptions → Allow Subscription Pause)。
更新付款方式
当订阅因付款失败而处于 on_hold 状态时,客户可以更新付款方式。系统会自动收取所有未结欠款。
await client.subscriptions.updatePaymentMethod('sub_xxxxx', {
payment_method: {
type: 'existing',
payment_method_id: 'pm_new_method',
},
});
成功后会依次触发 payment.succeeded 和 subscription.active。
列出用量历史记录(按用量计费的订阅)
const history = await client.subscriptions.retrieveUsageHistory('sub_xxxxx', {
page_size: 50,
page_number: 0,
});
获取额度用量
const creditUsage = await client.subscriptions.retrieveCreditUsage('sub_xxxxx');
console.log('Subscription:', creditUsage.subscription_id);
for (const item of creditUsage.items) {
console.log(item.credit_entitlement_name, item.balance); // balance is a string
}
套餐变更(升级与降级)
更改套餐
await client.subscriptions.changePlan('sub_xxxxx', {
product_id: 'pdt_higher_tier',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
on_payment_failure: 'prevent_change',
});
按比例计费模式:
| 模式 | 升级 | 降级 | 计费日期 |
|---|---|---|---|
prorated_immediately | 抵扣旧套餐未使用时段的费用,然后收取新周期的全额费用(净额 = 新周期全额费用 − 未使用额度的抵扣额) | 使用相同公式;只有未使用额度的抵扣额超过新套餐价格时才会产生余额 | 重置为变更日期 |
difference_immediately | 收取新套餐全额费用 | 差额转为额度 | 重置 |
full_immediately | 收取新套餐全额费用 | 收取新套餐全额费用,不提供抵扣 | 重置 |
do_not_bill | 不收费 | 不提供抵扣 | 保留 |
付款失败处理:
prevent_change:如果收费失败,则保留旧套餐。apply_change:即使付款失败,也应用新套餐(订阅可能变为on_hold)。
计划变更:effective_at 默认为 'immediately'。传入 effective_at: 'next_billing_date' 以安排变更(通常用于降级)——现在不收费,客户在当前周期结束前继续使用当前套餐,订阅上的待处理变更会显示为 scheduled_change(可使用 cancelChangePlan 取消)。
await client.subscriptions.changePlan('sub_xxxxx', {
product_id: 'pdt_lower_tier',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
effective_at: 'next_billing_date',
});
**当订阅处于 past_due 状态时,不允许更改套餐。**请先结清续订欠款。
预览套餐变更
在提交前向客户展示报价:
const preview = await client.subscriptions.previewChangePlan('sub_xxxxx', {
product_id: 'pdt_new_plan',
quantity: 1,
proration_billing_mode: 'prorated_immediately',
});
console.log('Effective at:', preview.immediate_charge.effective_at);
console.log('Line items:', preview.immediate_charge.line_items);
console.log('Summary:', preview.immediate_charge.summary);
console.log('New plan:', preview.new_plan);
取消已安排的套餐变更
await client.subscriptions.cancelChangePlan('sub_xxxxx');
取消
立即取消
await client.subscriptions.update('sub_xxxxx', {
status: 'cancelled',
});
访问权限会立即撤销。
在周期结束时取消
await client.subscriptions.update('sub_xxxxx', {
cancel_at_next_billing_date: true,
});
订阅会保持 active 状态,直到下一个计费日期,然后变为 expired。subscription.cancelled Webhook 会包含 cancel_at_next_billing_date: true 和 next_billing_date,以便你知道何时撤销访问权限。
按需(非会话)收费
对于按用量计费或计量型订阅,无需安排续订即可按需向客户收费。
创建带有授权的订阅
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'pdt_usage_based', quantity: 1 }
],
subscription_data: {
on_demand: {
mandate_only: true,
}
},
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/success',
});
这会创建带有授权但不自动续订的订阅。你可以自行决定何时收费。
按需收费
const charge = await client.subscriptions.charge('sub_xxxxx', {
product_price: 2500, // $25.00 in cents
product_currency: 'USD',
product_description: 'API calls for January 2025',
});
console.log('Payment ID:', charge.payment_id);
金额以货币的最小单位表示(例如,USD 的美分、INR 的派萨等)。
**Dodo 不会自动重试按需收费。**你需要自行负责重试逻辑和拒付筛选。请监控 payment.failed Webhook,并实现自己的重试策略。
付款失败与恢复
暂停中的订阅
续订或套餐变更收费失败时,订阅会变为 on_hold。此状态可以恢复。
// Listen for subscription.on_hold webhook
case 'subscription.on_hold':
// Notify customer, offer payment method update
await sendPaymentFailedEmail(data.customer.customer_id);
break;
恢复选项
-
**付款重试:**在“设置 → 恢复”中选择启用。Dodo 会在可配置的 1–30 天期限内(默认为 13 天)最多重试失败的续订 8 次。仅适用于软拒付。
-
**客户门户:**客户更新付款方式 → 自动收取未结欠款 →
subscription.active。 -
**手动收费:**你在服务端更新付款方式,然后调用
updatePaymentMethod()。
逾期催收
逾期催收会针对 on_hold 状态的续订和通过客户门户取消的订阅发送最多四封可配置的邮件。逾期催收次数用尽后,订阅状态不会改变;你必须处理最终结果。
客户门户
让客户自助管理:查看订阅、更新付款方式、取消订阅,以及升级或降级。
const portalSession = await client.customers.customerPortal.create(
'cus_xxxxx',
{ return_url: 'https://yoursite.com/account' }
);
// Redirect to portalSession.link
门户链接将在 24 小时后过期。客户可以:
- 查看订阅详情和续订日期
- 立即取消订阅或在当前周期结束时取消
- 在已启用的 Product Collections 中升级或降级
- 更新付款方式,并重新激活
on_hold订阅 - 查看账单历史记录并下载发票
如需完整的客户管理功能(创建、更新、列出),请参阅 customer-management 技能。
Webhook 事件
完整指南:references/webhook-events.md。
涵盖:
- 示例处理程序
积分权益
为订阅产品关联积分权益,即可在每个计费周期发放积分。如需完整的积分管理功能,请参阅 credit-based-billing 技能。
快速示例:
// Product has credit entitlement attached (e.g., 10,000 tokens/month)
const session = await client.checkoutSessions.create({
product_cart: [
{ product_id: 'pdt_pro_with_credits', quantity: 1 }
],
subscription_data: {
trial_period_days: 14,
},
customer: { email: '[email protected]' },
return_url: 'https://yoursite.com/success',
});
每次续订时都会发放积分。监控 credit.added 和 credit.deducted webhook,以同步你的账本。
常见错误
1. 在 return_url 而不是 webhook 中授予访问权限
错误:
// On return_url redirect
await grantAccess(user.id);
订阅完全创建之前就会触发 return_url。Dodo 可能仍在处理付款授权或付款。务必等待 subscription.active webhook。
正确:
// In webhook handler
case 'subscription.active':
await grantAccess(data.customer.customer_id);
break;
2. 使用已弃用的 subscriptions.create
错误:
const sub = await client.subscriptions.create({
product_id: 'pdt_monthly',
customer_id: 'cus_xxxxx',
});
此端点已弃用。请使用 Checkout Sessions。
正确:
const session = await client.checkoutSessions.create({
product_cart: [{ product_id: 'pdt_monthly', quantity: 1 }],
customer: { customer_id: 'cus_xxxxx' },
return_url: 'https://yoursite.com/success',
});
3. 误解 subscription.plan_changed
升级、降级、附加项变更,以及切换 cancel_at_next_billing_date 时,都会触发 subscription.plan_changed。不要假设每个 plan_changed 事件都意味着付费升级已成功。请检查有效负载,并确认订阅当前的 product_id。
4. 忘记设置按比例计费模式
错误:
// Missing proration_billing_mode
await client.subscriptions.changePlan('sub_xxxxx', {
product_id: 'pdt_new',
quantity: 1,
// proration_billing_mode: 'prorated_immediately', // REQUIRED
});
这会导致失败。务必指定按比例计费模式。
5. 混淆 on_hold 与 paused
on_hold 是非自愿状态:付款失败后,通过更新付款方式或重试成功来恢复。paused 是主动暂停:你或客户使用 subscriptions.update(id, { status: 'paused' }) 冻结了订阅,并通过 { status: 'active' } 恢复。请将 subscription.paused/subscription.unpaused 与 subscription.on_hold 分开处理。
6. 未处理 cancel_at_next_billing_date
// Wrong: revoke immediately
await revokeAccess(data.customer.customer_id);
// Right: check the flag
if (data.cancel_at_next_billing_date) {
await scheduleAccessRevocation(data.subscription_id, new Date(data.next_billing_date));
} else {
await revokeAccessImmediately(data.subscription_id);
}