跳转到主要内容

Change Plan API

Full API docs for updating subscriptions.

Plan Change Preview

See charge amounts before changing plans.

Integration Guide

Step-by-step subscription setup.

What is a subscription upgrade or downgrade?

Changing plans lets you move a customer between subscription tiers or quantities. Use it to:
  • Align pricing with usage or features
  • Move from monthly to annual (or vice versa)
  • Adjust quantity for seat-based products
Plan changes can trigger an immediate charge depending on the proration mode you choose.

When to use plan changes

  • Upgrade when a customer needs more features, usage, or seats
  • Downgrade when usage decreases
  • Migrate users to a new product or price without cancelling their subscription

Plan Change Flow

Prerequisites

Before implementing subscription plan changes, ensure you have:
  • A Dodo Payments merchant account with active subscription products
  • API credentials (API key and webhook secret key) from the dashboard
  • An existing active subscription to modify
  • Webhook endpoint configured to handle subscription events
For detailed setup instructions, see our Integration Guide.

Step-by-Step Implementation Guide

Follow this comprehensive guide to implement subscription plan changes in your application:
1

Understand Plan Change Requirements

Before implementing, determine:
  • Which subscription products can be changed to which others
  • What proration mode fits your business model
  • How to handle failed plan changes gracefully
  • Which webhook events to track for state management
Test plan changes thoroughly in test mode before implementing in production.
2

Choose Your Proration Strategy

Select the billing approach that aligns with your business needs:
Best for: SaaS applications wanting to charge fairly for unused time
  • Calculates exact prorated amount based on remaining cycle time
  • Charges a prorated amount based on unused time remaining in the cycle
  • Provides transparent billing to customers
3

Implement the Change Plan API

Use the Change Plan API to modify subscription details:
subscription_id
string
必填
The ID of the active subscription to modify.
product_id
string
必填
The new product ID to change the subscription to.
quantity
integer
默认值:"1"
Number of units for the new plan (for seat-based products).
proration_billing_mode
string
必填
How to handle immediate billing: prorated_immediately, full_immediately, difference_immediately, or do_not_bill.
addons
array
Optional addons for the new plan. Leaving this empty removes any existing addons.
on_payment_failure
string
Controls behavior when the plan change payment fails:
  • prevent_change: Keep subscription on current plan until payment succeeds
  • apply_change (default): Apply plan change immediately regardless of payment outcome
If not specified, uses the business-level default setting.
discount_codes
array
可选的 stacked 折扣代码应用于新计划(最多20个,按数组顺序应用)。行为取决于您传递的内容:
  • 未提供 / null — 如果适用于新产品,现有的折扣与 preserve_on_plan_change=true 会被保留。
  • [](空数组) — 从订阅中移除所有现有折扣。
  • ["CODE_A", "CODE_B", ...] — 用该堆叠集替换任何现有的折扣。
discount_code
string
已弃用
已弃用 — 新的集成优先使用 discount_codes。此字段仍然保留后向兼容性,但不能在同一请求中与 discount_codes 结合使用。
effective_at
string
默认值:"immediately"
何时应用计划更改:
  • immediately(默认):立即应用计划更改
  • next_billing_date:为下一个账单日期安排更改。客户在账单周期结束前保持当前计划。
对降级使用 next_billing_date,以便客户在账单周期结束前保留当前计划的好处。
4

Handle Webhook Events

设置 webhook 处理以跟踪计划更改的结果:
  • subscription.active:计划更改成功,订阅已更新
  • subscription.plan_changed:订阅计划已更改(升级/降级/插件更新)
  • subscription.on_hold:计划更改费用失败,订阅暂停
  • payment.succeeded:计划更改的即时费用已成功
  • payment.failed:即时费用失败
始终验证 webhook 签名并实现幂等事件处理。
5

Update Your Application State

根据 webhook 事件更新您的应用程序:
  • 根据新计划授予/撤销功能
  • 使用新计划详细信息更新客户仪表板
  • 发送有关计划更改的确认电子邮件
  • 记录账单更改以供审核
6

Test and Monitor

彻底测试您的实现:
  • 使用不同场景测试所有比例模式
  • 验证 webhook 处理是否正常工作
  • 监控计划更改的成功率
  • 为计划更改失败设置警报
您的订阅计划更改实施现在已准备好用于生产环境。

预览计划更改

在确认计划更改之前,使用预览 API 向客户展示他们将被收取的确切费用:
const preview = await client.subscriptions.previewChangePlan('sub_123', {
  product_id: 'prod_pro',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately'
});

// Show customer the charge before confirming
console.log('Immediate charge:', preview.immediate_charge.summary);
console.log('New plan details:', preview.new_plan);
使用预览 API 构建确认对话框,在客户确认计划更改之前显示他们将被收取的确切金额。

更改计划 API

使用更改计划 API 修改活动订阅的产品、数量和比例行为。

快速启动示例

import DodoPayments from 'dodopayments';

const client = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
  environment: 'test_mode', // defaults to 'live_mode'
});

async function changePlan() {
  const result = await client.subscriptions.changePlan('sub_123', {
    product_id: 'prod_new',
    quantity: 3,
    proration_billing_mode: 'prorated_immediately',
    on_payment_failure: 'prevent_change', // Optional: control behavior on payment failure
  });
  console.log(result.status, result.invoice_id, result.payment_id);
}

changePlan();
Success
{
  "status": "processing",
  "subscription_id": "sub_123",
  "invoice_id": "inv_789",
  "payment_id": "pay_456",
  "proration_billing_mode": "prorated_immediately"
}
只有在计划更改期间创建即时费用和/或发票时,字段如invoice_idpayment_id才会返回。总是依赖 webhook 事件(例如payment.succeededsubscription.plan_changed)来确认结果。
如果即时费用失败,订阅可能会移动到 subscription.on_hold,直到付款成功。

管理插件

在更改订阅计划时,您还可以修改插件:
// Add addons to the new plan
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_new',
  quantity: 1,
  proration_billing_mode: 'difference_immediately',
  addons: [
    { addon_id: 'addon_123', quantity: 2 }
  ]
});

// Remove all existing addons
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_new',
  quantity: 1,
  proration_billing_mode: 'difference_immediately',
  addons: [] // Empty array removes all existing addons
});
插件包含在比例计算中,将根据所选的比例模式收费。

应用折扣代码

在更改订阅计划时,您可以应用一个或多个 stacked 折扣代码(最多20个,按数组顺序应用)。这对于在升级或迁移时提供促销价格很有用。
// Apply stacked discount codes during plan change
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_pro',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately',
  discount_codes: ['UPGRADE20']
});

计划更改时的折扣行为

discount_codes行为
未提供 / null自动保留适用于新产品的现有折扣与 preserve_on_plan_change=true
[](空数组)从订阅中移除所有现有折扣。
["CODE_A", "CODE_B", ...]用该堆叠集替换任何现有折扣,按数组顺序验证和应用。
该端点上的单一 discount_code 字段 已弃用 但仍然可以用于后向兼容性 — 现有集成不需要立即更改。它不能与 discount_codes 在同一请求中结合使用。方便时迁移到数组形式。
使用 预览计划更改 APIdiscount_codes,在确认计划更改前向客户展示他们节省的确切金额。

比例模式

选择在更改计划时如何向客户收费:

prorated_immediately

  • 对当前周期的部分差异收费
  • 如果在试用期内,立即收费并转换为新计划
  • 降级:可能生成预付抵用金,用于未来续订

full_immediately

  • 立即全额收费
  • 忽略旧计划的剩余时间
使用difference_immediately进行的降级所创建的抵用金是订阅范围内的,与基于抵用的结算权利相区别。它们会自动应用于同一订阅的未来续订,不能在订阅之间转移。

difference_immediately

  • 升级:立即收费旧计划和新计划之间的价格差异
  • 降级:将剩余价值作为内部抵用金添加到订阅,并在续订时自动应用

do_not_bill

  • 不计算任何费用或抵用金
  • 客户立即切换到新计划,没有任何账单调整
  • 账单周期保持不变
  • 最适合礼遇迁移、免费计划切换或吸收成本差异
功能prorated_immediatelydifference_immediatelyfull_immediatelydo_not_bill
升级费用剩余天数的比例差异计划之间的全额价格差异新计划的全额价格无费用
降级抵用剩余天数的比例抵用全额价格差异作为抵用无抵用无抵用
账单周期未变未变重置为今天未变
试用行为结束试用,立即收费结束试用,立即收费结束试用,全额收费结束试用,无费用
最适合公平的基于时间的账单简单的升级/降级计算重置账单周期免费迁移或礼遇切换
复杂性中等(天数计算)低(简单减法)低(全额收费)

示例场景

使用这些规范数字保持一致:
  • 当前计划:Basic 每月 $30
  • 升级目标:Pro 每月 $80
  • 降级目标(从 Pro):Starter 每月 $20
  • 账单周期:30天,从 1月1日 开始
  • 计划更改发生在 1月16日(剩余 15 天,已使用 15 天)
Step 1: Calculate unused credit from current plan
  Unused days = 15 out of 30 days
  Credit = $30 × (15/30) = $15.00

Step 2: Calculate prorated cost of new plan
  Remaining days = 15 out of 30 days
  New plan cost = $80 × (15/30) = $40.00

Step 3: Calculate immediate charge
  Charge = New plan cost − Credit
  Charge = $40.00 − $15.00 = $25.00

→ Customer pays $25.00 now
→ Next renewal (Feb 1): $80.00/month
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_pro',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately'
})
Step 1: Calculate unused credit from current plan
  Unused days = 15 out of 30 days
  Credit = $80 × (15/30) = $40.00

Step 2: Calculate prorated cost of new plan
  Remaining days = 15 out of 30 days
  New plan cost = $20 × (15/30) = $10.00

Step 3: Calculate credit balance
  Credit = $40.00 − $10.00 = $30.00

→ No charge — $30.00 credit added to subscription
→ Credit auto-applies to future renewals
→ Next renewal (Feb 1): $20.00 − $30.00 credit = $0.00
→ Following renewal (Mar 1): $20.00 − $10.00 remaining credit = $10.00
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_starter',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately'
})
Immediate charge = New plan price − Old plan price
                 = $80 − $30
                 = $50.00

→ Customer pays $50.00 now (regardless of cycle position)
→ Next renewal (Feb 1): $80.00/month
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_pro',
  quantity: 1,
  proration_billing_mode: 'difference_immediately'
})
Credit = Old plan price − New plan price
       = $80 − $20
       = $60.00

→ No charge — $60.00 credit added to subscription
→ Credit auto-applies to future renewals
→ Next renewal: $20.00 − $20.00 (from credit) = $0.00
→ Following renewal: $20.00 − $20.00 (from credit) = $0.00
→ Third renewal: $20.00 − $20.00 (from remaining credit) = $0.00
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_starter',
  quantity: 1,
  proration_billing_mode: 'difference_immediately'
})
Immediate charge = Full new plan price = $80.00

→ Customer pays $80.00 now
→ No credit for unused time on old plan
→ Billing cycle resets to today (January 16)
→ Next renewal: February 16 at $80.00/month
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_pro',
  quantity: 1,
  proration_billing_mode: 'full_immediately'
})
Current: Basic plan ($30/month), no add-ons
New: Pro plan ($80/month) + Extra Seats add-on ($10/seat × 3 seats = $30/month)
Change on day 16 of 30 (15 days remaining)

Step 1: Credit from current plan
  Credit = $30 × (15/30) = $15.00

Step 2: Prorated cost of new plan + add-ons
  New plan = $80 × (15/30) = $40.00
  Add-ons = $30 × (15/30) = $15.00
  Total new = $55.00

Step 3: Immediate charge
  Charge = $55.00 − $15.00 = $40.00

→ Customer pays $40.00 now
→ Next renewal: $80.00 + $30.00 = $110.00/month
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_pro',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately',
  addons: [
    { addon_id: 'addon_seats', quantity: 3 }
  ]
})

每种模式如何处理账单

选择 prorated_immediately 进行公平的时间计费;选择 full_immediately 重启账单周期;使用 difference_immediately 执行简单升级并在降级时自动抵用;或使用 do_not_bill 在不进行任何账单调整的情况下切换计划。

处理支付失败

控制计划更改支付失败时的行为,使用 on_payment_failure 参数。

支付失败模式

如果未指定,on_payment_failure 参数使用您的仪表板中配置的业务级别默认设置。

何时使用每种模式

场景推荐模式理由
升级到高级功能prevent_change确保在授予访问权限之前支付成功
数量增加(更多席位)prevent_change防止未支付情况下使用
降级计划apply_change客户在减少支出
可信企业客户apply_change非支付风险较低
从试用到付费转换prevent_change关键支付时刻

处理 webhooks

通过 webhooks 跟踪订阅状态以确认计划更改和支付。

处理的事件类型

  • subscription.active:订阅已激活
  • subscription.plan_changed:订阅计划已更改(升级/降级/插件更改)
  • subscription.on_hold:费用失败,订阅暂停
  • subscription.renewed:续订成功
  • payment.succeeded:计划更改或续订的支付成功
  • payment.failed:支付失败
我们建议从订阅事件中驱动业务逻辑,使用支付事件进行确认和对账。

验证签名并处理意图

import { NextRequest, NextResponse } from 'next/server';

export async function POST(req) {
  const webhookId = req.headers.get('webhook-id');
  const webhookSignature = req.headers.get('webhook-signature');
  const webhookTimestamp = req.headers.get('webhook-timestamp');
  const secret = process.env.DODO_WEBHOOK_SECRET;

  const payload = await req.text();
  // verifySignature is a placeholder – in production, use a Standard Webhooks library
  const { valid, event } = await verifySignature(
    payload,
    { id: webhookId, signature: webhookSignature, timestamp: webhookTimestamp },
    secret
  );
  if (!valid) return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });

  switch (event.type) {
    case 'subscription.active':
      // mark subscription active in your DB
      break;
    case 'subscription.plan_changed':
      // refresh entitlements and reflect the new plan in your UI
      break;
    case 'subscription.on_hold':
      // notify user to update payment method
      break;
    case 'subscription.renewed':
      // extend access window
      break;
    case 'payment.succeeded':
      // reconcile payment for plan change
      break;
    case 'payment.failed':
      // log and alert
      break;
    default:
      // ignore unknown events
      break;
  }

  return NextResponse.json({ received: true });
}
有关详细的负载架构,请参见订阅 webhook 负载支付 webhook 负载

最佳实践

请遵循以下建议,以实现可靠的订阅计划更改:

计划更改策略

  • 彻底测试:在生产之前始终在测试模式下测试计划更改
  • 仔细选择比例方式:选择符合您业务模型的比例方式
  • 优雅地处理失败:实现适当的错误处理和重试逻辑
  • 监控成功率:跟踪计划更改成功/失败率并调查问题

Webhook 实施

  • 验证签名:始终验证 webhook 签名以确保真实性
  • 实现幂等性:优雅地处理重复的 webhook 事件
  • 异步处理:不要用繁重的操作阻塞 webhook 响应
  • 记录一切:保持详细日志以进行调试和审计

用户体验

  • 清晰沟通:告知客户账单变化和时间
  • 提供确认:为成功的计划更改发送电子邮件确认
  • 处理边缘案例:考虑试用期、比例调整和支付失败
  • 立即更新 UI:在您的应用程序界面中反映计划更改

常见问题及解决方案

解决订阅计划更改中遇到的典型问题:
症状:API 调用成功但订阅仍保留在旧计划常见原因
  • Webhook 处理失败或延迟
  • 在接收到 webhooks 后应用程序状态未更新
  • 在状态更新期间出现数据库事务问题
解决方案
  • 实现强大的 webhook 处理和重试逻辑
  • 对状态更新使用幂等操作
  • 添加监控以检测和提醒遗漏的 webhook 事件
  • 验证 webhook 终端是否可访问并正确响应
症状:客户降级但未看到抵用余额常见原因
  • 比例方式期望:降级使用 difference_immediately 抵用全额计划价格差异,而 prorated_immediately 根据周期内的剩余时间创建比例抵用金
  • 抵用金是订阅特定的,不会在订阅之间转移
  • 客户仪表板上未显示抵用余额
解决方案
  • 对降级使用 difference_immediately ,当您希望自动抵用时
  • 向客户解释抵用适用于同一订阅的未来续订
  • 实现客户门户显示抵用余额
  • 检查下一个发票预览以查看应用的抵用金
症状:由于签名无效,Webhook 事件被拒绝常见原因
  • 错误的 webhook 秘钥
  • 在签名验证前修改了原始请求主体
  • 错误的签名验证算法
解决方案
  • 验证您使用的 DODO_WEBHOOK_SECRET 是否正确,并来自仪表板
  • 在任何 JSON 解析中间件之前阅读原始请求主体
  • 使用适用于您的平台的标准 webhook 验证库
  • 在开发环境中测试 webhook 签名验证
症状:API 返回 422 不可处理的实体错误常见原因
  • 无效的订阅 ID 或产品 ID
  • 订阅未处于活动状态
  • 缺少必需参数
  • 产品不可用于计划更改
解决方案
  • 验证订阅是否存在且处于活动状态
  • 检查产品 ID 是否有效且可用
  • 确保提供所有必需的参数
  • 查看 API 文档以获取参数要求
症状:计划更改启动但即时费用失败常见原因
  • 客户的支付方式上的资金不足
  • 支付方式过期或无效
  • 银行拒绝交易
  • 欺诈检测阻止了收费
解决方案
  • 适当地处理 payment.failed webhooks 事件
  • 通知客户更新支付方式
  • 为临时故障实现重试逻辑
  • 考虑允许在即时费用失败的情况下进行计划更改
症状:计划更改费用失败,订阅移至 on_hold 状态发生了什么: 当计划更改费用失败时,订阅会自动进入 on_hold 状态。直到更新支付方式,订阅才会自动续订。解决方案:更新支付方式以重新激活订阅要在计划更改失败后从 on_hold 状态重新激活订阅:
  1. 使用更新支付方式 API 更新支付方式
  2. 自动收费创建:API 自动为剩余款项创建费用
  3. 生成发票:为费用生成发票
  4. 支付处理:使用新支付方式处理支付
  5. 重新激活:在支付成功后,将订阅重新激活到 active 状态
// Reactivate subscription from on_hold after failed plan change
async function reactivateAfterFailedPlanChange(subscriptionId) {
  // Update payment method - automatically creates charge for remaining dues
  const response = await client.subscriptions.updatePaymentMethod(subscriptionId, {
    type: 'new',
    return_url: 'https://example.com/return'
  });
  
  if (response.payment_id) {
    console.log('Charge created for remaining dues:', response.payment_id);
    console.log('Payment link:', response.payment_link);
    
    // Redirect customer to payment_link to complete payment
    // Monitor webhooks for:
    // 1. payment.succeeded - charge succeeded
    // 2. subscription.active - subscription reactivated
  }
  
  return response;
}

// Or use existing payment method if available
async function reactivateWithExistingPaymentMethod(subscriptionId, paymentMethodId) {
  const response = await client.subscriptions.updatePaymentMethod(subscriptionId, {
    type: 'existing',
    payment_method_id: paymentMethodId
  });
  
  // Monitor webhooks for payment.succeeded and subscription.active
  return response;
}
要监控的 Webhook 事件
  • subscription.on_hold:订阅已暂停(在计划更改费用失败时接收)
  • payment.succeeded:剩余费用支付成功(在更新支付方式后)
  • subscription.active:支付成功后订阅重新激活
最佳做法
  • 在计划更改费用失败时立即通知客户
  • 提供有关如何更新支付方式的明确说明
  • 监控 webhook 事件以跟踪重新激活状态
  • 考虑为临时支付故障实施自动重试逻辑

Update Payment Method API Reference

查看完整的 API 文档以更新支付方式并重新激活订阅。

测试您的实现

按照以下步骤彻底测试您的订阅计划更改实现:
1

Set up test environment

  • 使用测试 API 密钥和测试产品
  • 创建具有不同计划类型的测试订阅
  • 配置测试 webhook 终端
  • 设置监控和日志记录
2

Test different proration modes

  • 使用各种账单周期位置测试 prorated_immediately
  • 为升级和降级测试 difference_immediately
  • 测试 full_immediately 以重置账单周期
  • 测试 do_not_bill 以进行无费用/无抵用的计划切换
  • 验证抵用计算是否正确
3

Test webhook handling

  • 验证是否收到了所有相关的 webhook 事件
  • 测试 webhook 签名验证
  • 优雅地处理重复的 webhook 事件
  • 测试 webhook 处理失败情况
4

Test error scenarios

  • 测试无效的订阅 ID
  • 测试过期的支付方式
  • 测试网络故障和超时
  • 测试资金不足
5

Monitor in production

  • 为失败的计划更改设置警报
  • 监控 webhook 处理时间
  • 跟踪计划更改成功率
  • 查看客户支持票据以解决计划更改问题

错误处理

在您的实现中优雅地处理常见的 API 错误:

HTTP 状态代码

计划更改请求已成功处理。订阅正在更新,支付处理已经开始。
请求参数无效。检查所有必填字段是否已提供并正确格式化。
无效或缺失的 API 密钥。请验证您的 DODO_PAYMENTS_API_KEY 是否正确且拥有适当的权限。
订阅 ID 未找到或不属于您的帐户。
订阅无法更改(例如,已取消,产品不可用等)。
服务器发生错误。稍作延迟后重试请求。

错误响应格式

{
  "error": {
    "code": "subscription_not_found",
    "message": "The subscription with ID 'sub_123' was not found",
    "details": {
      "subscription_id": "sub_123"
    }
  }
}

下一步

Last modified on May 22, 2026