Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to upgrade or downgrade a subscription in Stripe

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.

What you'll learn

  • How to switch a subscription between plans via the API
  • How Stripe handles proration for upgrades and downgrades
  • How to preview proration amounts before making changes
  • How to control proration behavior with proration_behavior
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read15 minutesStripe API v2024-12+, Node.js 18+March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3const subscription = await stripe.subscriptions.retrieve('sub_xxx');
4const currentItem = subscription.items.data[0];
5
6console.log('Item ID:', currentItem.id); // si_xxx
7console.log('Current Price:', currentItem.price.id); // price_basic
8console.log('Amount:', currentItem.price.unit_amount); // 1000 ($10/mo)

Expected result: You have the subscription item ID and current plan details.

2

Preview the proration

Before changing the plan, show the customer what they will be charged or credited. Use invoices.retrieveUpcoming() to simulate the change.

typescript
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 ID
8 },
9 ],
10});
11
12// Calculate net proration
13const prorationLines = preview.lines.data.filter((line) => line.proration);
14const prorationTotal = prorationLines.reduce((sum, line) => sum + line.amount, 0);
15
16console.log('Proration amount:', prorationTotal); // Positive = charge, Negative = credit
17console.log('Next invoice total:', preview.amount_due);

Expected result: The preview shows the prorated charge or credit and the next invoice total.

3

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).

typescript
1const updated = await stripe.subscriptions.update('sub_xxx', {
2 items: [
3 {
4 id: currentItem.id, // MUST include the item ID to replace
5 price: 'price_pro', // New plan
6 },
7 ],
8 proration_behavior: 'create_prorations', // Default: add to next invoice
9});
10
11console.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.

4

Control proration behavior

Choose how proration is handled using the proration_behavior parameter. Different strategies suit upgrades vs downgrades.

typescript
1// create_prorations (default): Prorated charges/credits added to next invoice
2await stripe.subscriptions.update('sub_xxx', {
3 items: [{ id: itemId, price: 'price_pro' }],
4 proration_behavior: 'create_prorations',
5});
6
7// always_invoice: Invoice and charge immediately for upgrades
8await stripe.subscriptions.update('sub_xxx', {
9 items: [{ id: itemId, price: 'price_pro' }],
10 proration_behavior: 'always_invoice',
11});
12
13// none: No proration — new price starts at next billing cycle
14await 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.

5

Test plan changes

Create a test subscription on the Basic plan and upgrade to Pro. Check the Stripe Dashboard to verify proration.

typescript
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.50
7
8// Use test card: 4242 4242 4242 4242
9// Check Dashboard → Subscriptions → [sub] → Invoices

Expected result: The proration appears as line items on the invoice. Net charge reflects the plan difference.

Complete working example

plan-switch.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4const app = express();
5app.use(express.json());
6
7// Preview plan change
8app.post('/api/preview-plan-change', async (req, res) => {
9 const { subscriptionId, newPriceId } = req.body;
10
11 try {
12 const subscription = await stripe.subscriptions.retrieve(subscriptionId);
13 const itemId = subscription.items.data[0].id;
14
15 const preview = await stripe.invoices.retrieveUpcoming({
16 customer: subscription.customer,
17 subscription: subscriptionId,
18 subscription_items: [{ id: itemId, price: newPriceId }],
19 });
20
21 const prorationLines = preview.lines.data.filter((l) => l.proration);
22 const prorationTotal = prorationLines.reduce((s, l) => s + l.amount, 0);
23
24 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});
37
38// Apply plan change
39app.post('/api/change-plan', async (req, res) => {
40 const { subscriptionId, newPriceId, chargeImmediately = false } = req.body;
41
42 try {
43 const subscription = await stripe.subscriptions.retrieve(subscriptionId);
44 const itemId = subscription.items.data[0].id;
45
46 const updated = await stripe.subscriptions.update(subscriptionId, {
47 items: [{ id: itemId, price: newPriceId }],
48 proration_behavior: chargeImmediately
49 ? 'always_invoice'
50 : 'create_prorations',
51 });
52
53 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});
63
64app.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.

ChatGPT Prompt

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.

Stripe Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.