Plan Upgrade, Downgrade & Mid-Cycle Billing Changes: Chat Prompts
When a customer wants to change plans — upgrade to Pro, downgrade from Enterprise to Pro, switch from monthly to annual, add 5 more seats, change billing currency, switch payment method — the UX has to be self-serve, the math has to be correct (proration, credits, refunds, taxes), and the state machine has to handle the awkward middle states (downgrade scheduled but not effective; payment failed during upgrade; tier-locked feature already in use). Most teams ship the upgrade flow first (because that's revenue) and treat downgrade as an afterthought (which is how customers end up emailing support to cancel because the product won't let them do it themselves).
This is the chat-prompt playbook for plan changes that work for both directions, get the proration math right, handle the edge cases, and integrate cleanly with Stripe's billing primitives without you reinventing the wheel.
When This Belongs
Use a plan-change UX when:
- You have multiple paid tiers (Free / Pro / Enterprise; or seat-based plans)
- Customers self-serve their plan selection
- Billing happens via Stripe Subscriptions / Stripe Billing or equivalent
Don't bother when:
- Single-tier product (no plans to change)
- All sales-led contracting (CSM handles plan changes, not customer)
Stripe Subscription Primitives (the foundation)
Most modern SaaS uses Stripe Subscriptions. The primitives:
- Subscription: the recurring billing object
- SubscriptionItem: one line on a subscription; ties to a Price
- Price: a specific price + product + billing interval (monthly / annual)
- Proration: Stripe's automatic credit/charge calculation when a subscription changes mid-cycle
- Customer Balance: residual credits/debits on the customer's account
- Schedule (SubscriptionSchedule): planned future changes (downgrade scheduled at end of period)
- Phase: a period within a SubscriptionSchedule (current phase + future phase)
For a plan change, you typically:
- Update the SubscriptionItem to the new Price OR
- Create a SubscriptionSchedule with the current phase + a future phase representing the new plan
Stripe handles the proration math; your job is to choose the right primitive + give the customer clear UX.
Upgrade Flow (Customer Going to Higher Tier)
Most upgrades are "I want this immediately" — apply now, charge proration, unlock features instantly.
Build an upgrade flow for [product] from Pro ($49/mo) to Pro Plus ($99/mo).
Behavior:
1. User clicks "Upgrade to Pro Plus" from billing settings or paywall
2. Show preview modal:
- "You'll be charged $X today (prorated for remainder of this billing period)"
- "Your monthly bill going forward will be $99/mo"
- Date of next full bill
- Compute the proration math by calling Stripe's Upcoming Invoice API or your own logic
3. User confirms
4. Update Stripe subscription:
- `subscriptions.update(subId, { items: [{ id: itemId, price: newPriceId }], proration_behavior: 'create_prorations' })`
5. Stripe immediately charges proration via the latest invoice
6. Webhook receives `customer.subscription.updated` + `invoice.paid`
7. Your DB updates account tier to 'pro_plus' immediately
8. UI confirmation: "You're now on Pro Plus! 🎉"
9. Email receipt sent
Edge cases:
- Payment fails on proration: roll back tier? Or grant tier with grace period?
- Customer was on grandfathered pricing: don't lose that
- Tax handling: Stripe Tax + correct address
- Currency: stays on customer's existing currency
Implement:
- The preview function calling Stripe Upcoming Invoice
- The confirmation modal
- The actual subscription update
- The webhook handler updating the local DB
- Failure paths
Stack: Next.js + Stripe Node SDK + Drizzle.
Downgrade Flow (Customer Going to Lower Tier)
Downgrades are trickier. Customer wants to drop to Pro from Pro Plus. Standard pattern: schedule the downgrade at end of current billing period (don't refund the unused remainder unless your policy says so).
Build a downgrade flow from Pro Plus to Pro.
Behavior:
1. User clicks "Downgrade" from billing settings
2. Save offer / retention prompt (optional but worth it):
- "We're sorry to see you go to Pro. Would [common reason] help?" with one-click solutions
- Discount offer ("Stay on Pro Plus at 50% off for 3 months")
- Skip if user clicks "I'm sure I want to downgrade"
3. Show downgrade preview:
- "Your plan will downgrade to Pro on [date — end of current period]"
- "You'll keep Pro Plus access until that date"
- "Starting [date], you'll be billed $49/mo"
- Features they'll lose (e.g., "You'll lose access to: Advanced Reports, API access")
- If they currently use those features at limits beyond the lower tier (e.g., 12 users on Pro Plus where Pro caps at 5): warn them they need to remove users first OR data will be locked at downgrade
4. User confirms
5. Schedule via SubscriptionSchedule:
stripe.subscriptionSchedules.create({ from_subscription: subId, phases: [ { items: [{ price: currentPriceId, quantity: 1 }], end_date: currentPeriodEnd }, { items: [{ price: newPriceId, quantity: 1 }] } ] })
6. Confirmation: "Your downgrade is scheduled for [date]"
7. Banner shows "Plan downgrading to Pro on [date]" until effective
8. User can cancel the downgrade until it's effective
9. On the scheduled date, Stripe webhook `subscription_schedule.released` fires; subscription is now on the new plan
10. Email confirmation of the actual downgrade
Edge cases:
- User has more users / data than the lower tier allows
- User has enterprise features in use that don't exist on the lower tier
- User cancels the downgrade before it takes effect
Implement:
- The save-offer / retention modal
- The downgrade preview + warnings about feature loss
- The SubscriptionSchedule creation
- The "downgrade scheduled" banner
- The cancel-downgrade flow
- The webhook handler that updates account tier on the actual transition
Stack: Next.js + Stripe + Drizzle.
Switching Billing Interval (Monthly ↔ Annual)
A specific change with different math.
Build the "switch to annual" flow:
Behavior:
- User on Pro monthly ($49/mo) wants to switch to Pro annual ($490/yr — 2 months free).
- Apply immediately; charge them the new annual amount; credit them for the unused remainder of the current monthly period.
Steps:
1. Preview: "You'll save $98/yr on annual ($490 vs $588 monthly equivalent). Today you'll be charged $X (annual) minus credit of $Y (unused monthly). Net: $Z."
2. User confirms
3. Stripe subscription update with `proration_behavior: 'create_prorations'` and the new annual price
4. Stripe automatically charges the new amount + credits the unused monthly portion
5. Subscription's billing_cycle_anchor is reset to today
For switch FROM annual TO monthly:
- Usually scheduled at end of current annual period (no refund of unused annual)
- Optional: prorated refund if your policy allows
- Default: schedule the change for end of current period via SubscriptionSchedule
Implement both directions.
Stack: Next.js + Stripe + Drizzle.
Seat Changes (Add / Remove Seats)
Per-seat plans need their own UX.
Build seat-add / seat-remove for a per-seat product:
Add seats:
1. User adds 5 seats (10 → 15) via team admin page
2. Preview: "Add 5 seats × $10/seat/mo × prorated for remainder of period = $X. Going forward: $150/mo"
3. User confirms
4. Stripe update: subscriptionItem.quantity = 15
5. Charge prorated amount immediately
Remove seats:
1. User removes 3 seats (15 → 12)
2. Question: when does this take effect?
- Default: immediately, with proration credit (not refund) on customer balance
- Alternative: schedule for end of period (no immediate impact)
3. Preview: "Reduce to 12 seats. You'll receive $X credit toward future bills (or refund if requested)."
4. User confirms
5. Stripe update with proration
Edge cases:
- Removing more seats than active users (allow; admin must remove users separately)
- Removing seats below current active user count (block; force user removal first)
Implement both flows + per-seat config.
Stack: Next.js + Stripe + Drizzle.
Save Offers / Retention Prompts
The downgrade flow is where save offers belong. Done well, they convert 20-40% of would-be downgrades.
Implement save-offer logic in the downgrade flow:
Patterns:
1. **Reason picker**: "Why are you downgrading?" with options (too expensive, not using it, switching to competitor, etc.)
2. **Targeted offer based on reason**:
- Too expensive → "Stay on Pro Plus at 50% off for 3 months"
- Not using it → "Use these features you might have missed" + tutorial videos
- Switching to competitor → "What does [competitor] do that we don't?" (data for product team)
- Just trying to save money → "Switch to annual at 2 months free"
3. **One-click action**: accept the offer applies immediately
4. **No-questions exit**: "I just want to downgrade" always works
Track:
- Offer-shown rate
- Offer-accepted rate
- Downgrade-completed rate
- Long-term retention of save-offer takers
Don't:
- Force users through 3 modals to downgrade — feels like dark pattern
- Repeat the same save offer if rejected (one chance per attempt)
- Hide the actual downgrade button
Build the save-offer modal + reason picker + offer application logic.
Stack: Next.js + Stripe + your CRM event tracking.
Cancellation Flow (Distinct from Downgrade-to-Free)
Cancellation = ending the subscription entirely. Different from downgrade-to-free-tier.
Build cancellation flow:
1. User clicks "Cancel subscription" from billing settings
2. Save offers (similar to downgrade)
3. Cancellation preview:
- "Your subscription will end on [date — end of current period]"
- "You'll have access to Pro features until then"
- "After [date], your account becomes Free tier (or read-only / closing depending on your model)"
- Data export option clearly visible
4. User confirms
5. Stripe: `subscriptions.update(subId, { cancel_at_period_end: true })`
6. Banner shows "Subscription cancelled — ending [date]" until period ends
7. User can reactivate anytime before period end
8. On period end, webhook `customer.subscription.deleted` fires; account moves to Free / closing state
For immediate cancellation with refund (optional):
- Stripe: `subscriptions.cancel(subId, { prorate: true, invoice_now: true })`
- Customer gets refund for unused period
- Use only when policy allows / customer requests
Implement both end-of-period cancel + immediate cancel with refund.
Stack: Next.js + Stripe + Drizzle.
Payment Method Updates
Card expired or customer wants to switch.
Build a payment-method-update flow:
1. User clicks "Update payment method" in billing settings
2. Stripe Setup Intent: create with `usage: 'off_session'` for future charges
3. Render Stripe Elements (or Checkout) for card collection
4. On success: attach the new payment method as default
5. Optional: detach old payment method
6. Use the new method for future invoices automatically
Edge cases:
- Subscription is past due (last invoice failed): immediately retry the failed invoice with the new method
- Customer is in dunning state: clearing the dunning state on successful payment
Use Stripe Customer Portal as a quick alternative if you don't need custom UX. Self-rolled gives more control + branding.
Implement:
- The Setup Intent + Elements integration
- The retry-on-fail logic for past-due subscriptions
- The integration with Stripe Customer Portal (optional)
Stack: Next.js + Stripe + Drizzle.
State Visualization: Where Am I in the Plan?
Customers need to know what state they're in. Build a clear "Current plan" panel.
Build a billing dashboard showing:
1. Current plan + quantity + next billing date
2. Active scheduled changes (e.g., "Downgrading to Pro on [date]") with cancel button
3. Next bill amount + breakdown (prorated items, taxes)
4. Payment method (last 4 + expiry)
5. Past invoices (link to download)
6. Upgrade / downgrade / cancel actions
7. Save offers banner if applicable
States to handle:
- Active + on-schedule
- Active + scheduled change (downgrade pending)
- Past due (last payment failed)
- Cancelled (ending in N days)
- Cancelled + ended (Free / read-only)
- Trial (with countdown if applicable)
Use Stripe Customer Portal for a simpler path if you want default UX; build custom if branding/integration matters.
Stack: Next.js + Stripe + Drizzle + shadcn/ui.
Common Pitfalls
Math wrong on proration. Reinventing Stripe's proration logic in your code. Don't — use Stripe's Upcoming Invoice API to preview and let Stripe handle the actual calculation.
Downgrade applied immediately when you meant scheduled. Customer downgrades; their plan downgrades right now; they're upset because they paid for the higher tier. Default to scheduled-end-of-period.
No "feature loss" warning on downgrade. Customer downgrades; previously-used features stop working without warning. List what they'll lose; offer to migrate them.
No "scheduled change" banner. Customer downgrades; weeks pass; they forget; their plan changes; they're surprised. Persistent banner.
No way to cancel a scheduled change. Customer schedules downgrade; changes their mind tomorrow; can't undo. Always allow cancellation of scheduled changes.
Upgrade flow that doesn't apply immediately. Customer wants Pro Plus now; system says "your plan will upgrade at end of period." Confusing. Upgrades = immediate; downgrades = end of period (default).
Payment failure during upgrade silently rolls back. Customer thinks they upgraded; they didn't; surprise next month. Surface failures clearly.
No save offers on downgrade. Leaving 20-40% retention on the table.
Save offers that feel like dark patterns. 4 modals before "yes I really want to downgrade" — generates rage. Make exit fast for users who decided.
Tax handling forgotten. Proration math doesn't include taxes; customer charged less than expected. Use Stripe Tax + correct address handling.
Currency mismatch. Customer changed plans; new plan is in different currency. Stripe doesn't handle this gracefully — keep customer on their existing currency.
Webhook race conditions. Subscription updated event fires before invoice paid event; your DB tier-update happens before the charge succeeds. Listen to specific events that confirm payment.
No retry on failed proration charge. Stripe's auto-retry usually handles, but watch for stuck states.
Per-seat changes that break SSO / SAML. Removing seats removes users; their SAML config breaks. Coordinate seat changes with user removal.
Plan changes for grandfathered customers losing their grandfathered pricing. A user on a legacy "founder" plan upgrades; your code applies the current price; they're now paying more. Preserve grandfathered pricing on upgrade unless explicitly opted out.
No "preview before commit" UX. Customer clicks upgrade; immediately charged; surprised by the proration amount. Always show the preview first.
Cancellation that locks data immediately. Customer cancels; their data becomes inaccessible same-day; they panic. Provide grace period; export option; clear timeline.
No "reactivate" path. Cancelled customers want to come back; have to call sales. Self-serve reactivation always.
Forgetting to handle annual plan partial refunds. Annual prepay; customer downgrades mid-year; what about the 8 months they paid for and won't use? Decide policy; stick to it.
Trial that auto-converts without warning. Trial ends → card charged for full year → customer didn't realize → chargeback. Show countdown; email reminders; first-charge confirmation email.
Stripe Customer Portal vs custom UI mismatch. Some flows in your custom UI; some in the Customer Portal; users get confused. Either commit to one or build coherent linking.
Webhook handler not idempotent. Same event delivered twice; tier updated twice; customer gets duplicate confirmation emails. Idempotency keys + processed-event tracking.
See Also
- Stripe
- Stripe Customer Portal
- Stripe Usage-Based Billing
- Subscription Billing Providers — Recurly / Chargebee alternatives
- Subscription Analytics Platforms
- Trial to Paid — separate flow for trial → first paid
- Trial Countdown / Conversion UI
- Trial Extension / Save Offer UX
- Dunning & Failed Payments
- Refunds & Chargebacks
- Tax / VAT Handling
- Currency / FX Handling
- Quotas, Limits & Plan Enforcement
- Account Suspension & Fraud Holds
- Account Deletion & Data Export
- Reduce Churn
- Win-Back Churned Customers
- Pricing Page (LaunchWeek)
- Pricing Strategy (LaunchWeek)
- Pricing Migration / Repackaging (LaunchWeek)
- Pricing Experiments (LaunchWeek)
- Free Trial vs Freemium (LaunchWeek)
- Discount and Promotion Strategy (LaunchWeek)
- Raise Prices (LaunchWeek)
- Pricing Review Cadence (LaunchWeek)
- Customer Segmentation & Tiering (LaunchWeek)
- Free to Paid (LaunchWeek)
- Settings & Account Pages
- Roles & Permissions
- In-App Status Banners & System Notifications
- Toast Notifications UI
- Microcopy & Product Copy Systems