Change Plan API
Plan Change Preview
Integration Guide
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
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
Step-by-Step Implementation Guide
Follow this comprehensive guide to implement subscription plan changes in your application:Understand Plan Change Requirements
- 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
Choose Your Proration Strategy
- prorated_immediately
- difference_immediately
- full_immediately
- do_not_bill
- 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
Implement the Change Plan API
prorated_immediately, full_immediately, difference_immediately, or do_not_bill.prevent_change: Keep subscription on current plan until payment succeedsapply_change(default): Apply plan change immediately regardless of payment outcome
allow_plan_change_via_payment_link 能力(Settings → Subscriptions → Collect Plan Change Payments by Payment Link)、effective_at: immediately 和 on_payment_failure: prevent_change。请参阅通过结账链接收款。预览路由会忽略此参数。- 未提供 /
null— 如果现有折扣适用于新产品,则会保留带有preserve_on_plan_change=true的现有折扣。 [](空数组) — 移除订阅中的所有现有折扣。["CODE_A", "CODE_B", ...]— 使用此堆叠折扣集替换所有现有折扣。
discount_codes。为保持向后兼容,此字段仍然有效,但不能在同一个请求中与 discount_codes 组合使用。immediately(默认):立即应用套餐变更next_billing_date:将变更安排在下一账单日期。客户在计费周期结束前仍保留当前套餐。
next_billing_date,以便客户在计费周期结束前继续享有当前套餐的权益。Handle Webhook Events
subscription.active:套餐变更成功,订阅已更新subscription.plan_changed:订阅套餐已变更(升级/降级/附加项更新)subscription.on_hold:套餐变更扣款失败,续订已停止payment.succeeded:套餐变更的即时扣款成功payment.failed:即时扣款失败
Update Your Application State
- 根据新套餐授予或撤销功能
- 使用新套餐详情更新客户控制面板
- 发送有关套餐变更的确认邮件
- 记录账单变更以供审计
Test and Monitor
- 使用不同场景测试所有按比例计费模式
- 验证 webhook 处理是否正常工作
- 监控套餐变更成功率
- 为失败的套餐变更设置提醒
预览套餐变更
在提交套餐变更之前,使用 Preview API 向客户准确展示他们将被收取的金额:- Node.js SDK
- Python SDK
Change Plan API
使用 Change Plan API 修改有效订阅的产品、数量和按比例计费行为。快速入门示例
- Node.js SDK
- Python SDK
- Go SDK
- HTTP
200 OK — 此时任何扣款尚未实际结算。响应正文(ChangePlanResponse)的内容取决于变更的收款方式:
collect_via_payment_link 请求,结果会在之后异步确定 — 响应只会向你提供一个结账链接,订阅保持当前套餐不变,并且在客户通过该链接完成支付之前,无法得知结果。无论哪种情况,都不要根据此响应推断结果。请通过 webhook(payment.succeeded、payment.failed、subscription.plan_changed)确认,或使用 GET /subscriptions/{subscription_id} 重新读取订阅 — 有关支付链接的具体说明,请参阅链接未支付时会发生什么。通过结账链接收款
默认情况下,即时套餐变更会直接从订阅已保存的支付方式中扣款。将collect_via_payment_link: true 设置为将客户发送到托管结账页面 — 当没有允许进行 off-session 扣款的已保存支付方式,或你希望客户主动确认新价格时,这会很有用。
要求
只有在以下所有条件均满足时,collect_via_payment_link: true 才会成功 — 否则请求会失败并返回 422:
- 企业已启用
allow_plan_change_via_payment_link能力(Settings → Subscriptions → Collect Plan Change Payments by Payment Link)。 effective_at为immediately(默认值)。已安排的变更(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,请先调用预览套餐变更,仅在预览金额值得收取时请求链接。
- Node.js SDK
- Python SDK
- HTTP
链接未支付时会发生什么
- 订阅保持在其当前套餐 —
product_id、recurring_pre_tax_amount和next_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 个,按数组顺序应用)。这适用于在升级或迁移时提供促销价格。- Node.js SDK
- Python SDK
- HTTP
套餐变更时的折扣行为
discount_code 字段已弃用,但为保持向后兼容仍然有效 — 现有集成不必立即更改。它不能在同一个请求中与 discount_codes 组合使用。请在方便时迁移到数组形式。按比例计费模式
选择变更套餐时向客户收取费用的方式: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 天)
Upgrade: Basic ($30) → Pro ($80) with prorated_immediately
Upgrade: Basic ($30) → Pro ($80) with prorated_immediately
Downgrade: Pro ($80) → Starter ($20) with prorated_immediately
Downgrade: Pro ($80) → Starter ($20) with prorated_immediately
Upgrade: Basic ($30) → Pro ($80) with difference_immediately
Upgrade: Basic ($30) → Pro ($80) with difference_immediately
Downgrade: Pro ($80) → Starter ($20) with difference_immediately
Downgrade: Pro ($80) → Starter ($20) with difference_immediately
Upgrade: Basic ($30) → Pro ($80) with full_immediately
Upgrade: Basic ($30) → Pro ($80) with full_immediately
Mid-cycle upgrade with add-ons using prorated_immediately
Mid-cycle upgrade with add-ons using prorated_immediately
各模式如何处理账单
处理支付失败
使用on_payment_failure 参数控制套餐变更支付失败时的处理方式。
支付失败模式
- prevent_change (Recommended for critical upgrades)
- apply_change (Default)
- 套餐变更标记为“pending”
- 客户继续使用当前套餐
- 只有支付成功后,订阅才会转为
active状态 - 适用于希望在授予升级功能前确保已完成支付的场景
on_payment_failure 参数会使用你在控制面板中配置的企业级默认设置。何时使用各模式
企业与收款默认设置
你可以在企业级别一次性设置默认的升级和降级行为,而不是在每次套餐变更时传入按比例计费参数。这些默认设置适用于所有 Customer Portal 套餐变更,并且可以按产品集合覆盖。 升级和降级分别拥有独立的默认设置:解析顺序
对于任何套餐变更,各设置均按以下顺序解析:处理 webhooks
通过 webhooks 跟踪订阅状态,以确认套餐变更和支付。需要处理的事件类型
subscription.active:订阅已激活subscription.plan_changed:订阅套餐已变更(升级/降级/附加项变更)subscription.on_hold:扣款失败,续订已停止subscription.renewed:续订成功payment.succeeded:套餐变更或续订的支付成功payment.failed:支付失败
验证签名并处理意图
- Next.js Route Handler
- Express.js
最佳实践
遵循以下建议,以确保订阅套餐变更可靠:套餐变更策略
- 充分测试:上线前始终在测试模式下测试套餐变更
- 谨慎选择按比例计费方式:选择符合业务模式的按比例计费模式
- 妥善处理失败:实现适当的错误处理和重试逻辑
- 监控成功率:跟踪套餐变更成功/失败率并调查问题
Webhook 实现
- 验证签名:始终验证 webhook 签名以确保真实性
- 实现幂等性:妥善处理重复的 webhook 事件
- 异步处理:不要让繁重操作阻塞 webhook 响应
- 记录所有内容:维护详细日志以便调试和审计
用户体验
- 清晰沟通:告知客户账单变更及其时间
- 提供确认信息:为成功的套餐变更发送确认邮件
- 处理边界情况:考虑试用期、按比例计费和支付失败
- 立即更新 UI:在应用界面中反映套餐变更
常见问题与解决方案
解决订阅套餐变更期间常见的问题:Charge created but subscription not updated
Charge created but subscription not updated
- Webhook 处理失败或延迟
- 收到 webhook 后应用状态未更新
- 更新状态时出现数据库事务问题
- 实现带有重试逻辑的健壮 webhook 处理
- 对状态更新使用幂等操作
- 添加监控,以检测并提醒遗漏的 webhook 事件
- 验证 webhook 端点可访问且响应正常
Credits not applied after downgrade
Credits not applied after downgrade
- 对按比例计费模式的预期不同:使用
difference_immediately降级时,抵扣为完整套餐价格差额;而prorated_immediately会根据周期剩余时间创建按比例计算的抵扣 - 抵扣仅限于特定订阅,不能在订阅之间转移
- 客户控制面板中未显示额度余额
- 如果希望自动生成抵扣,请在降级时使用
difference_immediately - 向客户说明抵扣会应用于同一订阅的未来续订
- 实现 Customer Portal 以显示额度余额
- 查看下一张发票预览,确认已应用的抵扣
Webhook signature verification fails
Webhook signature verification fails
- Webhook secret key 不正确
- 在验证签名之前修改了原始请求正文
- 使用了错误的签名验证算法
- 确认使用的是控制面板中的正确
DODO_WEBHOOK_SECRET - 在任何 JSON 解析中间件运行前读取原始请求正文
- 使用适用于你平台的标准 webhook 验证库
- 在开发环境中测试 webhook 签名验证
Plan change fails with 422 error
Plan change fails with 422 error
- 订阅 ID 或产品 ID 无效
- 订阅不处于 active 状态
- 缺少必需参数
- 产品不支持套餐变更
- 验证订阅存在且处于 active 状态
- 检查产品 ID 是否有效且可用
- 确保提供所有必需参数
- 查阅 API 文档了解参数要求
Immediate charge fails during plan change
Immediate charge fails during plan change
- 客户支付方式中的资金不足
- 支付方式已过期或无效
- 银行拒绝交易
- 欺诈检测阻止了扣款
- 适当处理
payment.failedwebhook 事件 - 通知客户更新支付方式
- 为临时性失败实现重试逻辑
- 考虑允许即时扣款失败时仍变更套餐
Subscription on hold after plan change
Subscription on hold after plan change
on_hold 状态发生的情况:
套餐变更扣款失败时,订阅会自动进入 on_hold 状态。在更新支付方式之前,订阅不会自动续订。解决方案:更新支付方式以重新激活订阅套餐变更失败后,要从 on_hold 状态重新激活订阅:- 更新支付方式:使用 Update Payment Method API
- 自动创建扣款:API 会自动为剩余应付款创建扣款
- 生成发票:为该扣款生成发票
- 处理支付:使用新的支付方式处理支付
- 重新激活:支付成功后,订阅会重新激活并进入
active状态
subscription.on_hold:订阅进入暂停状态(套餐变更扣款失败时收到)payment.succeeded:剩余应付款支付成功(更新支付方式后)subscription.active:支付成功后订阅重新激活
- 套餐变更扣款失败时立即通知客户
- 清晰说明如何更新支付方式
- 监控 webhook 事件以跟踪重新激活状态
- 考虑为临时性支付失败实现自动重试逻辑
Update Payment Method API Reference
测试你的实现
按照以下步骤全面测试订阅套餐变更实现:Set up test environment
- 使用测试 API keys 和测试产品
- 创建不同套餐类型的测试订阅
- 配置测试 webhook 端点
- 设置监控和日志
Test different proration modes
- 在计费周期的不同位置测试
prorated_immediately - 测试
difference_immediately的升级和降级 - 测试
full_immediately以重置计费周期 - 测试
do_not_bill的无收费/无抵扣套餐切换 - 验证额度计算是否正确
Test webhook handling
- 验证是否收到所有相关 webhook 事件
- 测试 webhook 签名验证
- 妥善处理重复的 webhook 事件
- 测试 webhook 处理失败场景
Test error scenarios
- 使用无效订阅 ID 进行测试
- 使用已过期的支付方式进行测试
- 测试网络故障和超时
- 使用资金不足的支付方式进行测试
Monitor in production
- 为失败的套餐变更设置提醒
- 监控 webhook 处理时间
- 跟踪套餐变更成功率
- 查看客户支持工单中的套餐变更问题
错误处理
在实现中妥善处理常见 API 错误:HTTP 状态码
200 OK
200 OK
collect_via_payment_link 请求会返回结账句柄外,响应正文为空 — 请参阅通过结账链接收款。如果 on_payment_failure=prevent_change,套餐变更会保持待处理,直到支付成功。400 Bad Request
400 Bad Request
404 Not Found
404 Not Found
409 Conflict
409 Conflict
PendingPlanChangeExists)。对于已安排的变更,请先使用 DELETE /subscriptions/{subscription_id}/change-plan/scheduled 取消,再提交新的变更。对于待处理的支付链接变更,没有可用的取消端点 — 客户支付或链接过期后,订阅才会接受新的套餐变更请求。422 Unprocessable Entity
422 Unprocessable Entity
collect_via_payment_link 的条件 — 企业未启用相应能力、effective_at 不是 immediately,或 on_payment_failure 不是 prevent_change。请参阅要求。500 Internal Server Error
500 Internal Server Error
错误响应格式
后续步骤
- 查看 Change Plan API
- 探索 基于额度的计费
- 为
subscription.on_hold实现提醒 - 查看我们的 Webhook Integration Guide