Stripe订阅业务最佳设置:我们运营的4个模式
我运营的四种Stripe定价模式及其陷阱
在过去几年里,我已经设置过大约十几次Stripe集成,这是我学到的东西:入门教程很简单。难的部分是在同一个组织中运行四种完全不同的定价模型,处理30多个国家的货币而不出现舍入错误,并确保您的webhook处理程序不会在周六凌晨3点悄悄失败。
这不是另一篇"在5分钟内创建结账会话"的文章。我们将详细讲解我们构建和运营的四种生产定价模型——具有地区定价的分层订阅、通过Stripe Connect进行的市场佣金、与特定实体绑定的循环捐赠以及一次性服务付款。每一种都有其自身的陷阱,我将分享特定的代码模式和配置决策,这些决策使我们避免了痛苦的错误。
目录
- 为什么一个Stripe设置不适合所有人
- 模型1:具有地区定价的分层订阅
- 模型2:通过Stripe Connect进行的市场佣金
- 模型3:与实体绑定的循环捐赠
- 模型4:一次性服务付款
- 真正起作用的Webhook架构
- 失败支付重试逻辑和催款
- 花钱教训的零小数位货币错误
- 四种模型的比较
- 常见问题

为什么一个Stripe设置不适合所有人
Stripe的文档对于单一模式的业务非常出色。您选择订阅或一次性付款,按照指南操作,然后您就上线了。但是大多数真实的业务不会长期保持如此简单。
我们跨多个产品运营:一个具有分层订阅的SaaS平台、一个从提供商处获取佣金的市场、一个具有循环赞助的慈善项目,以及一个具有一次性付款的咨询预订系统。这些都在同一个Stripe账户下,但需要完全不同的产品、定价、webhook和客户管理配置。
我看到团队犯的最大错误是试图将所有计费强制纳入一个模型。订阅优先的架构在您需要一次性付款时会崩溃。仅结账会话方法在您需要具有按比例分摊的循环计费时会分崩离析。您需要将Stripe设置视为计费模式的投资组合。
如果您正在构建类似的东西——尤其是在前端使用Next.js或Astro的无头架构上——这些模式将为您节省数周的调试时间。
模型1:具有地区定价的分层订阅
这是我们运营的最复杂的模型,也是教会我们最痛苦的教训的模型。设置:四个层级(免费、基础、专业、高级),定价在30多个国家/地区有所不同。
Stripe中的产品结构
在Stripe中,每个层级都是一个单独的产品。每个产品有多个价格——每个货币/地区组合一个。这很重要:不要尝试使用单一价格并自己进行货币转换。Stripe的多货币定价是为此而专门构建的。
// 地区定价配置
const REGIONAL_PRICING = {
pro: {
USD: { monthly: 2900, yearly: 29000 }, // $29/月,$290/年
EUR: { monthly: 2700, yearly: 27000 }, // €27/月,€270/年
GBP: { monthly: 2300, yearly: 23000 }, // £23/月,£230/年
JPY: { monthly: 4200, yearly: 42000 }, // ¥4,200/月 -- 不是 ¥42.00!
KRW: { monthly: 38000, yearly: 380000 }, // ₩38,000/月
INR: { monthly: 190000, yearly: 1900000 }, // ₹1,900/月
BRL: { monthly: 14900, yearly: 149000 }, // R$149/月
},
// ... 对基础和高级重复
};
注意那些JPY和KRW的值?我稍后会详细讲解那个错误,但简短版本:这些是零小数位货币。当您为JPY传递4200时,Stripe会将其解释为¥4,200——而不是¥42.00。如果您像处理USD一样乘以100,您刚刚向某人收费了¥420,000(2,800美元)而不是¥4,200(28美元)。问我是怎么知道的。
地区试用逻辑
并不是每个地区都可以获得免费试用。我们在某些东南亚市场中以困难的方式学到了这一点,其中试用滥用的情况明显高于其他地区。我们的配置看起来像这样:
const TRIAL_CONFIG = {
default_trial_days: 14,
excluded_regions: ['VN', 'PH', 'ID', 'TH', 'MM', 'KH', 'LA'],
reduced_trial_regions: {
IN: 7,
BR: 7,
},
};
function getTrialDays(countryCode) {
if (TRIAL_CONFIG.excluded_regions.includes(countryCode)) {
return 0;
}
return TRIAL_CONFIG.reduced_trial_regions[countryCode]
?? TRIAL_CONFIG.default_trial_days;
}
这被传递到订阅创建中:
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: regionalPriceId }],
trial_period_days: getTrialDays(customer.address.country),
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method: 'on_subscription',
},
expand: ['latest_invoice.payment_intent'],
});
按比例分摊行为
当某人在周期中途从基础升级到专业时,您需要决定:他们立即支付差额,还是在下一个计费周期支付?我们使用create_prorations进行即时付款:
const updatedSubscription = await stripe.subscriptions.update(subscriptionId, {
items: [{
id: existingItemId,
price: newPriceId,
}],
proration_behavior: 'create_prorations',
payment_behavior: 'pending_if_incomplete',
});
对于降级,我们计划在计费周期结束时进行更改。没有人希望在发票上看到意外的信用计算。
客户门户
Stripe的客户门户被低估了。与其构建自己的订阅管理UI,不如配置门户并将用户重定向到那里:
const portalSession = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.APP_URL}/settings/billing`,
});
在Stripe仪表板中配置它以允许计划变更、取消(带有取消原因调查——数据很宝贵)和付款方式更新。仅这一项就为我们节省了大约40小时的前端开发时间。
模型2:通过Stripe Connect进行的市场佣金
我们的市场模式使用Stripe Connect促进客户和服务提供商之间的支付。平台对每笔交易收取佣金。这是大多数教程都忽略的Stripe Connect设置。
提供商入职
市场上的每个提供商都需要一个Stripe Express账户。入职流程创建账户并将其重定向到Stripe的托管入职:
const account = await stripe.accounts.create({
type: 'express',
country: provider.country,
email: provider.email,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
business_type: 'individual',
metadata: {
provider_id: provider.id,
platform: 'fme',
},
});
const accountLink = await stripe.accountLinks.create({
account: account.id,
refresh_url: `${process.env.APP_URL}/provider/onboarding/refresh`,
return_url: `${process.env.APP_URL}/provider/onboarding/complete`,
type: 'account_onboarding',
});
关键细节:refresh_url是Stripe在链接过期时发送用户的地方。这发生的频率比您想象的要高——如果有人在手机上开始入职,分心了,稍后回来。始终通过生成新链接来优雅地处理这个问题。
佣金结构
当客户预订服务时,我们创建一个带有application_fee_amount的PaymentIntent:
const paymentIntent = await stripe.paymentIntents.create({
amount: bookingAmountInCents,
currency: 'usd',
application_fee_amount: Math.round(bookingAmountInCents * 0.15), // 15% 平台费用
transfer_data: {
destination: providerStripeAccountId,
},
metadata: {
booking_id: booking.id,
provider_id: provider.id,
customer_id: customer.id,
},
});
15%的佣金归平台所有。剩余的85%(扣除Stripe的处理费)转入提供商的Express账户。
支付安排
默认情况下,Stripe按滚动基础向Express账户支付。我们将其覆盖为每周支付,这为我们提供了退款和争议的缓冲:
await stripe.accounts.update(providerStripeAccountId, {
settings: {
payouts: {
schedule: {
interval: 'weekly',
weekly_anchor: 'friday',
},
},
},
});
周五支付意味着提供商在周一之前看到他们账户中的钱。这是一个小事情,但对提供商的满意度和留任率非常重要。
Connect特定的Webhook
通过Stripe Connect,您会收到来自您的平台和连接账户的webhook。您需要一个单独的Connect事件webhook端点:
// 常规webhook端点
app.post('/webhooks/stripe', handlePlatformWebhooks);
// Connect webhook端点
app.post('/webhooks/stripe-connect', handleConnectWebhooks);
Connect webhook处理程序需要以不同的方式验证事件并检查account字段:
async function handleConnectWebhooks(req, res) {
const event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_CONNECT_WEBHOOK_SECRET // 不同的秘密!
);
const connectedAccountId = event.account;
// 现在在连接账户的背景下处理事件
}

模型3:与实体绑定的循环捐赠
这个针对我们正在构建的动物慈善倡议——考虑"赞助特定动物"每月循环捐赠。捐赠者选择一个动物,设置月度金额,并获得照片更新。
实体链接的订阅
这里的技巧是将Stripe订阅链接到您数据库中的特定实体(动物)。我们完全通过元数据完成此操作:
const subscription = await stripe.subscriptions.create({
customer: donorCustomerId,
items: [{
price_data: {
currency: 'usd',
product: sponsorshipProductId,
unit_amount: donorChosenAmount, // 捐赠者选择他们的金额
recurring: {
interval: 'month',
},
},
}],
metadata: {
entity_id: animal.id,
entity_type: 'animal',
entity_name: animal.name,
sponsor_email: donor.email,
},
});
使用price_data而不是预先创建的价格允许捐赠者选择自己的月度金额。这比创建数百个价格对象更清晰。
月度更新电子邮件
当赞助订阅的invoice.paid触发时,我们触发月度更新流程:
async function handleSponsorshipInvoicePaid(invoice) {
const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
const entityId = subscription.metadata.entity_id;
// 使用最新照片队列月度更新电子邮件
await emailQueue.add('sponsorship-update', {
donorEmail: subscription.metadata.sponsor_email,
entityId,
invoiceAmount: invoice.amount_paid,
invoicePdf: invoice.invoice_pdf,
});
}
电子邮件包含发票PDF(Stripe自动生成这些)、被赞助动物的最新照片和护理更新。这是一个小接触,可以显著减少循环捐赠的流失。
处理捐赠取消
当有人取消其赞助时,您需要与SaaS取消的处理方式不同。没有"降级"——要么取消要么什么都没有。但您希望以后更容易重新订阅:
async function handleSponsorshipCancellation(subscription) {
const entityId = subscription.metadata.entity_id;
// 将赞助标记为不活跃,而不是删除
await db.sponsorships.update({
where: { stripeSubscriptionId: subscription.id },
data: {
status: 'inactive',
cancelledAt: new Date(),
},
});
// 发送"我们会想念您"电子邮件,并提供简单的重新订阅链接
await sendCancellationEmail(subscription.metadata.sponsor_email, entityId);
}
模型4:一次性服务付款
最简单的模型,但仍有重要的细节。此模式适用于咨询预订,其中某人支付一次并获得服务——不重复计费。
带有预订数据的结账会话
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{
price: consultationPriceId,
quantity: 1,
}],
customer_email: customer.email,
metadata: {
booking_id: booking.id,
service_type: 'consultation',
appointment_date: booking.date.toISOString(),
practitioner_id: booking.practitionerId,
},
success_url: `${process.env.APP_URL}/booking/confirmed?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/booking/${booking.id}`,
expires_after: 1800, // 30分钟
payment_intent_data: {
metadata: {
booking_id: booking.id,
},
},
});
有两点需要注意。首先:expires_after可防止废弃的结账会话无限期存在。预订时隙不应无限期保留。其次:我们在payment_intent_data.metadata中复制booking_id,因为PaymentIntent元数据与结账会话元数据分开。当您收到payment_intent.succeeded webhook时,您会希望该预订ID就在那里。
付款+预订确认
在checkout.session.completed上,我们一次性确认预订并发送所有内容:
async function handleCheckoutComplete(session) {
const bookingId = session.metadata.booking_id;
// 确认预订
const booking = await db.bookings.update({
where: { id: bookingId },
data: {
status: 'confirmed',
paymentSessionId: session.id,
paidAt: new Date(),
},
});
// 向客户发送确认
await sendBookingConfirmation(session.customer_email, booking);
// 通知从业者
await notifyPractitioner(booking.practitionerId, booking);
}
真正起作用的Webhook架构
在所有四个模型中,webhook都是骨干。这是我们经过太多调试会话后确定的架构:
const WEBHOOK_HANDLERS = {
'checkout.session.completed': handleCheckoutComplete,
'invoice.paid': handleInvoicePaid,
'invoice.payment_failed': handlePaymentFailed,
'customer.subscription.created': handleSubscriptionCreated,
'customer.subscription.updated': handleSubscriptionUpdated,
'customer.subscription.deleted': handleSubscriptionDeleted,
'account.updated': handleConnectAccountUpdated,
'payment_intent.succeeded': handlePaymentSucceeded,
};
async function webhookHandler(req, res) {
let event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody, // 您需要原始正文,而不是解析的JSON
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('Webhook签名验证失败:', err.message);
return res.status(400).send();
}
// 幂等性:检查我们是否已处理此事件
const processed = await db.webhookEvents.findUnique({
where: { stripeEventId: event.id },
});
if (processed) {
return res.status(200).json({ received: true, duplicate: true });
}
const handler = WEBHOOK_HANDLERS[event.type];
if (handler) {
try {
await handler(event.data.object, event);
await db.webhookEvents.create({
data: { stripeEventId: event.id, type: event.type, processedAt: new Date() },
});
} catch (err) {
console.error(`处理${event.type}时出错:`, err);
return res.status(500).send(); // Stripe将重试
}
}
res.status(200).json({ received: true });
}
幂等性检查至关重要。Stripe将重试失败的webhook,您绝对不想处理同一事件两次——尤其是对于创建预订或触发支付的操作。
失败支付重试逻辑和催款
Stripe具有内置的智能重试,但您应该在顶部添加自己的催款逻辑:
async function handlePaymentFailed(invoice) {
const attemptCount = invoice.attempt_count;
const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
if (attemptCount === 1) {
// 首次失败:温和推动
await sendEmail(invoice.customer_email, 'payment-failed-soft', {
updatePaymentUrl: await createPortalLink(invoice.customer),
});
} else if (attemptCount === 2) {
// 第二次失败:更紧急
await sendEmail(invoice.customer_email, 'payment-failed-urgent', {
updatePaymentUrl: await createPortalLink(invoice.customer),
daysUntilCancellation: 7,
});
} else if (attemptCount >= 3) {
// 最后警告
await sendEmail(invoice.customer_email, 'payment-failed-final', {
updatePaymentUrl: await createPortalLink(invoice.customer),
});
}
}
在仪表板中配置Stripe的重试计划 → 设置 → 订阅和电子邮件 → 管理失败的付款。我们在取消前使用3次重试,跨越14天。
花钱教训的零小数位货币错误
这值得拥有自己的部分,因为这是一个最终困扰每个人的错误。Stripe对大多数货币使用美分(最小货币单位)。$29.00变成2900。但某些货币没有小数位。
以下是重要的零小数位货币:
| 货币 | 代码 | 示例:"$29等价物" | 您传递给Stripe的内容 |
|---|---|---|---|
| 日本日元 | JPY | ¥4,200 | 4200(不是420000) |
| 韩国元 | KRW | ₩38,000 | 38000(不是3800000) |
| 越南盾 | VND | ₫700,000 | 700000 |
| 智利比索 | CLP | $25,000 | 25000 |
| 巴拉圭瓜拉尼 | PYG | ₲200,000 | 200000 |
这是我们在任何地方使用的实用程序函数:
const ZERO_DECIMAL_CURRENCIES = [
'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW',
'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF',
'XOF', 'XPF',
];
function toStripeAmount(amount, currency) {
const curr = currency.toUpperCase();
if (ZERO_DECIMAL_CURRENCIES.includes(curr)) {
return Math.round(amount); // 已在最小单位中
}
return Math.round(amount * 100);
}
function fromStripeAmount(stripeAmount, currency) {
const curr = currency.toUpperCase();
if (ZERO_DECIMAL_CURRENCIES.includes(curr)) {
return stripeAmount;
}
return stripeAmount / 100;
}
在任何地方使用它。在您的结账创建中、在您的webhook处理程序中、在您的仪表板显示中。任何地方。您忘记的一次是您向某人收费100倍预期金额的时间。
四种模型的比较
| 方面 | 分层订阅 | 市场 | 循环捐赠 | 一次性付款 |
|---|---|---|---|---|
| Stripe产品 | 多个产品,每个产品多个价格 | 每个服务类型一个产品 | 单一产品,动态定价 | 单一产品,固定价格 |
| 计费模式 | subscription |
payment与Connect |
subscription |
payment |
| Webhook复杂性 | 高(生命周期事件) | 高(Connect事件) | 中等 | 低 |
| 货币处理 | 地区定价矩阵 | 提供商的货币 | 捐赠者的货币 | 单一货币 |
| 试用支持 | 是,取决于地区 | 不适用 | 不适用 | 不适用 |
| 按比例分摊 | 是,升级时 | 不适用 | 不适用 | 不适用 |
| 退款复杂性 | 按比例分摊计算 | 平台费用撤销 | 简单全额退款 | 简单全额退款 |
| 客户门户 | 必不可少 | 不需要 | 很好有 | 不需要 |
| Stripe费用(2025) | 2.9% + 30¢ | 2.9% + 30¢ + 0.5% Connect | 2.9% + 30¢ | 2.9% + 30¢ |
常见问题
我应该为分层定价创建多少个Stripe产品? 每个层级一个产品。所以如果您有免费、基础、专业和高级,那就是四个产品。然后每个产品有多个价格——每个货币和计费间隔组合一个。具有月度和年度计费的Pro层级跨越10种货币意味着该单个产品上有20个价格对象。听起来像很多,但Stripe处理得很好,这使您的目录保持井然有序。
我可以将Stripe结账用于具有地区定价的订阅吗? 是的,但您需要在创建结账会话之前确定客户的地区,以便您可以传递正确的价格ID。我们使用IP地理位置(通过Cloudflare头)预先选择货币,然后让客户确认或更改它。不要依赖结账的自动货币——您希望控制他们看到的价格。
Stripe Connect Express和Custom账户之间有什么区别? Express账户让Stripe处理您的提供商的入职、身份验证和仪表板。Custom账户为您提供完全控制,但需要您自己构建所有这些。对于大多数市场,Express是正确的选择。我们从未遇到过丧失控制权使工程成本值得的情况。Express账户也自动处理税务报告(美国的1099s),这是一个巨大的合规优势。
我如何处理失败的订阅付款而不失去客户?
分层三个东西:Stripe的智能重试(在仪表板中启用)、由invoice.payment_failed webhook触发的自定义催款电子邮件,以及取消前的宽限期。我们在3次重试尝试中给予14天。第一封电子邮件是友好的("嘿,您的卡可能已过期"),第二封是紧急的,第三封是最后警告。包含直接链接到客户门户,他们可以在其中更新其付款方式。仅这一项就恢复了大约30-40%的失败付款。
我是否需要为Stripe Connect提供单独的webhook端点?
是的。平台事件和Connect账户事件使用不同的webhook密钥和不同的事件结构。Connect事件包括一个标识事件所关联的连接账户的account字段。在Stripe仪表板中注册两个端点:一个用于平台事件,一个用于Connect事件。这种分离也使调试变得更加容易。
什么是零小数位货币,我为什么应该关心? 零小数位货币,如JPY(日本日元)和KRW(韩国元)不使用小数单位。当Stripe说"最小货币单位的金额"时,对于USD,这是美分(2900 = $29.00),但对于JPY,它是日元(4200 = ¥4,200)。如果您像处理USD一样乘以100,您收费₩420,000 而不是¥4,200。始终使用在转换前检查货币的辅助函数。Stripe在其文档中维护官方零小数位货币列表。
我应该使用Stripe Billing的客户门户还是构建自己的? 使用客户门户进行订阅管理,除非您有非常具体的UI需求。它开箱即用处理计划变更、取消、付款方式更新和发票历史记录。您可以自定义品牌并配置允许哪些操作。构建自己的门户意味着自己处理按比例分摊计算、支付方式标记化和SCA/3D Secure流程。门户是免费的——它包含在您的Stripe订阅费中。
我如何在本地测试地区定价和货币处理?
Stripe的测试模式支持所有货币。在您计划支持的每种货币中创建测试价格,然后使用Stripe CLI将webhook转发到您的本地服务器:stripe listen --forward-to localhost:3000/webhooks/stripe。对于零小数位货币测试,特别是创建一个JPY价格并在webhook处理程序日志中验证金额,然后再上线。我们还维护一个测试套件,在每个支持的货币上运行toStripeAmount和fromStripeAmount——它已多次捕获问题。
如果您正在构建基于订阅的产品并需要帮助解决计费架构问题,或者如果您正在将Stripe与无头CMS集成,请联系我们。我们已经在多个无头CMS项目上构建了这些模式,可以帮助您避免昂贵的错误。查看我们的定价页面了解互动模型——我们提供基于项目的构建和持续咨询。