💳 Stripe 訂閱制 (Subscriptions) 實戰:打造持續性收入

在前面幾章,我們學會了如何實作「單次買斷 (One-time Payment)」的結帳流程。 但如果你打算做 SaaS 平台或是付費會員制社群,「持續性收入 (MRR, Monthly Recurring Revenue)」 才是讓你達到財富自由的關鍵!

本章我們將帶你完整走過 Stripe 訂閱制的建置流程,包含建立訂閱方案、發起 Checkout Session,以及最重要的:如何監聽每個月自動扣款成功的 Webhook。


1. 什麼是 Stripe Subscriptions?

在 Stripe 中,訂閱制與單次付款最大的差異在於它的「週期性」。

  • 單次付款 (Payment Intents):刷卡一次,收費一次,結束。
  • 訂閱制 (Subscriptions):客戶綁定信用卡後,Stripe 會自動在每個月的同一天發起扣款,直到客戶主動取消。

訂閱制的核心元素:

  1. Product (產品):例如「Vibe Tutor 尊榮會員」。
  2. Price (價格):這不是單一數字,而是一個帶有「週期 (Recurring)」屬性的物件,例如「每月 $299」。
  3. Customer (客戶):訂閱制強制需要建立一個 Customer 物件,用來綁定信用卡與聯絡資訊。
  4. Subscription (訂閱):把 Customer 跟 Price 綁在一起的契約。

2. 在 Stripe 後台建立訂閱方案

在寫程式碼之前,最簡單的做法是直接到 Stripe Dashboard 去建立我們的商品與價格。

  1. 登入 Stripe 後台,點擊頂部的 Products (產品) -> Add Product
  2. Name 輸入:Vibe Tutor 尊榮會員
  3. Pricing model 選擇:Standard pricing
  4. Price 輸入:299
  5. Billing period 選擇:Monthly (每月)
  6. 點擊 Save product

儲存後,在價格列表會出現一個以 price_ 開頭的 ID(例如:price_1Nxyz...)。 請把這個 Price ID 複製下來,這是我們程式碼中最重要的鑰匙!


3. 發起訂閱制 Checkout Session

有了 Price ID 之後,我們要在 Next.js 後端建立一支 API 來產生結帳網址。 這個步驟跟單次付款非常像,但有兩個關鍵差異:

  1. mode 必須設定為 'subscription'
  2. 我們必須傳遞 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 狀態。

針對訂閱制,我們最需要監聽的三個事件是:

  1. checkout.session.completed:客戶首次綁卡並付款成功。
  2. invoice.payment_succeeded:每個月自動扣款成功!(這才是 MRR 的精華)。
  3. 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 了嗎?🚀

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!