Upgrade or downgrade a Stripe subscription by updating the subscription item's price via stripe.subscriptions.update(). Stripe calculates proration automatically — crediting unused time on the old plan and charging the difference for the new plan. Use invoices.retrieveUpcoming() to preview charges before confirming the change.
Switching Subscription Plans with Stripe Proration
Customers frequently want to upgrade or downgrade their plan. Stripe makes this straightforward: update the subscription item with a new Price ID, and Stripe handles proration automatically. For upgrades, the customer is charged the prorated difference. For downgrades, they receive a credit applied to future invoices. You can preview the proration before making the change, and control the behavior with the proration_behavior parameter.
Prerequisites
- An active Stripe subscription with at least one item
- Multiple Price IDs for different plans (e.g., price_basic, price_pro)
- Node.js 18+ with the stripe npm package
- Your Stripe secret key in environment variables
Step-by-step guide
Retrieve the subscription and its current item
Retrieve the subscription and its current item
Get the subscription to find the item ID (si_xxx) you need to update. Each subscription item has a unique ID separate from the subscription ID.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23const subscription = await stripe.subscriptions.retrieve('sub_xxx');4const currentItem = subscription.items.data[0];56console.log('Item ID:', currentItem.id); // si_xxx7console.log('Current Price:', currentItem.price.id); // price_basic8console.log('Amount:', currentItem.price.unit_amount); // 1000 ($10/mo)Expected result: You have the subscription item ID and current plan details.
Preview the proration
Preview the proration
Before changing the plan, show the customer what they will be charged or credited. Use invoices.retrieveUpcoming() to simulate the change.
1const preview = await stripe.invoices.retrieveUpcoming({2 customer: subscription.customer,3 subscription: subscription.id,4 subscription_items: [5 {6 id: currentItem.id,7 price: 'price_pro', // New plan Price ID8 },9 ],10});1112// Calculate net proration13const prorationLines = preview.lines.data.filter((line) => line.proration);14const prorationTotal = prorationLines.reduce((sum, line) => sum + line.amount, 0);1516console.log('Proration amount:', prorationTotal); // Positive = charge, Negative = credit17console.log('Next invoice total:', preview.amount_due);Expected result: The preview shows the prorated charge or credit and the next invoice total.
Apply the plan change
Apply the plan change
Update the subscription with the new Price ID. Pass the existing item ID to replace the plan (not add a new one).
1const updated = await stripe.subscriptions.update('sub_xxx', {2 items: [3 {4 id: currentItem.id, // MUST include the item ID to replace5 price: 'price_pro', // New plan6 },7 ],8 proration_behavior: 'create_prorations', // Default: add to next invoice9});1011console.log('New plan:', updated.items.data[0].price.id);12console.log('Status:', updated.status);Expected result: The subscription item is updated to the new plan. Prorated charges or credits are added to the next invoice.
Control proration behavior
Control proration behavior
Choose how proration is handled using the proration_behavior parameter. Different strategies suit upgrades vs downgrades.
1// create_prorations (default): Prorated charges/credits added to next invoice2await stripe.subscriptions.update('sub_xxx', {3 items: [{ id: itemId, price: 'price_pro' }],4 proration_behavior: 'create_prorations',5});67// always_invoice: Invoice and charge immediately for upgrades8await stripe.subscriptions.update('sub_xxx', {9 items: [{ id: itemId, price: 'price_pro' }],10 proration_behavior: 'always_invoice',11});1213// none: No proration — new price starts at next billing cycle14await stripe.subscriptions.update('sub_xxx', {15 items: [{ id: itemId, price: 'price_basic' }],16 proration_behavior: 'none',17});Expected result: Proration is applied according to the chosen behavior.
Test plan changes
Test plan changes
Create a test subscription on the Basic plan and upgrade to Pro. Check the Stripe Dashboard to verify proration.
1// 1. Create a subscription on price_basic ($10/mo)2// 2. After 15 days, upgrade to price_pro ($25/mo)3// Expected proration:4// Credit: $5.00 (15 unused days of $10 plan)5// Charge: $12.50 (15 days of $25 plan)6// Net charge: $7.5078// Use test card: 4242 4242 4242 42429// Check Dashboard → Subscriptions → [sub] → InvoicesExpected result: The proration appears as line items on the invoice. Net charge reflects the plan difference.
Complete working example
1const express = require('express');2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);34const app = express();5app.use(express.json());67// Preview plan change8app.post('/api/preview-plan-change', async (req, res) => {9 const { subscriptionId, newPriceId } = req.body;1011 try {12 const subscription = await stripe.subscriptions.retrieve(subscriptionId);13 const itemId = subscription.items.data[0].id;1415 const preview = await stripe.invoices.retrieveUpcoming({16 customer: subscription.customer,17 subscription: subscriptionId,18 subscription_items: [{ id: itemId, price: newPriceId }],19 });2021 const prorationLines = preview.lines.data.filter((l) => l.proration);22 const prorationTotal = prorationLines.reduce((s, l) => s + l.amount, 0);2324 res.json({25 proration_amount: prorationTotal,26 next_invoice_total: preview.amount_due,27 currency: preview.currency,28 proration_details: prorationLines.map((l) => ({29 description: l.description,30 amount: l.amount,31 })),32 });33 } catch (err) {34 res.status(400).json({ error: err.message });35 }36});3738// Apply plan change39app.post('/api/change-plan', async (req, res) => {40 const { subscriptionId, newPriceId, chargeImmediately = false } = req.body;4142 try {43 const subscription = await stripe.subscriptions.retrieve(subscriptionId);44 const itemId = subscription.items.data[0].id;4546 const updated = await stripe.subscriptions.update(subscriptionId, {47 items: [{ id: itemId, price: newPriceId }],48 proration_behavior: chargeImmediately49 ? 'always_invoice'50 : 'create_prorations',51 });5253 res.json({54 subscriptionId: updated.id,55 newPrice: updated.items.data[0].price.id,56 newAmount: updated.items.data[0].price.unit_amount,57 status: updated.status,58 });59 } catch (err) {60 res.status(400).json({ error: err.message });61 }62});6364app.listen(4000, () => console.log('Server on port 4000'));Common mistakes when upgrading or downgrade a subscription in Stripe
Why it's a problem: Not passing the subscription item ID when updating
How to avoid: Always include the existing item's id (si_xxx) in the items array. Without it, Stripe adds a new item instead of replacing, billing the customer for both plans.
Why it's a problem: Not previewing the proration before applying the change
How to avoid: Always show the customer the proration amount using invoices.retrieveUpcoming() before confirming. Unexpected charges lead to customer complaints.
Why it's a problem: Using proration_behavior: 'none' for upgrades
How to avoid: With 'none', the customer gets upgraded access without paying the difference until the next billing cycle. Use 'create_prorations' or 'always_invoice' for upgrades.
Why it's a problem: Not handling the payment failure on immediate proration invoices
How to avoid: When using 'always_invoice', the prorated charge may fail (card declined, 3DS required). Listen for invoice.payment_failed and handle accordingly.
Best practices
- Always preview proration with invoices.retrieveUpcoming() before making changes
- Include the existing subscription item ID (si_xxx) when updating to replace (not add) the plan
- Use 'always_invoice' for upgrades to charge the difference immediately
- Use 'create_prorations' for downgrades to credit the next invoice
- Show a clear proration breakdown to the customer before confirming
- Listen for customer.subscription.updated webhook to sync plan changes in your database
- Test with test clocks to simulate mid-cycle plan changes and verify proration math
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write Node.js Express endpoints for Stripe subscription plan changes. Include: 1) A preview endpoint using invoices.retrieveUpcoming() that returns proration details. 2) A change-plan endpoint that updates the subscription item with a new price and configurable proration_behavior.
Add plan upgrade/downgrade to my app. Create: 1) An endpoint to preview what the customer will pay when switching plans (using Stripe upcoming invoice). 2) An endpoint to apply the change with proper proration. Use the subscription item ID to replace the current plan.
Frequently asked questions
How is proration calculated?
Stripe calculates based on unused time. If a customer is 15 days into a 30-day cycle on a $30 plan and upgrades to $60, they get a $15 credit (15 unused days at $30/30) and a $30 charge (15 days at $60/30). Net: $15.
Can I switch from monthly to yearly billing?
Yes. Use the yearly Price ID for the same Product. Stripe prorates the remaining monthly charge and starts the yearly cycle. Preview the invoice first — the one-time charge can be significant.
What if the prorated payment fails?
With 'always_invoice', the invoice enters 'open' status and the subscription may go to 'past_due'. Listen for invoice.payment_failed and direct the customer to update their payment method.
Can I skip proration entirely?
Yes. Set proration_behavior: 'none'. The new price takes effect immediately but the customer is not charged or credited until the next billing cycle.
Can RapidDev help implement complex plan management?
Yes. RapidDev builds subscription management systems with multi-tier pricing, add-ons, proration previews, grandfathered plans, and admin dashboards — all integrated with the Stripe API.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation