Skip to content
Back to Monetization & Revenue

Upgrade / Downgrade

How to handle mid-cycle plan changes with proration that is fair to the user and simple to reason about.

Why

Users need to upgrade and downgrade mid-cycle — a team grows and needs more seats, or downsizes and wants to pay less. The hard part is proration: how much to charge or credit for the unused time on the old plan. Get this wrong and you create either angry users (charged too much on upgrade) or revenue leaks (credited too generously on downgrade). Stripe handles the math for you with proration_behavior='create_prorations', but you must understand what it does so you can explain it to users and handle edge cases. The key decisions are: (1) whether to charge the proration immediately or on the next invoice, (2) whether downgrades take effect immediately or at period end, and (3) how to handle plan changes that cross a billing-cycle boundary. The user-facing rule should be simple: 'upgrades are effective immediately, downgrades take effect at the end of your current period' — this is the most common and least surprising policy.

When

When you have more than one plan. Build the upgrade/downgrade flow before launch — users will try to change plans on day one.

How

1. Decide your policy: upgrades effective immediately with proration charged now; downgrades effective at period end (no proration, no credit) — this is the Stripe default and the least surprising
2. For upgrades: call stripe.subscriptions.update with the new price_id and proration_behavior='create_prorations' — Stripe computes the unused-time credit and the new-plan proration and nets them into an immediate invoice
3. For downgrades: call stripe.subscriptions.update with the new price_id and proration_behavior='none' and billing_cycle_anchor='unchanged' — the downgrade takes effect at the next renewal, no immediate charge
4. Show the user a confirmation screen before the change: "Upgrading to Pro — you will be charged $18.50 today (prorated) and $29/mo starting next cycle" — never let a plan change happen without a clear preview
5. After the change, send a receipt email with the proration breakdown — users forward these to finance and the breakdown prevents 'why was I charged $X' tickets
6. For seat-based plans, handle seat adds/removes separately from plan changes — adding a seat mid-cycle is a proration, removing a seat is effective at period end
7. Track upgrade and downgrade funnels separately — a high downgrade rate signals a pricing problem (plans too far apart) and a high upgrade rate signals good product-market fit
8. Never auto-downgrade users who hit their plan limit — show an in-app prompt to upgrade instead; silent downgrades feel like punishment

Example

// upgrade: immediate, prorated
async function upgrade(subId: string, newPriceId: string) {
  const sub = await stripe.subscriptions.update(subId, {
    items: [{ id: (await stripe.subscriptions.retrieve(subId)).items.data[0].id, price: newPriceId }],
    proration_behavior: "create_prorations",
  });
  // Stripe creates an immediate invoice for the proration difference
  return sub;
}

// downgrade: effective at period end, no proration
async function downgrade(subId: string, newPriceId: string) {
  const sub = await stripe.subscriptions.update(subId, {
    items: [{ id: (await stripe.subscriptions.retrieve(subId)).items.data[0].id, price: newPriceId }],
    proration_behavior: "none",
    billing_cycle_anchor: "unchanged",
  });
  return sub;
}

// preview the proration before charging (Stripe Invoice Upcoming Preview)
const preview = await stripe.invoices.retrieveUpcoming({
  subscription: subId,
  subscription_items: [{ id: itemId, price: newPriceId }],
  proration_behavior: "create_prorations",
});
// preview.total_prorations_inclusive_cents → show to user as "you will be charged $X today"

Common Problems

  • ⚠Charging the full new-plan price on upgrade instead of prorating — the user on day 29 of a $29 plan upgrades to $99 and is charged $99, effectively paying $128 for the month. Always prorate.
  • ⚠Making downgrades effective immediately with a credit — the user downgrades, gets a $18 credit, and then forgets about it; next month the credit is still there and finance is confused. Downgrade at period end.
  • ⚠Not previewing the charge before confirming — the user clicks 'upgrade' and sees a $47 charge they did not expect, then disputes it. Always show the prorated amount on a confirmation screen first.

Best Practices

  • ✓Use Stripe's retrieveUpcoming invoice preview to show the exact charge before the user confirms — this eliminates 95% of 'unexpected charge' support tickets.
  • ✓Make upgrades a one-click action from in-app limit prompts — when a user hits their project limit, show 'Upgrade to Pro for $18 more this month' with a one-click button, no pricing page navigation.
  • ✓Log every plan change with the reason (user-initiated, limit-triggered, admin) — this data is how you learn which features drive upgrades and which pricing gaps drive downgrades.