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.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
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:
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:
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:
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.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.