Skip to main content

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:
string
必填
The ID of the active subscription to modify.
string
必填
The new product ID to change the subscription to.
integer
必填
Number of units for the new plan (for seat-based products).
string
必填
How to handle immediate billing: prorated_immediately, full_immediately, difference_immediately, or do_not_bill.
array
Optional addons for the new plan. Leaving this empty removes any existing addons.
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.
使用支付链接收取套餐变更金额,而不是从订阅已保存的支付方式中扣款。客户会在托管结账页面完成支付。需要企业启用 allow_plan_change_via_payment_link 能力(Settings → Subscriptions → Collect Plan Change Payments by Payment Link)、effective_at: immediatelyon_payment_failure: prevent_change。请参阅通过结账链接收款预览路由会忽略此参数。
array
可选的要应用于新套餐的堆叠折扣码(最多 20 个,按数组顺序应用)。具体行为取决于传入的内容:
  • 未提供 / null — 如果现有折扣适用于新产品,则会保留带有 preserve_on_plan_change=true 的现有折扣。
  • [](空数组) — 移除订阅中的所有现有折扣。
  • ["CODE_A", "CODE_B", ...] — 使用此堆叠折扣集替换所有现有折扣。
string
已弃用
已弃用 — 新集成请优先使用 discount_codes。为保持向后兼容,此字段仍然有效,但不能在同一个请求中与 discount_codes 组合使用。
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 处理是否正常工作
  • 监控套餐变更成功率
  • 为失败的套餐变更设置提醒
你的订阅套餐变更实现现已准备好投入生产环境。

预览套餐变更

在提交套餐变更之前,使用 Preview API 向客户准确展示他们将被收取的金额:
使用预览 API 构建确认对话框,在客户确认套餐变更之前,向其展示准确的扣款金额。

Change Plan API

使用 Change Plan API 修改有效订阅的产品、数量和按比例计费行为。

快速入门示例

成功的套餐变更会立即返回 200 OK — 此时任何扣款尚未实际结算。响应正文(ChangePlanResponse)的内容取决于变更的收款方式:
在所有情况下,此响应都不是支付结果 — 它仅表示请求本身已被接受。它不代表即时扣款是否实际成功。对于普通即时扣款,结果会在调用后立即以 off-session 方式异步确定。对于 collect_via_payment_link 请求,结果会在之后异步确定 — 响应只会向你提供一个结账链接,订阅保持当前套餐不变,并且在客户通过该链接完成支付之前,无法得知结果。无论哪种情况,都不要根据此响应推断结果。请通过 webhook(payment.succeededpayment.failedsubscription.plan_changed)确认,或使用 GET /subscriptions/{subscription_id} 重新读取订阅 — 有关支付链接的具体说明,请参阅链接未支付时会发生什么
如果即时扣款失败,订阅可能会转为 subscription.on_hold,直到支付成功。

通过结账链接收款

默认情况下,即时套餐变更会直接从订阅已保存的支付方式中扣款。将 collect_via_payment_link: true 设置为将客户发送到托管结账页面 — 当没有允许进行 off-session 扣款的已保存支付方式,或你希望客户主动确认新价格时,这会很有用。
这也是 Settings → SubscriptionsCollect Plan Change Payments by Payment Link 开关的实现方式:它会让内置 Customer Portal 的套餐变更流程通过结账页面完成,而不是使用已保存的卡。

要求

只有在以下所有条件均满足时,collect_via_payment_link: true 才会成功 — 否则请求会失败并返回 422
  • 企业已启用 allow_plan_change_via_payment_link 能力(Settings → Subscriptions → Collect Plan Change Payments by Payment Link)。
  • effective_atimmediately(默认值)。已安排的变更(next_billing_date)不需要结账页面,因为在变更生效前不会收取任何费用。
  • 有效的 on_payment_failure 解析为 prevent_change。你不必显式传入该参数 — 如果企业级默认值(请参阅下方的 Business & Collection Defaults)已经是 prevent_change,省略此字段也满足要求。显式设置为 apply_change,或解析后的默认值为 apply_change 时,请求会失败并返回 422
collect_via_payment_link 不仅适用于升级 — 只要满足上述要求,它适用于任何会产生扣款的即时变更,包括降级。
如果变更金额为零或产生抵扣proration_billing_mode: do_not_bill,或其他在本周期内净额为零的模式 — 就没有需要放到结账页面上的金额。不会签发支付链接,payment_link 及相关字段会返回 null,并且变更会立即生效,与不使用 collect_via_payment_link 时相同。这不是 422;该标志仅在存在需要收取的正金额时生效。如果你对一般套餐变更而非明确的升级统一设置 collect_via_payment_link,请先调用预览套餐变更,仅在预览金额值得收取时请求链接。
成功的请求会返回结账句柄:

链接未支付时会发生什么

  • 订阅保持在其当前套餐 — product_idrecurring_pre_tax_amountnext_billing_date 在链接支付前都不会改变。
  • 当链接处于待处理状态时,对同一订阅发起进一步的 change-plan 请求会被 409 PendingPlanChangeExists 拒绝。如有需要,可使用 DELETE /subscriptions/{subscription_id}/change-plan/scheduled 取消已安排的变更,但该端点无法取消待支付链接变更 — 只有支付成功或链接过期才能取消它。
  • 如果银行卡被拒付,客户可以在同一个结账会话中重试;新的 change-plan 调用不是重试方式。
  • 如果链接始终未支付,则会在 expires_on 后停止工作 — 稍后订阅会自动允许接收新的套餐变更请求。
  • 如果之前已有一个已安排的变更(next_billing_date),并且你使用 cancel_scheduled_change_plan: true 替换它,则在链接未支付期间,原计划仍会保留;只有链接支付后,原计划才会在应用新套餐的同一事务中被取消。
一旦签发即时支付链接变更,该订阅的所有后续套餐变更请求(包括无副作用的预览)都会被阻止,直到链接处理完成。不要签发你不打算让客户立即支付的链接。

管理附加项

变更订阅套餐时,你还可以修改附加项:
附加项会计入按比例计费计算,并根据选定的按比例计费模式收取费用。

应用折扣码

变更订阅套餐时,你可以应用一个或多个堆叠折扣码(最多 20 个,按数组顺序应用)。这适用于在升级或迁移时提供促销价格。

套餐变更时的折扣行为

此端点上的单数 discount_code 字段已弃用,但为保持向后兼容仍然有效 — 现有集成不必立即更改。它不能在同一个请求中与 discount_codes 组合使用。请在方便时迁移到数组形式。
使用带有 discount_codesPreview Plan Change API,在客户确认套餐变更之前,准确展示他们可以节省的金额。

按比例计费模式

选择变更套餐时向客户收取费用的方式:

prorated_immediately

  • 收取当前周期内差额的按比例金额
  • 如果处于试用期,则立即收费并立即切换到新套餐
  • 降级:可能产生按比例计算的抵扣,并应用于未来续订

full_immediately

  • 立即收取新套餐的全额
  • 忽略旧套餐的剩余时间
使用 difference_immediately 降级所创建的抵扣与基于额度的计费权益不同,且作用域限定在订阅内。它们会自动应用于同一订阅的未来续订,不能在订阅之间转移。

difference_immediately

  • 升级:立即收取新旧套餐之间的价格差额
  • 降级:将剩余价值添加为订阅内部额度,并在续订时自动应用

do_not_bill

  • 不计算任何费用或抵扣
  • 客户立即切换到新套餐,不进行任何账单调整
  • 计费周期保持不变
  • 适用于礼遇迁移、切换到免费套餐或吸收价格差异

示例场景

请始终使用以下标准数字:
  • 当前套餐:Basic$30/月
  • 升级目标:Pro$80/月
  • 降级目标(从 Pro):Starter$20/月
  • 计费周期:30 天,从 January 1 开始
  • 套餐变更发生在 January 16(剩余 15 天,已使用 15 天)

各模式如何处理账单

选择 prorated_immediately 以实现公平的按时间计费;选择 full_immediately 以重启计费;使用 difference_immediately 进行简单升级并在降级时自动生成抵扣;或者使用 do_not_bill,在不进行任何账单调整的情况下切换套餐。

处理支付失败

使用 on_payment_failure 参数控制套餐变更支付失败时的处理方式。

支付失败模式

如果未指定,on_payment_failure 参数会使用你在控制面板中配置的企业级默认设置。

何时使用各模式

企业与收款默认设置

你可以在企业级别一次性设置默认的升级和降级行为,而不是在每次套餐变更时传入按比例计费参数。这些默认设置适用于所有 Customer Portal 套餐变更,并且可以按产品集合覆盖 升级和降级分别拥有独立的默认设置: Settings → Subscriptions 下配置企业默认设置,并在每个产品集合中配置集合覆盖设置。每个集合字段彼此独立 — 留空则继承企业默认设置,设置值则仅覆盖该集合的设置。

解析顺序

对于任何套餐变更,各设置均按以下顺序解析:
显式传入 Change Plan API 的值始终优先。只有未提供显式值时,才会使用企业和集合默认设置 — 所有从 Customer Portal 发起的套餐变更均属于这种情况。
一种常见配置是:将升级设置为 immediately + difference_immediately,让客户支付差额并立即获得访问权限;将降级设置为 next_billing_date,让客户在计费周期结束前继续使用当前套餐。

处理 webhooks

通过 webhooks 跟踪订阅状态,以确认套餐变更和支付。

需要处理的事件类型

  • subscription.active:订阅已激活
  • subscription.plan_changed:订阅套餐已变更(升级/降级/附加项变更)
  • subscription.on_hold:扣款失败,续订已停止
  • subscription.renewed:续订成功
  • payment.succeeded:套餐变更或续订的支付成功
  • payment.failed:支付失败
我们建议根据订阅事件驱动业务逻辑,并使用支付事件进行确认和对账。

验证签名并处理意图

有关详细的 payload schema,请参阅 Subscription webhook payloadsPayment webhook payloads

最佳实践

遵循以下建议,以确保订阅套餐变更可靠:

套餐变更策略

  • 充分测试:上线前始终在测试模式下测试套餐变更
  • 谨慎选择按比例计费方式:选择符合业务模式的按比例计费模式
  • 妥善处理失败:实现适当的错误处理和重试逻辑
  • 监控成功率:跟踪套餐变更成功/失败率并调查问题

Webhook 实现

  • 验证签名:始终验证 webhook 签名以确保真实性
  • 实现幂等性:妥善处理重复的 webhook 事件
  • 异步处理:不要让繁重操作阻塞 webhook 响应
  • 记录所有内容:维护详细日志以便调试和审计

用户体验

  • 清晰沟通:告知客户账单变更及其时间
  • 提供确认信息:为成功的套餐变更发送确认邮件
  • 处理边界情况:考虑试用期、按比例计费和支付失败
  • 立即更新 UI:在应用界面中反映套餐变更

常见问题与解决方案

解决订阅套餐变更期间常见的问题:
症状:API 调用成功,但订阅仍保持旧套餐常见原因
  • Webhook 处理失败或延迟
  • 收到 webhook 后应用状态未更新
  • 更新状态时出现数据库事务问题
解决方案
  • 实现带有重试逻辑的健壮 webhook 处理
  • 对状态更新使用幂等操作
  • 添加监控,以检测并提醒遗漏的 webhook 事件
  • 验证 webhook 端点可访问且响应正常
症状:客户降级后看不到额度余额常见原因
  • 对按比例计费模式的预期不同:使用 difference_immediately 降级时,抵扣为完整套餐价格差额;而 prorated_immediately 会根据周期剩余时间创建按比例计算的抵扣
  • 抵扣仅限于特定订阅,不能在订阅之间转移
  • 客户控制面板中未显示额度余额
解决方案
  • 如果希望自动生成抵扣,请在降级时使用 difference_immediately
  • 向客户说明抵扣会应用于同一订阅的未来续订
  • 实现 Customer Portal 以显示额度余额
  • 查看下一张发票预览,确认已应用的抵扣
症状:由于签名无效,webhook 事件被拒绝常见原因
  • Webhook secret key 不正确
  • 在验证签名之前修改了原始请求正文
  • 使用了错误的签名验证算法
解决方案
  • 确认使用的是控制面板中的正确 DODO_WEBHOOK_SECRET
  • 在任何 JSON 解析中间件运行前读取原始请求正文
  • 使用适用于你平台的标准 webhook 验证库
  • 在开发环境中测试 webhook 签名验证
症状:API 返回 422 Unprocessable Entity 错误常见原因
  • 订阅 ID 或产品 ID 无效
  • 订阅不处于 active 状态
  • 缺少必需参数
  • 产品不支持套餐变更
解决方案
  • 验证订阅存在且处于 active 状态
  • 检查产品 ID 是否有效且可用
  • 确保提供所有必需参数
  • 查阅 API 文档了解参数要求
症状:已发起套餐变更,但即时扣款失败常见原因
  • 客户支付方式中的资金不足
  • 支付方式已过期或无效
  • 银行拒绝交易
  • 欺诈检测阻止了扣款
解决方案
  • 适当处理 payment.failed webhook 事件
  • 通知客户更新支付方式
  • 为临时性失败实现重试逻辑
  • 考虑允许即时扣款失败时仍变更套餐
症状:套餐变更扣款失败,订阅转为 on_hold 状态发生的情况: 套餐变更扣款失败时,订阅会自动进入 on_hold 状态。在更新支付方式之前,订阅不会自动续订。解决方案:更新支付方式以重新激活订阅套餐变更失败后,要从 on_hold 状态重新激活订阅:
  1. 更新支付方式:使用 Update Payment Method API
  2. 自动创建扣款:API 会自动为剩余应付款创建扣款
  3. 生成发票:为该扣款生成发票
  4. 处理支付:使用新的支付方式处理支付
  5. 重新激活:支付成功后,订阅会重新激活并进入 active 状态
需要监控的 Webhook 事件
  • subscription.on_hold:订阅进入暂停状态(套餐变更扣款失败时收到)
  • payment.succeeded:剩余应付款支付成功(更新支付方式后)
  • subscription.active:支付成功后订阅重新激活
最佳实践
  • 套餐变更扣款失败时立即通知客户
  • 清晰说明如何更新支付方式
  • 监控 webhook 事件以跟踪重新激活状态
  • 考虑为临时性支付失败实现自动重试逻辑

Update Payment Method API Reference

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

测试你的实现

按照以下步骤全面测试订阅套餐变更实现:
1

Set up test environment

  • 使用测试 API keys 和测试产品
  • 创建不同套餐类型的测试订阅
  • 配置测试 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 状态码

套餐变更请求处理成功。除成功的 collect_via_payment_link 请求会返回结账句柄外,响应正文为空 — 请参阅通过结账链接收款。如果 on_payment_failure=prevent_change,套餐变更会保持待处理,直到支付成功。
请求参数无效。请检查所有必需字段是否已提供且格式正确。
API key 无效或缺失。请验证 DODO_PAYMENTS_API_KEY 是否正确且具有适当权限。
找不到订阅 ID,或该订阅不属于你的账户。
此订阅已有待处理的套餐变更(PendingPlanChangeExists)。对于已安排的变更,请先使用 DELETE /subscriptions/{subscription_id}/change-plan/scheduled 取消,再提交新的变更。对于待处理的支付链接变更,没有可用的取消端点 — 客户支付或链接过期后,订阅才会接受新的套餐变更请求。
订阅处于非 active 或 on-demand 状态,或者该请求不符合 collect_via_payment_link 的条件 — 企业未启用相应能力、effective_at 不是 immediately,或 on_payment_failure 不是 prevent_change。请参阅要求
发生服务器错误。请稍等片刻后重试请求。

错误响应格式

后续步骤

最后修改于 2026年8月26日