# How to upgrade or downgrade a subscription in Stripe

- Tool: Stripe
- Difficulty: Intermediate
- Time required: 15 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+
- Last updated: March 2026

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

## Before you start

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

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const subscription = await stripe.subscriptions.retrieve('sub_xxx');
const currentItem = subscription.items.data[0];

console.log('Item ID:', currentItem.id);           // si_xxx
console.log('Current Price:', currentItem.price.id); // price_basic
console.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.

```
const preview = await stripe.invoices.retrieveUpcoming({
  customer: subscription.customer,
  subscription: subscription.id,
  subscription_items: [
    {
      id: currentItem.id,
      price: 'price_pro', // New plan Price ID
    },
  ],
});

// Calculate net proration
const prorationLines = preview.lines.data.filter((line) => line.proration);
const prorationTotal = prorationLines.reduce((sum, line) => sum + line.amount, 0);

console.log('Proration amount:', prorationTotal); // Positive = charge, Negative = credit
console.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).

```
const updated = await stripe.subscriptions.update('sub_xxx', {
  items: [
    {
      id: currentItem.id,   // MUST include the item ID to replace
      price: 'price_pro',    // New plan
    },
  ],
  proration_behavior: 'create_prorations', // Default: add to next invoice
});

console.log('New plan:', updated.items.data[0].price.id);
console.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.

```
// create_prorations (default): Prorated charges/credits added to next invoice
await stripe.subscriptions.update('sub_xxx', {
  items: [{ id: itemId, price: 'price_pro' }],
  proration_behavior: 'create_prorations',
});

// always_invoice: Invoice and charge immediately for upgrades
await stripe.subscriptions.update('sub_xxx', {
  items: [{ id: itemId, price: 'price_pro' }],
  proration_behavior: 'always_invoice',
});

// none: No proration — new price starts at next billing cycle
await stripe.subscriptions.update('sub_xxx', {
  items: [{ id: itemId, price: 'price_basic' }],
  proration_behavior: 'none',
});
```

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

```
// 1. Create a subscription on price_basic ($10/mo)
// 2. After 15 days, upgrade to price_pro ($25/mo)
// Expected proration:
//   Credit: $5.00 (15 unused days of $10 plan)
//   Charge: $12.50 (15 days of $25 plan)
//   Net charge: $7.50

// Use test card: 4242 4242 4242 4242
// Check Dashboard → Subscriptions → [sub] → Invoices
```

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

## Complete code example

File: `plan-switch.js`

```javascript
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

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

// Preview plan change
app.post('/api/preview-plan-change', async (req, res) => {
  const { subscriptionId, newPriceId } = req.body;

  try {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    const itemId = subscription.items.data[0].id;

    const preview = await stripe.invoices.retrieveUpcoming({
      customer: subscription.customer,
      subscription: subscriptionId,
      subscription_items: [{ id: itemId, price: newPriceId }],
    });

    const prorationLines = preview.lines.data.filter((l) => l.proration);
    const prorationTotal = prorationLines.reduce((s, l) => s + l.amount, 0);

    res.json({
      proration_amount: prorationTotal,
      next_invoice_total: preview.amount_due,
      currency: preview.currency,
      proration_details: prorationLines.map((l) => ({
        description: l.description,
        amount: l.amount,
      })),
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Apply plan change
app.post('/api/change-plan', async (req, res) => {
  const { subscriptionId, newPriceId, chargeImmediately = false } = req.body;

  try {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    const itemId = subscription.items.data[0].id;

    const updated = await stripe.subscriptions.update(subscriptionId, {
      items: [{ id: itemId, price: newPriceId }],
      proration_behavior: chargeImmediately
        ? 'always_invoice'
        : 'create_prorations',
    });

    res.json({
      subscriptionId: updated.id,
      newPrice: updated.items.data[0].price.id,
      newAmount: updated.items.data[0].price.unit_amount,
      status: updated.status,
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.listen(4000, () => console.log('Server on port 4000'));
```

## Common mistakes

- **Not passing the subscription item ID when updating** — undefined Fix: 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.
- **Not previewing the proration before applying the change** — undefined Fix: Always show the customer the proration amount using invoices.retrieveUpcoming() before confirming. Unexpected charges lead to customer complaints.
- **Using proration_behavior: 'none' for upgrades** — undefined Fix: With 'none', the customer gets upgraded access without paying the difference until the next billing cycle. Use 'create_prorations' or 'always_invoice' for upgrades.
- **Not handling the payment failure on immediate proration invoices** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-upgrade-or-downgrade-a-subscription-via-stripe-api
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-upgrade-or-downgrade-a-subscription-via-stripe-api
