Skip to main content

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

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:
  • prorated_immediately
  • difference_immediately
  • full_immediately
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
required
The ID of the active subscription to modify.
product_id
string
required
The new product ID to change the subscription to.
quantity
integer
default:"1"
Number of units for the new plan (for seat-based products).
proration_billing_mode
string
required
How to handle immediate billing: prorated_immediately, full_immediately, or difference_immediately.
addons
array
Optional addons for the new plan. Leaving this empty removes any existing addons.
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, subscription paused
  • 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.

Change Plan API

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

Quick start examples

  • Node.js SDK
  • Python SDK
  • Go SDK
  • HTTP
import DodoPayments from 'dodopayments';

const client = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
});

async function changePlan() {
  const result = await client.subscriptions.changePlan('sub_123', {
    product_id: 'prod_new',
    quantity: 3,
    proration_billing_mode: 'prorated_immediately',
  });
  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"
}
Fields like invoice_id and payment_id are returned only when an immediate charge and/or invoice is created during the plan change. Always rely on webhook events (e.g., payment.succeeded, subscription.plan_changed) to confirm outcomes.
If the immediate charge fails, the subscription may move to subscription.on_hold until payment succeeds.

Managing Addons

When changing subscription plans, you can also modify addons:
// 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
});
Addons are included in the proration calculation and will be charged according to the selected proration mode.

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 Customer Credits. 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

Example scenarios

await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_pro',
  quantity: 1,
  proration_billing_mode: 'difference_immediately'
})
// Immediate charge: $50
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_starter',
  quantity: 1,
  proration_billing_mode: 'difference_immediately'
})
// Credit added: $30 (auto-applied to future renewals for this subscription)
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_new',
  quantity: 1,
  proration_billing_mode: 'prorated_immediately'
})
// Immediate prorated charge based on remaining days in cycle
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_new',
  quantity: 1,
  proration_billing_mode: 'full_immediately'
})
// Immediate full charge for new plan; no credits calculated
Pick prorated_immediately for fair-time accounting; choose full_immediately to restart billing; use difference_immediately for simple upgrades and automatic credit on downgrades.

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, subscription paused
  • 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

  • Next.js Route Handler
  • Express.js
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 });
}
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

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
  • 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 subscription is being updated and payment processing has begun.
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.
The subscription cannot be changed (e.g., already cancelled, product not available, etc.).
Server error occurred. Retry the request after a brief delay.

Error Response Format

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

Next steps

I