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
required
The ID of the active subscription to modify.
string
required
The new product ID to change the subscription to.
integer
required
Number of units for the new plan (for seat-based products).
string
required
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.
Collect the plan-change amount with a payment link instead of charging the subscription’s saved payment method. The customer pays on a hosted checkout page.Requires the business’s 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.
array
Optional stacked discount codes to apply to the new plan (max 20, applied in array order). Behavior depends on what you pass:
  • Not provided / null — existing discounts with preserve_on_plan_change=true are preserved if applicable to the new product.
  • [] (empty array) — removes all existing discounts from the subscription.
  • ["CODE_A", "CODE_B", ...] — replaces any existing discounts with this stacked set.
string
deprecated
Deprecated — prefer discount_codes for new integrations. This field still works for backward compatibility, but cannot be combined with discount_codes in the same request.
string
default:"immediately"
When to apply the plan change:
  • immediately (default): Apply the plan change right away
  • next_billing_date: Schedule the change for the next billing date. The customer retains their current plan until the billing period ends.
Use next_billing_date for downgrades so customers keep their current plan benefits until the end of the billing period.
4

Handle Webhook Events

Set up webhook handling to track plan change outcomes:
  • subscription.active: Plan change successful, subscription updated
  • subscription.plan_changed: Subscription plan changed (upgrade/downgrade/addon update)
  • subscription.on_hold: Plan change charge failed, renewals stopped
  • payment.succeeded: Immediate charge for plan change succeeded
  • payment.failed: Immediate charge failed
Always verify webhook signatures and implement idempotent event processing.
5

Update Your Application State

Based on webhook events, update your application:
  • Grant/revoke features based on new plan
  • Update customer dashboard with new plan details
  • Send confirmation emails about plan changes
  • Log billing changes for audit purposes
6

Test and Monitor

Thoroughly test your implementation:
  • Test all proration modes with different scenarios
  • Verify webhook handling works correctly
  • Monitor plan change success rates
  • Set up alerts for failed plan changes
Your subscription plan change implementation is now ready for production use.

Preview Plan Changes

Before committing to a plan change, use the Preview API to show customers exactly what they’ll be charged:
Use the preview API to build confirmation dialogs that show customers the exact amount they’ll be charged before they confirm a plan change.

Change Plan API

Use the Change Plan API to modify product, quantity, and proration behavior for an active subscription.

Quick start examples

A successful plan change returns 200 OK immediately — before any charge has actually settled. What the body (ChangePlanResponse) contains depends on how the change was collected:
In every case, this response is not a payment result — only that the request itself was accepted. It says nothing about whether an immediate charge actually succeeded.For an ordinary immediate charge, that outcome resolves off-session, right after the call.For a collect_via_payment_link request, it resolves later and asynchronously — the response only hands you a checkout link, the subscription stays on its current plan, and nothing is known about the outcome until the customer actually completes payment on that link.Either way, don’t infer the outcome from this response. Confirm it via webhook (payment.succeeded, payment.failed, subscription.plan_changed) or by re-reading the subscription with GET /subscriptions/{subscription_id} — see What Happens While the Link Is Unpaid for the payment-link case specifically.
If the immediate charge fails, the subscription may move to subscription.on_hold until payment succeeds.
By default, an immediate plan change charges the subscription’s saved payment method directly. Set collect_via_payment_link: true to send the customer to a hosted checkout page instead — useful when there’s no saved payment method you’re allowed to charge off-session, or when you want the customer to actively confirm the new price.
This is also what powers the Collect Plan Change Payments by Payment Link toggle in Settings → Subscriptions, which routes the built-in Customer Portal’s plan-change flow through checkout instead of the saved card.

Requirements

collect_via_payment_link: true only succeeds when all of the following hold — otherwise the request fails with 422:
  • The business has the allow_plan_change_via_payment_link capability enabled (Settings → Subscriptions → Collect Plan Change Payments by Payment Link).
  • effective_at is immediately (the default). A scheduled change (next_billing_date) never needs a checkout page, since nothing is charged until it applies.
  • The effective on_payment_failure resolves to prevent_change. You don’t have to send it explicitly — if your business-level default (see Business & Collection Defaults below) is already prevent_change, omitting the field satisfies this too. An explicit apply_change, or a resolved default of apply_change, fails with 422.
collect_via_payment_link isn’t limited to upgrades — it applies to any immediate change that results in a charge, downgrades included, as long as the requirements above are met.
If the change comes out to zero or a creditproration_billing_mode: do_not_bill, or another mode that happens to net to nothing this cycle — there’s nothing to put on a checkout page. No payment link is issued, payment_link and friends come back null, and the change applies immediately, the same as it would without collect_via_payment_link. This isn’t a 422; the flag only takes effect when there’s a positive amount to collect. If you set collect_via_payment_link on plan changes generically rather than on clear-cut upgrades, call Preview Plan Change first and only request a link when the previewed amount is worth collecting.
A successful request returns the checkout handles:
  • The subscription stays on its current plan — product_id, recurring_pre_tax_amount, and next_billing_date are all untouched until the link is paid.
  • A further change-plan request on the same subscription is rejected with 409 PendingPlanChangeExists while the link is pending. Cancel a scheduled change with DELETE /subscriptions/{subscription_id}/change-plan/scheduled if needed, but that endpoint doesn’t cancel a pending payment-link change — only a successful payment or expiry does.
  • The customer can retry a card on the same checkout session after a decline; a fresh change-plan call is not the retry path.
  • If the link is never paid, it stops working after expires_on — the subscription automatically becomes free to accept a new plan-change request shortly after.
  • If a scheduled change (next_billing_date) already existed and you replace it with cancel_scheduled_change_plan: true, the original schedule stays in place while the link is unpaid, and is only cancelled once the link is paid — in the same transaction that applies the new plan.
Once an immediate payment-link change is issued, every further plan-change request on that subscription — including the side-effect-free preview — is blocked until the link resolves. Don’t issue a link you don’t intend to have the customer pay right away.

Managing Addons

When changing subscription plans, you can also modify addons:
Addons are included in the proration calculation and will be charged according to the selected proration mode.

Applying Discount Codes

You can apply one or more stacked discount codes when changing subscription plans (max 20, applied in array order). This is useful for offering promotional pricing on upgrades or migrations.

Discount behavior on plan change

The singular discount_code field on this endpoint is deprecated but still works for backward compatibility — existing integrations don’t need to change immediately. It cannot be combined with discount_codes in the same request. Migrate to the array form when convenient.
Use the Preview Plan Change API with discount_codes to show customers exactly how much they’ll save before confirming the plan change.

Proration modes

Choose how to bill the customer when changing plans:

prorated_immediately

  • Charges for the partial difference in the current cycle
  • If in trial, charges immediately and switches to the new plan now
  • Downgrade: may generate a prorated credit applied to future renewals

full_immediately

  • Charges the full amount of the new plan immediately
  • Ignores remaining time from the old plan
Credits created by downgrades using difference_immediately are subscription-scoped and distinct from Credit-Based Billing entitlements. They automatically apply to future renewals of the same subscription and are not transferable between subscriptions.

difference_immediately

  • Upgrade: immediately charge the price difference between old and new plans
  • Downgrade: add remaining value as internal credit to the subscription and auto-apply on renewals

do_not_bill

  • No charges or credits are calculated
  • Customer switches to the new plan immediately without any billing adjustment
  • Billing cycle remains unchanged
  • Best for courtesy migrations, free plan switches, or absorbing cost differences

Example scenarios

Use these canonical numbers consistently:
  • Current plan: Basic at $30/month
  • Upgrade target: Pro at $80/month
  • Downgrade target (from Pro): Starter at $20/month
  • Billing cycle: 30 days, started on January 1
  • Plan change happens on January 16 (15 days remaining, 15 days used)

How each mode processes billing

Pick prorated_immediately for fair-time accounting; choose full_immediately to restart billing; use difference_immediately for simple upgrades and automatic credit on downgrades; or use do_not_bill to switch plans without any billing adjustment.

Handling Payment Failures

Control what happens when a plan change payment fails using the on_payment_failure parameter.

Payment Failure Modes

If not specified, the on_payment_failure parameter uses your business-level default setting configured in the dashboard.

When to Use Each Mode

Business & Collection Defaults

Instead of passing proration parameters on every plan change, you can set default upgrade & downgrade behavior once at the business level. These defaults apply to all customer-portal plan changes, and can be overridden per product collection. There are separate defaults for upgrades and downgrades: Configure business defaults under Settings → Subscriptions, and collection overrides on each product collection. Each collection field is independent — leave it unset to inherit from the business default, or set a value to override it for that collection only.

Resolution order

For any given plan change, each setting resolves in this order:
A value passed explicitly to the Change Plan API always wins. The business and collection defaults only take effect when no explicit value is supplied — which is the case for all plan changes initiated from the customer portal.
A common setup: keep upgrades immediately + difference_immediately so customers pay the difference and get access right away, and keep downgrades on next_billing_date so customers keep their current plan until the cycle ends.

Handling webhooks

Track subscription state through webhooks to confirm plan changes and payments.

Event types to handle

  • subscription.active: subscription activated
  • subscription.plan_changed: subscription plan changed (upgrade/downgrade/addon changes)
  • subscription.on_hold: charge failed, renewals stopped
  • subscription.renewed: renewal succeeded
  • payment.succeeded: payment for plan change or renewal succeeded
  • payment.failed: payment failed
We recommend driving business logic from subscription events and using payment events for confirmation and reconciliation.

Verify signatures and handle intents

For detailed payload schemas, see the Subscription webhook payloads and Payment webhook payloads.

Best Practices

Follow these recommendations for reliable subscription plan changes:

Plan Change Strategy

  • Test thoroughly: Always test plan changes in test mode before production
  • Choose proration carefully: Select the proration mode that aligns with your business model
  • Handle failures gracefully: Implement proper error handling and retry logic
  • Monitor success rates: Track plan change success/failure rates and investigate issues

Webhook Implementation

  • Verify signatures: Always validate webhook signatures to ensure authenticity
  • Implement idempotency: Handle duplicate webhook events gracefully
  • Process asynchronously: Don’t block webhook responses with heavy operations
  • Log everything: Maintain detailed logs for debugging and audit purposes

User Experience

  • Communicate clearly: Inform customers about billing changes and timing
  • Provide confirmations: Send email confirmations for successful plan changes
  • Handle edge cases: Consider trial periods, prorations, and failed payments
  • Update UI immediately: Reflect plan changes in your application interface

Common Issues and Solutions

Resolve typical problems encountered during subscription plan changes:
Symptoms: API call succeeds but subscription remains on old planCommon causes:
  • Webhook processing failed or was delayed
  • Application state not updated after receiving webhooks
  • Database transaction issues during state update
Solutions:
  • Implement robust webhook handling with retry logic
  • Use idempotent operations for state updates
  • Add monitoring to detect and alert on missed webhook events
  • Verify webhook endpoint is accessible and responding correctly
Symptoms: Customer downgrades but doesn’t see credit balanceCommon causes:
  • Proration mode expectations: downgrades credit the full plan price difference with difference_immediately, while prorated_immediately creates a prorated credit based on remaining time in the cycle
  • Credits are subscription-specific and don’t transfer between subscriptions
  • Credit balance not visible in customer dashboard
Solutions:
  • Use difference_immediately for downgrades when you want automatic credits
  • Explain to customers that credits apply to future renewals of the same subscription
  • Implement customer portal to show credit balances
  • Check next invoice preview to see applied credits
Symptoms: Webhook events rejected due to invalid signatureCommon causes:
  • Incorrect webhook secret key
  • Raw request body modified before signature verification
  • Wrong signature verification algorithm
Solutions:
  • Verify you’re using the correct DODO_WEBHOOK_SECRET from dashboard
  • Read raw request body before any JSON parsing middleware
  • Use the standard webhook verification library for your platform
  • Test webhook signature verification in development environment
Symptoms: API returns 422 Unprocessable Entity errorCommon causes:
  • Invalid subscription ID or product ID
  • Subscription not in active state
  • Missing required parameters
  • Product not available for plan changes
Solutions:
  • Verify subscription exists and is active
  • Check product ID is valid and available
  • Ensure all required parameters are provided
  • Review API documentation for parameter requirements
Symptoms: Plan change initiated but immediate charge failsCommon causes:
  • Insufficient funds on customer’s payment method
  • Payment method expired or invalid
  • Bank declined the transaction
  • Fraud detection blocked the charge
Solutions:
  • Handle payment.failed webhook events appropriately
  • Notify customer to update payment method
  • Implement retry logic for temporary failures
  • Consider allowing plan changes with failed immediate charges
Symptoms: Plan change charge fails and subscription moves to on_hold stateWhat happens: When a plan change charge fails, the subscription is automatically placed in on_hold state. The subscription will not renew automatically until the payment method is updated.Solution: Update the payment method to reactivate the subscriptionTo reactivate a subscription from on_hold state after a failed plan change:
  1. Update the payment method using the Update Payment Method API
  2. Automatic charge creation: The API automatically creates a charge for remaining dues
  3. Invoice generation: An invoice is generated for the charge
  4. Payment processing: The payment is processed using the new payment method
  5. Reactivation: Upon successful payment, the subscription is reactivated to active state
Webhook events to monitor:
  • subscription.on_hold: Subscription placed on hold (received when plan change charge fails)
  • payment.succeeded: Payment for remaining dues succeeded (after updating payment method)
  • subscription.active: Subscription reactivated after successful payment
Best practices:
  • Notify customers immediately when a plan change charge fails
  • Provide clear instructions on how to update their payment method
  • Monitor webhook events to track reactivation status
  • Consider implementing automatic retry logic for temporary payment failures

Update Payment Method API Reference

View the complete API documentation for updating payment methods and reactivating subscriptions.

Testing Your Implementation

Follow these steps to thoroughly test your subscription plan change implementation:
1

Set up test environment

  • Use test API keys and test products
  • Create test subscriptions with different plan types
  • Configure test webhook endpoint
  • Set up monitoring and logging
2

Test different proration modes

  • Test prorated_immediately with various billing cycle positions
  • Test difference_immediately for upgrades and downgrades
  • Test full_immediately to reset billing cycles
  • Test do_not_bill for no-charge/no-credit plan switches
  • Verify credit calculations are correct
3

Test webhook handling

  • Verify all relevant webhook events are received
  • Test webhook signature verification
  • Handle duplicate webhook events gracefully
  • Test webhook processing failure scenarios
4

Test error scenarios

  • Test with invalid subscription IDs
  • Test with expired payment methods
  • Test network failures and timeouts
  • Test with insufficient funds
5

Monitor in production

  • Set up alerts for failed plan changes
  • Monitor webhook processing times
  • Track plan change success rates
  • Review customer support tickets for plan change issues

Error Handling

Handle common API errors gracefully in your implementation:

HTTP Status Codes

Plan change request processed successfully. The response body is empty, except for a successful collect_via_payment_link request, which returns checkout handles — see Collecting Payment via a Checkout Link. If on_payment_failure=prevent_change, the plan change stays pending until payment succeeds.
Invalid request parameters. Check that all required fields are provided and properly formatted.
Invalid or missing API key. Verify your DODO_PAYMENTS_API_KEY is correct and has proper permissions.
Subscription ID not found or doesn’t belong to your account.
A pending plan change already exists for this subscription (PendingPlanChangeExists). For a scheduled change, cancel it with DELETE /subscriptions/{subscription_id}/change-plan/scheduled before submitting a new one. For a pending payment-link change, there’s no cancel endpoint — the subscription accepts a new plan-change request once the customer pays or the link expires.
The subscription is inactive or on-demand, or the request is not eligible for collect_via_payment_link — the business doesn’t have the capability enabled, effective_at isn’t immediately, or on_payment_failure isn’t prevent_change. See Requirements.
Server error occurred. Retry the request after a brief delay.

Error Response Format

Next steps

Last modified on August 26, 2026