最適 Stripe 訂閱商務設置:4 個營運模式
我已設定過 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 Dashboard 中配置它以允許計畫變更、取消(帶取消原因調查──數據很寶貴)和付款方法更新。這項工作單獨節省了我們大約 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 在連結過期時發送用戶的位置。這種情況發生的頻率比你想像的要多──如果某人在手機上開始上線,分心了,稍後回來。始終通過生成新連結來優雅地處理此問題。
佣金結構
當客戶預訂服務時,我們建立一個 PaymentIntent,其中包含 application_fee_amount:
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 用於你的平台和你的連接帳戶。你需要一個單獨的 webhook 端點用於 Connect 事件:
// 常規 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),
});
}
}
在 Dashboard → Settings → Subscriptions and emails → Manage failed payments 中配置 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 事件) | 中等 | 低 |
| 貨幣處理 | 區域定價矩陣 | 提供者的貨幣 | 捐贈者的貨幣 | 單一貨幣 |
| 試用支持 | 是,取決於地區 | N/A | N/A | N/A |
| 按比例分配 | 是,升級時 | N/A | N/A | N/A |
| 退款複雜度 | 按比例分配計算 | 平台費用反轉 | 簡單全額退款 | 簡單全額退款 |
| 客戶入口 | 必要 | 不需要 | 很好擁有 | 不需要 |
| Stripe 費用(2025) | 2.9% + 30¢ | 2.9% + 30¢ + 0.5% Connect | 2.9% + 30¢ | 2.9% + 30¢ |
常見問題
我應該為分層定價建立多少個 Stripe 產品? 每個層級一個產品。因此,如果你有免費、基本、專業和高級,那就是四個產品。然後每個產品有多個價格──每個貨幣和計費間隔組合一個。具有 10 種貨幣的月度和年度帳單的專業層意味著該單一產品上有 20 個價格對象。聽起來很多,但 Stripe 處理得很好,它可以使你的目錄保持井井有條。
我能否將 Stripe Checkout 用於具有區域定價的訂閱? 是的,但你需要在建立 Checkout 階段之前確定客戶的地區,以便傳遞正確的價格 ID。我們使用 IP 地理定位(通過 Cloudflare 標頭)預先選擇貨幣,然後讓客戶確認或更改。不要依賴 Checkout 的自動貨幣──你想控制他們看到的價格。
Stripe Connect Express 帳戶和自定義帳戶之間有什麼區別? Express 帳戶讓 Stripe 處理你提供者的上線、身份驗證和儀表板。自定義帳戶讓你完全控制,但需要你自己構建所有這些。對於大多數市集,Express 是正確的選擇。我們從未遇到過損失控制能證明自定義工程成本合理的情況。Express 帳戶也會自動處理稅務報告(美國的 1099),這是一個巨大的合規性勝利。
我如何在不失去客戶的情況下處理失敗的訂閱付款?
分層三件事:Stripe 的智能重試(在 Dashboard 中啟用)、由 invoice.payment_failed webhook 觸發的自訂催款電子郵件,以及取消前的寬限期。我們在 3 次重試中提供 14 天。第一封電子郵件很友善(「嘿,你的卡可能已過期」),第二封更緊迫,第三封是最後警告。包括客戶入口的直接連結,他們可以在那裡更新他們的付款方法。僅此一項就恢復了大約 30-40% 的失敗付款。
我是否需要為 Stripe Connect 提供單獨的 webhook 端點?
是的。平台事件和 Connect 帳戶事件使用不同的 webhook 秘密和不同的事件結構。Connect 事件包含一個 account 欄位,用於識別事件涉及的連接帳戶。在 Stripe Dashboard 中註冊兩個端點:一個用於平台事件,一個用於 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 專案 中構建了這些模式,可以幫助你跳過昂貴的錯誤。檢查我們的 定價頁面了解參與模式──我們同時進行基於專案的構建和持續顧問。