💳 Stripe 訂閱制 (Subscriptions) 實戰:打造持續性收入
在前面幾章,我們學會了如何實作「單次買斷 (One-time Payment)」的結帳流程。 但如果你打算做 SaaS 平台或是付費會員制社群,「持續性收入 (MRR, Monthly Recurring Revenue)」 才是讓你達到財富自由的關鍵!
本章我們將帶你完整走過 Stripe 訂閱制的建置流程,包含建立訂閱方案、發起 Checkout Session,以及最重要的:如何監聽每個月自動扣款成功的 Webhook。
1. 什麼是 Stripe Subscriptions?
在 Stripe 中,訂閱制與單次付款最大的差異在於它的「週期性」。
- 單次付款 (Payment Intents):刷卡一次,收費一次,結束。
- 訂閱制 (Subscriptions):客戶綁定信用卡後,Stripe 會自動在每個月的同一天發起扣款,直到客戶主動取消。
訂閱制的核心元素:
- Product (產品):例如「Vibe Tutor 尊榮會員」。
- Price (價格):這不是單一數字,而是一個帶有「週期 (Recurring)」屬性的物件,例如「每月 $299」。
- Customer (客戶):訂閱制強制需要建立一個 Customer 物件,用來綁定信用卡與聯絡資訊。
- Subscription (訂閱):把 Customer 跟 Price 綁在一起的契約。
2. 在 Stripe 後台建立訂閱方案
在寫程式碼之前,最簡單的做法是直接到 Stripe Dashboard 去建立我們的商品與價格。
- 登入 Stripe 後台,點擊頂部的 Products (產品) -> Add Product。
- Name 輸入:
Vibe Tutor 尊榮會員。 - Pricing model 選擇:
Standard pricing。 - Price 輸入:
299。 - Billing period 選擇:
Monthly (每月)。 - 點擊 Save product。
儲存後,在價格列表會出現一個以 price_ 開頭的 ID(例如:price_1Nxyz...)。
請把這個 Price ID 複製下來,這是我們程式碼中最重要的鑰匙!
3. 發起訂閱制 Checkout Session
有了 Price ID 之後,我們要在 Next.js 後端建立一支 API 來產生結帳網址。 這個步驟跟單次付款非常像,但有兩個關鍵差異:
mode必須設定為'subscription'。- 我們必須傳遞
customer_email或是建立一個customer,因為訂閱需要知道是誰在付錢。
撰寫 API Route
建立檔案:src/app/api/stripe/create-subscription/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
// 初始化 Stripe (記得把 Secret Key 放在 .env.local)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
export async function POST(request: Request) {
try {
const body = await request.json();
const { email, userId } = body;
// 1. 檢查參數
if (!email || !userId) {
return NextResponse.json({ error: 'Missing email or userId' }, { status: 400 });
}
// 2. 建立 Checkout Session
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'subscription', // 關鍵 1:這是一筆訂閱!
customer_email: email, // 關鍵 2:需要 Email 來綁定客戶
line_items: [
{
// 把剛剛在後台複製的 Price ID 貼在這裡
price: 'price_1NxyzYOUR_PRICE_ID_HERE',
quantity: 1,
},
],
// 結帳成功與取消的轉跳網址
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
// 關鍵 3:把我們資料庫的 User ID 塞進 metadata,這樣 Webhook 才知道是誰付錢的
subscription_data: {
metadata: {
vibe_user_id: userId,
},
},
});
// 回傳生成的結帳網址
return NextResponse.json({ url: session.url });
} catch (error: any) {
console.error('Stripe Subscription Error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
4. 監聽訂閱狀態的 Webhook
當客戶在 Checkout 頁面刷卡成功後,Stripe 會開始每個月自動扣款。 我們需要一支 Webhook 來接收 Stripe 的通知,並更新我們資料庫中的 VIP 狀態。
針對訂閱制,我們最需要監聽的三個事件是:
checkout.session.completed:客戶首次綁卡並付款成功。invoice.payment_succeeded:每個月自動扣款成功!(這才是 MRR 的精華)。customer.subscription.deleted:客戶取消訂閱或信用卡徹底失效。
實作 Webhook 邏輯
建立/修改檔案:src/app/api/stripe/webhook/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { headers } from 'next/headers';
// 假設你有自己的資料庫更新邏輯
import { updateUserSubscription } from '@/lib/db';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text();
const sig = headers().get('stripe-signature');
let event: Stripe.Event;
try {
// 驗證這是真的來自 Stripe 的呼叫,不是駭客偽造的
event = stripe.webhooks.constructEvent(body, sig!, endpointSecret);
} catch (err: any) {
return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
}
// 根據不同事件進行處理
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
if (session.mode === 'subscription') {
// 首次訂閱成功
const userId = session.subscription_data?.metadata?.vibe_user_id;
const subscriptionId = session.subscription as string;
console.log(`User ${userId} started subscription ${subscriptionId}`);
// TODO: 把 subscriptionId 存入資料庫,並開通 VIP 權限
await updateUserSubscription(userId, 'active', subscriptionId);
}
break;
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object as Stripe.Invoice;
if (invoice.subscription) {
// 每個月扣款成功!續約!
const subscriptionId = invoice.subscription as string;
console.log(`Payment succeeded for subscription ${subscriptionId}`);
// TODO: 更新資料庫中的訂閱到期日 (往後延一個月)
}
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
const subscriptionId = subscription.id;
console.log(`Subscription ${subscriptionId} was canceled.`);
// TODO: 拔除使用者的 VIP 權限
break;
}
default:
console.log(`Unhandled event type ${event.type}`);
}
return NextResponse.json({ received: true });
}
5. 讓客戶自己管理訂閱 (Customer Portal)
做訂閱制最怕遇到「客服災難」:每天都有人來信說「我要換信用卡」、「我要取消訂閱」、「我要下載發票」。 Stripe 最佛心的一點,就是它內建了一個完全免費的 Customer Portal (客戶門戶)。
只要一行程式碼,你就能產生一個專屬網址,客戶點進去就能自己:
- 更新信用卡資料
- 暫停或取消訂閱
- 下載過去的收據發票
產生 Customer Portal 網址
// src/app/api/stripe/portal/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
export async function POST(request: Request) {
try {
const { customerId } = await request.json(); // 從你資料庫讀出的 Stripe Customer ID
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard`, // 客戶管理完後跳轉回哪裡
});
return NextResponse.json({ url: session.url });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
你只需要在前端做一個按鈕 <button onClick={handleManageBilling}>管理我的訂閱</button>,去呼叫這支 API 拿到 URL 然後轉跳過去,你的客服成本直接降為 0!
這就是 SaaS 平台之所以迷人的原因:寫好一次程式碼,系統就會像一個不知疲倦的機器人,每個月自動幫你收錢、發票、處理退訂。準備好迎接你的第一筆 MRR 了嗎?🚀