Change Plan API
Plan Change Preview
Integration Guide
What Is a Subscription Upgrade or Downgrade?
Change a customer’s subscription plan to move them between tiers, adjust quantity for seat-based products, or migrate to a new product. The API automatically calculates prorations and charges based on your chosen billing mode.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
- Credits the unused portion of the current cycle, prorated by time remaining
- Then charges a full cycle at the new plan — the new plan price is never prorated
- Net charge = full new cycle − (remaining fraction × full old cycle)
Implement the Change Plan API
prorated_immediately、full_immediately、difference_immediately 或 do_not_bill。null 或发送空数组都会移除任何现有附加项,因此请包含当前附加项以保留它们。prevent_change:在付款成功前让订阅保持当前方案apply_change(默认):无论付款结果如何,立即应用方案变更
allow_plan_change_via_payment_link capability (Settings → Subscriptions → Collect Plan Change Payments by Payment Link), effective_at: immediately, and on_payment_failure: prevent_change. See Collecting Payment via a Checkout Link. Ignored by the preview route.subscription_id、payment_id 和 status。需要设置 collect_via_payment_link: true;否则请求会因 422 失败。请参阅付款后重定向客户。409。请参阅替换待处理的 payment link。preview 路由会忽略此字段。- 未提供 /
null— 如果适用于新产品,则保留带有preserve_on_plan_change=true的现有折扣。 [](空数组) — 从订阅中移除所有现有折扣。["CODE_A", "CODE_B", ...]— 使用此堆叠集合替换所有现有折扣。
discount_codes。此字段仍可用于向后兼容,但不能在同一请求中与 discount_codes 组合使用。immediately(默认):立即应用方案变更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
- 使用不同场景测试所有 proration 模式
- 验证 webhook 处理是否正常
- 监控方案变更成功率
- 为失败的方案变更设置提醒
预览方案变更
在提交方案变更前,使用 Preview API 向客户准确展示将收取的费用:- Node.js SDK
- Python SDK
immediate_charge.summary 中,customer_credits 是客户信用余额的净变动,使用 customer_credits_currency 指定的货币表示。这是客户 credit wallet 的货币,也就是订阅货币。它可能与摘要中的 currency 不同,后者适用于 total_amount 和 tax。例如,使用 INR 支付 USD 订阅的客户会看到 USD credits。Change Plan API
使用 Change Plan API 修改有效订阅的产品、数量和 proration 行为。快速开始示例
- 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} 重新读取订阅 — 对于 payment-link 情况,请参阅链接未付款期间会发生什么。通过 Checkout Link 收款
默认情况下,立即方案变更会直接向订阅保存的付款方式收费。将collect_via_payment_link: true 设置为将客户发送到托管结账页面,而不是直接收费 — 当没有保存的付款方式,或希望客户主动确认新价格时,此方式非常有用。
要求
只有在满足以下所有条件时,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)不需要 checkout 页面。- 请求设置了
on_payment_failure: prevent_change。显式设置apply_change会因422失败。
collect_via_payment_link 适用于任何会产生收费的立即变更,包括降级。payment_link 及其他 checkout 字段会返回 null,并且变更会立即应用。这不是 422。请求 link 前,先调用 Preview Plan Change 检查金额。
- Node.js SDK
- Python SDK
- HTTP
链接未付款期间会发生什么
- 订阅会保持当前方案 — 在链接付款前,
product_id、recurring_pre_tax_amount和next_billing_date均不会改变。 - 链接处于待处理状态时,后续
change-plan请求会因409 PendingPlanChangeExists被拒绝。要替换待处理变更,请在新请求中发送cancel_older_payment_link: true。请参阅替换待处理的 payment link。 - 付款被拒后重试时,再次调用
change-plan以获取新链接。 - 如果链接始终未付款,则会在
expires_on后停止工作 — 随后订阅会自动恢复为可以接受新方案变更请求的状态。 - 如果此前已有已安排的变更,并使用
cancel_scheduled_change_plan: true替换它,则在链接未付款期间原计划仍会保留;只有链接付款后才会取消原计划 — 新方案会在同一事务中应用。
付款后重定向客户
设置return_url,使客户付款后返回您的网站:
return_url:
return_url 需要 collect_via_payment_link: true。没有该字段,请求会因 422 失败。
替换待处理的 Payment Link
如果客户未付款就离开 checkout,请在下一次change-plan 请求中设置 cancel_older_payment_link: true。Dodo Payments 会取消未付款链接,并使用新的方案变更替换待处理变更。已取消的链接不再接受付款,即使 checkout 页面已经打开。
change-plan 请求都会生成自己的 invoice。被替换的方案变更及其 invoice 会被取消。如果没有待处理的方案变更,cancel_older_payment_link 不会产生任何效果。
管理附加组件
变更订阅方案时,也可以修改附加组件:应用折扣码
变更订阅方案时应用一个或多个堆叠折扣码(最多 20 个,按数组顺序应用):- Node.js SDK
- Python SDK
- HTTP
方案变更时的折扣行为
discount_code 字段已弃用,但仍可用于向后兼容 — 现有集成无需立即更改。它不能在同一请求中与 discount_codes 组合使用。方便时请迁移到数组形式。Proration 模式
选择变更方案时向客户收费的方式:prorated_immediately
- 按剩余时间对当前周期未使用的部分(基础方案、数量和附加组件)进行 proration,并记为 credit
- 然后按新方案、数量和附加组件收取一个完整周期的费用。该收费本身不会进行 proration
- 立即净收费 =(新的完整周期)−(剩余比例 × 旧的完整周期)
- 如果 credit 超过新周期收费(降级时很常见),差额会作为订阅范围的 credit 保留,用于未来续订
- 如果处于 trial,则立即收费并立即切换到新方案
full_immediately
- 立即收取新方案的全额
- 忽略旧方案的剩余时间 — 当前周期不产生 credit
prorated_immediately 创建的 credit,以及使用 difference_immediately 降级时创建的 credit,均属于订阅范围,与 Credit-Based Billing 权益不同。它们会自动应用于同一订阅的未来续订,且不能在订阅之间转移。difference_immediately
- 升级:立即收取新旧方案之间的价格差额
- 降级:将剩余价值作为内部 credit 添加到订阅,并在续订时自动应用
do_not_bill
- 不计算收费或 credit
- 客户立即切换到新方案,不进行任何计费调整
- 计费周期保持不变
- 适用于礼遇迁移、切换到免费方案或吸收价格差异
示例场景
始终使用以下标准数字:- 当前方案:Basic,$30/月
- 升级目标:Pro,$80/月
- 降级目标(从 Pro):Starter,$20/月
- 计费周期:30 天,从 1 月 1 日开始
- 方案变更发生在 1 月 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 参数会使用您在 dashboard 中配置的商家级默认设置。各模式的适用场景
商家与收款默认设置
在 Settings → Subscriptions 下设置商家级别的升级和降级行为。这些默认设置适用于所有 Customer Portal 方案变更,也可以按产品集合覆盖。 升级和降级分别有独立的默认设置:解析顺序
对于任意方案变更,每项设置都按以下顺序解析:处理 Webhook
通过 webhook 跟踪订阅状态,以确认方案变更和付款。要处理的事件类型
subscription.active:订阅已激活subscription.plan_changed:订阅方案已变更(升级/降级/附加组件变更)subscription.on_hold:收费失败,续订已停止subscription.renewed:续订成功payment.succeeded:方案变更或续订的付款成功payment.failed:付款失败
验证签名并处理意图
- Next.js Route Handler
- Express.js
最佳实践
方案变更策略
- 充分测试:生产环境前始终在测试模式下测试方案变更
- 谨慎选择 proration:选择符合业务模式的 proration 模式
- 妥善处理失败:实现适当的错误处理和重试逻辑
- 监控成功率:跟踪方案变更成功/失败率并调查问题
Webhook 实现
- 验证签名:始终验证 webhook 签名以确保真实性
- 实现幂等性:妥善处理重复的 webhook 事件
- 异步处理:不要让繁重操作阻塞 webhook 响应
- 记录所有内容:维护详细日志,以便调试和审计
用户体验
- 清晰沟通:告知客户计费变更及时间安排
- 提供确认:为成功的方案变更发送确认邮件
- 处理边界情况:考虑 trial 周期、proration 和付款失败
- 立即更新 UI:在应用界面中反映方案变更
常见问题与解决方案
解决订阅方案变更期间遇到的常见问题:Charge created but subscription not updated
Charge created but subscription not updated
- Webhook 处理失败或延迟
- 收到 webhook 后未更新应用状态
- 更新状态时出现数据库事务问题
- 使用重试逻辑实现 webhook 处理
- 对状态更新使用幂等操作
- 添加监控,以检测并提醒遗漏的 webhook 事件
- 验证 webhook endpoint 可访问且能正确响应
Credits not applied after downgrade
Credits not applied after downgrade
- Proration 模式预期不一致:使用
difference_immediately降级时,credit 为完整方案价格差额;而prorated_immediately会抵扣旧周期未使用时间,再收取新方案的完整周期费用 — 因此只有当该 credit 超过新方案价格时,才会保留 credit balance - Credit 属于特定订阅,不能在订阅之间转移
- 客户控制面板中看不到 credit balance
- 如果希望自动生成 credit,降级时使用
difference_immediately - 向客户说明 credit 会应用于同一订阅的未来续订
- 实现 Customer Portal 以显示 credit balance
- 检查下一张 invoice 的 preview,查看已应用的 credit
Webhook signature verification fails
Webhook signature verification fails
- Webhook secret key 不正确
- 签名验证前原始请求正文被修改
- 签名验证算法错误
- 确认使用的是 dashboard 中正确的
DODO_PAYMENTS_WEBHOOK_KEY - 在任何 JSON 解析 middleware 之前读取原始请求正文
- 使用适用于您平台的标准 webhook 验证库
- 在开发环境中测试 webhook 签名验证
Plan change fails with 422 error
Plan change fails with 422 error
- Subscription ID 或 product ID 无效
- 订阅不处于 active 状态
- 缺少必填参数
- 产品不支持方案变更
- 确认订阅存在且处于 active 状态
- 检查 product 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 会自动为剩余欠款创建收费
- 生成 invoice:系统会为该收费生成 invoice
- 处理付款:使用新的付款方式处理付款
- 重新激活:付款成功后,订阅会重新进入
active状态
subscription.on_hold:订阅被暂停(方案变更收费失败时接收)payment.succeeded:剩余欠款付款成功(更新付款方式后)subscription.active:付款成功后订阅重新激活
- 方案变更收费失败时立即通知客户
- 清晰说明如何更新付款方式
- 监控 webhook 事件以跟踪重新激活状态
- 考虑为临时付款失败实现自动重试逻辑
Update Payment Method API Reference
测试您的实现
全面测试订阅方案变更实现:Set up test environment
- 使用 test API keys 和 test products
- 创建不同方案类型的测试订阅
- 配置 test webhook endpoint
- 设置监控和日志
Test different proration modes
- 使用不同的计费周期位置测试
prorated_immediately - 测试
difference_immediately的升级和降级 - 测试
full_immediately以重置计费周期 - 测试
do_not_bill的无收费/无 credit 方案切换 - 验证 credit 计算是否正确
Test webhook handling
- 验证是否收到所有相关 webhook 事件
- 测试 webhook 签名验证
- 妥善处理重复的 webhook 事件
- 测试 webhook 处理失败场景
Test error scenarios
- 使用无效 subscription ID 测试
- 使用过期付款方式测试
- 测试网络失败和超时
- 使用余额不足的情况测试
Monitor in production
- 为失败的方案变更设置提醒
- 监控 webhook 处理时间
- 跟踪方案变更成功率
- 查看客户支持工单中的方案变更问题
错误处理
在实现中妥善处理常见 API 错误:HTTP 状态码
200 OK
200 OK
payment_id、payment_link、client_secret 和 expires_on 的 ChangePlanResponse。四个字段均可为 null,因此普通 off-session 变更的正文会序列化为 {};成功的 collect_via_payment_link 请求会填充这些字段并返回 checkout 句柄 — 请参阅通过 Checkout Link 收款。如果为 on_payment_failure=prevent_change,方案变更会保持 pending,直到付款成功。400 Bad Request
400 Bad Request
409 Conflict
409 Conflict
PendingPlanChangeExists)。对于已安排的变更,请先使用 DELETE /subscriptions/{subscription_id}/change-plan/scheduled 取消,再提交新的变更。对于待处理的payment-link变更,请在新请求中发送 cancel_older_payment_link: true 以替换它。请参阅替换待处理的 payment link。使用 cancel_older_payment_link: true 时,409 表示客户正在支付之前的链接(PLAN_CHANGE_PAYMENT_IN_PROGRESS),或已经支付该链接(PLAN_CHANGE_PAYMENT_ALREADY_COMPLETED)。422 Unprocessable Entity
422 Unprocessable Entity
collect_via_payment_link 的条件 — 商家未启用该功能,effective_at 不是 immediately,或 on_payment_failure 不是 prevent_change。请参阅要求。不存在或不属于您账户的 subscription ID 会返回带有 NOT_FOUND 代码的 404。500 Internal Server Error
500 Internal Server Error
错误响应格式
错误会返回 JSON 正文,其中包含code 和人类可读的 message:
后续步骤
- 查看 Change Plan API
- 探索 Credit-Based Billing
- 为
subscription.on_hold实现提醒 - 查看 Webhook Integration Guide