/stripe-guides

How to upgrade or downgrade a subscription via Stripe API?

Learn how to upgrade or downgrade a customer’s subscription using the Stripe API, with step-by-step code examples, proration, scheduling, and webhook handling.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to upgrade or downgrade a subscription via Stripe API?

How to Upgrade or Downgrade a Subscription via Stripe API

 

In this tutorial, I'll walk you through the process of upgrading or downgrading a customer's subscription using the Stripe API. This is a common requirement for SaaS businesses that offer multiple pricing tiers.

 

Step 1: Set Up Your Stripe Environment

 

First, you need to set up your Stripe API environment by installing the Stripe library for your programming language. Here's how to do it in several popular languages:

For Node.js:

npm install stripe

For Python:

pip install stripe

For PHP:

composer require stripe/stripe-php

For Ruby:

gem install stripe

 

Step 2: Initialize the Stripe Client

 

After installing the library, initialize the Stripe client with your API key:

Node.js:

const stripe = require('stripe')('sk_test_your_stripe_secret\_key');

Python:

import stripe
stripe.api_key = 'sk_test_your_stripe_secret_key'

PHP:

\Stripe\Stripe::setApiKey('sk_test_your_stripe_secret\_key');

Ruby:

require 'stripe'
Stripe.api_key = 'sk_test_your_stripe_secret_key'

 

Step 3: Retrieve the Current Subscription

 

Before modifying a subscription, you need to retrieve the current subscription details:

Node.js:

async function getSubscription(subscriptionId) {
  try {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    return subscription;
  } catch (error) {
    console.error('Error retrieving subscription:', error);
    throw error;
  }
}

Python:

def get_subscription(subscription_id):
    try:
        subscription = stripe.Subscription.retrieve(subscription\_id)
        return subscription
    except Exception as e:
        print(f"Error retrieving subscription: {e}")
        raise

 

Step 4: Determine the Upgrade/Downgrade Strategy

 

Stripe offers different approaches for handling subscription changes:

  • Immediate update: Switch the plan immediately and prorate charges
  • Scheduled update: Schedule the change for the next billing cycle
  • Create a new subscription and cancel the old one

We'll cover the first two approaches as they are most common.

 

Step 5: Immediate Upgrade/Downgrade with Proration

 

To immediately change a subscription and prorate the charges:

Node.js:

async function updateSubscription(subscriptionId, newPriceId) {
  try {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    
    // Get the current subscription item ID
    const subscriptionItemId = subscription.items.data[0].id;
    
    // Update the subscription with the new price
    const updatedSubscription = await stripe.subscriptions.update(
      subscriptionId,
      {
        items: [
          {
            id: subscriptionItemId,
            price: newPriceId,
          },
        ],
        proration_behavior: 'create_prorations', // This creates prorated credits/charges
      }
    );
    
    return updatedSubscription;
  } catch (error) {
    console.error('Error updating subscription:', error);
    throw error;
  }
}

Python:

def update_subscription(subscription_id, new_price_id):
    try:
        subscription = stripe.Subscription.retrieve(subscription\_id)
        
        # Get the current subscription item ID
        subscription_item_id = subscription\['items']\['data']\[0]\['id']
        
        # Update the subscription with the new price
        updated\_subscription = stripe.Subscription.modify(
            subscription\_id,
            items=[{
                'id': subscription_item_id,
                'price': new_price_id,
            }],
            proration_behavior='create_prorations',  # This creates prorated credits/charges
        )
        
        return updated\_subscription
    except Exception as e:
        print(f"Error updating subscription: {e}")
        raise

 

Step 6: Schedule Upgrade/Downgrade for Next Billing Cycle

 

If you want to schedule the change for the next billing cycle (no proration):

Node.js:

async function scheduleSubscriptionUpdate(subscriptionId, newPriceId) {
  try {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    
    // Get the current subscription item ID
    const subscriptionItemId = subscription.items.data[0].id;
    
    // Update the subscription with the new price at the end of the current period
    const updatedSubscription = await stripe.subscriptions.update(
      subscriptionId,
      {
        items: [
          {
            id: subscriptionItemId,
            price: newPriceId,
          },
        ],
        proration\_behavior: 'none', // Don't prorate
        billing_cycle_anchor: 'unchanged', // Keep the billing cycle the same
      }
    );
    
    return updatedSubscription;
  } catch (error) {
    console.error('Error scheduling subscription update:', error);
    throw error;
  }
}

Python:

def schedule_subscription_update(subscription_id, new_price\_id):
    try:
        subscription = stripe.Subscription.retrieve(subscription\_id)
        
        # Get the current subscription item ID
        subscription_item_id = subscription\['items']\['data']\[0]\['id']
        
        # Update the subscription with the new price at the end of the current period
        updated\_subscription = stripe.Subscription.modify(
            subscription\_id,
            items=[{
                'id': subscription_item_id,
                'price': new_price_id,
            }],
            proration\_behavior='none',  # Don't prorate
            billing_cycle_anchor='unchanged',  # Keep the billing cycle the same
        )
        
        return updated\_subscription
    except Exception as e:
        print(f"Error scheduling subscription update: {e}")
        raise

 

Step 7: Handle Multi-Item Subscriptions

 

If your subscription has multiple items (e.g., base plan plus add-ons), you need to handle all items:

Node.js:

async function updateMultiItemSubscription(subscriptionId, updatedItems) {
  try {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    
    // Prepare items array for the update
    const items = updatedItems.map(item => ({
      id: item.subscriptionItemId,
      price: item.newPriceId,
      // Optional: update quantity if needed
      quantity: item.quantity || 1,
    }));
    
    // Update the subscription with multiple items
    const updatedSubscription = await stripe.subscriptions.update(
      subscriptionId,
      {
        items: items,
        proration_behavior: 'create_prorations',
      }
    );
    
    return updatedSubscription;
  } catch (error) {
    console.error('Error updating multi-item subscription:', error);
    throw error;
  }
}

Python:

def update_multi_item_subscription(subscription_id, updated\_items):
    try:
        # Prepare items array for the update
        items = [
            {
                'id': item['subscription_item_id'],
                'price': item['new_price_id'],
                # Optional: update quantity if needed
                'quantity': item.get('quantity', 1)
            } 
            for item in updated\_items
        ]
        
        # Update the subscription with multiple items
        updated\_subscription = stripe.Subscription.modify(
            subscription\_id,
            items=items,
            proration_behavior='create_prorations',
        )
        
        return updated\_subscription
    except Exception as e:
        print(f"Error updating multi-item subscription: {e}")
        raise

 

Step 8: Implement Quantity Changes

 

Sometimes you need to change the quantity of a subscription item rather than the price itself:

Node.js:

async function updateSubscriptionQuantity(subscriptionId, quantity) {
  try {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    
    // Get the current subscription item ID
    const subscriptionItemId = subscription.items.data[0].id;
    
    // Update the subscription quantity
    const updatedSubscription = await stripe.subscriptions.update(
      subscriptionId,
      {
        items: [
          {
            id: subscriptionItemId,
            quantity: quantity,
          },
        ],
        proration_behavior: 'create_prorations',
      }
    );
    
    return updatedSubscription;
  } catch (error) {
    console.error('Error updating subscription quantity:', error);
    throw error;
  }
}

Python:

def update_subscription_quantity(subscription\_id, quantity):
    try:
        subscription = stripe.Subscription.retrieve(subscription\_id)
        
        # Get the current subscription item ID
        subscription_item_id = subscription\['items']\['data']\[0]\['id']
        
        # Update the subscription quantity
        updated\_subscription = stripe.Subscription.modify(
            subscription\_id,
            items=[{
                'id': subscription_item_id,
                'quantity': quantity,
            }],
            proration_behavior='create_prorations',
        )
        
        return updated\_subscription
    except Exception as e:
        print(f"Error updating subscription quantity: {e}")
        raise

 

Step 9: Handle Subscription Upgrade/Downgrade in a Web Application

 

Here's a complete example of handling a subscription upgrade in a Node.js Express application:

const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')('sk_test_your_stripe_secret\_key');

const app = express();
app.use(bodyParser.json());

// Endpoint to handle subscription upgrades/downgrades
app.post('/api/subscription/change', async (req, res) => {
  try {
    const { customerId, subscriptionId, newPriceId, immediateChange } = req.body;
    
    // Verify the customer owns this subscription
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    if (subscription.customer !== customerId) {
      return res.status(403).json({ error: 'Unauthorized access to subscription' });
    }
    
    // Get the current subscription item ID
    const subscriptionItemId = subscription.items.data[0].id;
    
    // Determine if we should change immediately or at period end
    const updateParams = {
      items: [
        {
          id: subscriptionItemId,
          price: newPriceId,
        },
      ],
    };
    
    // If immediate change is requested, prorate the charges
    if (immediateChange) {
      updateParams.proration_behavior = 'create_prorations';
    } else {
      // Otherwise, change at period end (no proration)
      updateParams.proration\_behavior = 'none';
      updateParams.billing_cycle_anchor = 'unchanged';
    }
    
    // Update the subscription
    const updatedSubscription = await stripe.subscriptions.update(
      subscriptionId,
      updateParams
    );
    
    // Return the updated subscription to the client
    res.json({
      success: true,
      subscription: updatedSubscription,
    });
  } catch (error) {
    console.error('Error changing subscription:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

 

Step 10: Handle Webhooks for Subscription Updates

 

It's important to listen for Stripe webhooks to handle subscription update events:

app.post('/webhook', bodyParser.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    // Verify the webhook signature
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      'whsec_your_webhook_signing_secret'
    );
  } catch (err) {
    console.log(`Webhook Error: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle subscription update events
  switch (event.type) {
    case 'customer.subscription.updated':
      const subscription = event.data.object;
      console.log(`Subscription ${subscription.id} was updated!`);
      
      // Update your database or notify relevant systems
      await updateSubscriptionInDatabase(subscription);
      break;
      
    case 'invoice.payment\_succeeded':
      const invoice = event.data.object;
      // Handle successful payment for the subscription change
      if (invoice.billing_reason === 'subscription_update') {
        console.log(`Payment succeeded for subscription update: ${invoice.subscription}`);
        // Notify the customer or update your systems
      }
      break;
      
    case 'invoice.payment\_failed':
      const failedInvoice = event.data.object;
      // Handle failed payment for the subscription change
      if (failedInvoice.billing_reason === 'subscription_update') {
        console.log(`Payment failed for subscription update: ${failedInvoice.subscription}`);
        // Notify the customer or take remedial action
      }
      break;
  }

  // Return a 200 response to acknowledge receipt of the event
  res.json({received: true});
});

 

Step 11: Test Your Implementation

 

Before deploying to production, test your implementation thoroughly:

  • Create test subscriptions in Stripe's test mode
  • Test upgrading to a higher-priced plan
  • Test downgrading to a lower-priced plan
  • Test immediate changes with proration
  • Test changes scheduled for the next billing cycle
  • Verify webhook handling for subscription update events

You can use Stripe's CLI to test webhooks locally:

stripe listen --forward-to localhost:3000/webhook

 

Step 12: Handle Edge Cases and Errors

 

Make sure your implementation handles these common edge cases:

  • Past-due subscriptions
  • Canceled subscriptions
  • Failed payments during upgrades
  • Rate limits from the Stripe API
  • Network errors and retries

Here's an example of more robust error handling:

async function robustSubscriptionUpdate(subscriptionId, newPriceId, maxRetries = 3) {
  let retryCount = 0;
  
  while (retryCount < maxRetries) {
    try {
      const subscription = await stripe.subscriptions.retrieve(subscriptionId);
      
      // Check if subscription is in a valid state for updates
      if (subscription.status === 'canceled') {
        throw new Error('Cannot update a canceled subscription');
      }
      
      // Handle past due subscriptions
      if (subscription.status === 'past\_due') {
        console.warn('Updating a past\_due subscription. This may result in immediate charge attempts.');
      }
      
      const subscriptionItemId = subscription.items.data[0].id;
      
      const updatedSubscription = await stripe.subscriptions.update(
        subscriptionId,
        {
          items: [
            {
              id: subscriptionItemId,
              price: newPriceId,
            },
          ],
          proration_behavior: 'create_prorations',
        }
      );
      
      return updatedSubscription;
    } catch (error) {
      retryCount++;
      
      // Check if we should retry
      if (error.type === 'StripeRateLimitError' && retryCount < maxRetries) {
        // Exponential backoff: wait longer between each retry
        const sleepTime = Math.pow(2, retryCount) \* 1000;
        console.log(`Rate limited. Retrying in ${sleepTime}ms...`);
        await new Promise(resolve => setTimeout(resolve, sleepTime));
      } else if (retryCount >= maxRetries) {
        console.error(`Failed to update subscription after ${maxRetries} attempts:`, error);
        throw error;
      } else {
        // Non-retriable error
        console.error('Error updating subscription:', error);
        throw error;
      }
    }
  }
}

 

Conclusion

 

You now have a comprehensive understanding of how to upgrade or downgrade subscriptions using the Stripe API. Remember to always test thoroughly in Stripe's test mode before implementing in production, and make sure to handle webhooks to keep your system in sync with Stripe's subscription events.

By following these steps, you'll be able to provide a seamless subscription management experience for your customers while ensuring that billing is handled correctly.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022