Stripe API问题:免费订阅升级付费时如何避免重复扣费
订阅升级流程优化与重复扣费问题解决
当前流程说明
1. 生成支付会话
用户点击网站上的upgrade按钮后,调用以下代码创建Stripe Checkout会话,引导用户完成支付:
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, }, ], })
2. 支付完成后的处理
用户完成支付后,系统收到checkout.session.completed Webhook事件,先执行本地操作(如更新SQL数据库),再调用以下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, }, ], })
问题分析
从免费订阅升级到付费订阅时,会出现重复扣费:Checkout会话已完成一次扣费,后续调用stripe.subscriptions.update时,Stripe会再次生成账单扣费。而付费订阅间升级时无此问题,因为原有付费订阅的计费周期已存在,加上设置了proration_behavior: 'none',不会触发立即计费。
核心原因:免费订阅无有效计费周期,更新订阅时添加付费项目会触发Stripe的立即计费逻辑,导致二次扣费;而付费订阅升级时,系统默认沿用原有计费周期,不会立即生成新账单。
解决方案
方案1:区分订阅类型处理
在Webhook处理逻辑中,先判断用户原有订阅是否为免费订阅:
- 如果是免费订阅升级:Checkout会话已创建新的付费订阅,只需取消原有免费订阅即可,无需调用
stripe.subscriptions.update更新旧订阅。 - 如果是付费订阅升级:保留现有更新逻辑,利用
proration_behavior: 'none'避免立即计费。
关键代码调整示例:
// 从checkout.session.completed事件中获取新创建的订阅ID const newSubscriptionId = event.data.object.subscription; const oldSubscriptionId = getCustomerCurrentSubscriptionId(customerId); // 自定义方法获取用户当前订阅ID const oldSubscription = await stripe.subscriptions.retrieve(oldSubscriptionId); // 判断原有订阅是否为免费(通过price的unit_amount是否为0判断) const isOldSubscriptionFree = oldSubscription.items.data[0].price.unit_amount === 0; if (isOldSubscriptionFree) { // 免费转付费:直接取消旧订阅,无需更新 await stripe.subscriptions.cancel(oldSubscriptionId); } else { // 付费转付费:执行原有更新逻辑 await stripe.subscriptions.update(oldSubscriptionId, { proration_behavior: 'none', items: [ { id: oldSubscription.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, }, ], }); // 取消Checkout创建的新订阅(因为原有更新逻辑是升级旧订阅) await stripe.subscriptions.cancel(newSubscriptionId); }
方案2:统一使用订阅升级模式(推荐)
不再通过Checkout创建新订阅,而是直接基于现有订阅创建升级会话,避免重复创建订阅导致的计费问题:
- 创建Checkout会话时,指定
subscription参数为用户现有订阅ID,同时设置payment_method_types和line_items为目标付费方案:
await stripe.checkout.sessions.create({ customer: customerId, payment_method_types: ['card', 'cashapp'], mode: 'subscription', subscription: oldSubscriptionId, // 指定现有订阅ID line_items: [ { price_data: { currency: 'usd', unit_amount: priceCents, product: targetPlanStripeProductId, recurring: { interval: 'month', }, tax_behavior: 'exclusive', }, quantity: 1, }, ], proration_behavior: 'none', // 升级时不生成 prorations })
- 用户完成支付后,Stripe会自动更新现有订阅,无需再手动调用
stripe.subscriptions.update,彻底避免重复扣费。
这个方案更符合Stripe的官方升级流程,同时减少冗余操作,降低出错概率。
内容的提问来源于stack exchange,提问作者Noname
相关产品推荐
相关产品推荐

