In our codebase this is how we do upgrade subscription:
Customer clicks upgrade button in our custom website. Then we call:
await stripe.checkout.sessions.create({
customer: customerId,
payment_method_types: ['card', 'cashapp'],
mode: 'subscription',
line_items: [
{
price_data: {
currency: 'usd',
unit_amount: priceCents,
product: stripeProductId,
recurring: {
interval: 'month',
},
tax_behavior: 'exclusive',
},
quantity: 1,
},
],
})
to get the session link and give it to the customer so that he pays to upgrade.
After user pays and we receive checkout.session.completed webhook event, we do stuff on our side (change sql database and etc.). Then we update his subscription using Stripe API:
const subscription = await stripe.subscriptions.retrieve(
subscriptionId,
)
await stripe.subscriptions.update(subscriptionId, {
proration_behavior: 'none',
items: [
{
id: subscription.items.data[0].id,
deleted: true,
},
{
price_data: {
currency: 'usd',
unit_amount: Math.round(priceCentsForFutureCycles),
product: targetPlanStripeProductId,
recurring: {
interval: 'month',
},
tax_behavior: 'exclusive',
},
quantity: 1,
},
],
})
Here is the problem with this API call: when we update from free subscription to a paid one, an invoice is generated automatically and customer gets charged again. In contrary, when we are updating from paid subscription to paid one, invoice is not generated and customer doesn't get charged after this API call, which is desired behavior for us
If all prices on a subscription are 'zero-amount' (i.e. a free subscription), then switching to a paid subscription always resets the billing cycle anchor to the current moment[0], which generates an invoice and charges the customer.
If a customer is subscribed to a 'non-zero' price, with a quantity of 0 (so also a free subscription), changing the quantity to a non-zero amount does not reset the billing cycle[1]. So this approach doesn't result in the customer being charged at the time of the quantity increase.
If you want to avoid charging the customer when switching from a free subscription to a paid one, you could potentially configure your 'free' subscriptions using a non-zero amount price, and setting the quantity to 0. That way, when you update the quantity to switch to a 'paid' subscription, the billing cycle would be unaffected.
Alternatively, if that approach doesn't work for you, then you could use a free trial period[0] to configure the billing cycle, when updating from 'zero-amount' price to a paid one. Setting a free trial period always resets the billing cycle anchor to the end date of the trial, so you could set trial_end[2] when upgrading, to match the desired billing date. This way, you could maintain the existing billing cycle by ensuring that the trial_end set in the Update Subscription call matches the current_period_end[3] of the subscription's existing items.
[0] https://docs.stripe.com/billing/subscriptions/billing-cycle#changing
[1] https://docs.stripe.com/billing/subscriptions/change-price#handle-zero-amount-prices-and-quantities
[2] https://docs.stripe.com/api/subscriptions/object#subscription_object-items-data-current_period_end
[3] https://docs.stripe.com/api/subscriptions/create#create_subscription-trial_end