mcpField
S
subscription-integration

subscription-integration

github.com/dodopayments开发131.0k 次安装3 周前更新
代码仓库

关于 subscription-integration

面向使用 Dodo Payments 构建循环计费产品的开发者,指导订阅创建、生命周期管理和支付恢复。内容还涵盖计划变更、按需扣款与客户自助门户。

主要功能

  1. 01通过 Checkout Sessions 创建订阅
  2. 02管理订阅状态与取消
  3. 03处理升级、降级和按比例计费
  4. 04执行按需扣款
  5. 05恢复失败付款
  6. 06配置客户自助门户

适用场景

  1. 01构建含试用期的订阅产品
  2. 02实现套餐升级或降级
  3. 03为用量计费客户发起按需扣款
  4. 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;
恢复选项
  1. **付款重试:**在“设置 → 恢复”中选择启用。Dodo 会在可配置的 1–30 天期限内(默认为 13 天)最多重试失败的续订 8 次。仅适用于软拒付。

  2. **客户门户:**客户更新付款方式 → 自动收取未结欠款 → subscription.active。

  3. **手动收费:**你在服务端更新付款方式,然后调用 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);
}

资源

评论

支持 Markdown