💳 Stripe サブスクリプション実戦:持続可能な収益を構築
前章までで「ワンタイム決済(One-time Payment)」の実装方法を学びました。 しかしSaaSプラットフォームや有料会員制コミュニティを運営する場合、**「継続収益(MRR, Monthly Recurring Revenue)」**こそが経済的自由を実現する鍵です!
本章ではStripeサブスクリプションの構築プロセスを完全ガイド。プラン作成、Checkout Sessionの開始、そして最も重要な「月次自動課金成功を監視するWebhook」の設定までを解説します。
1. Stripeサブスクリプションとは?
Stripeにおけるサブスクリプションとワンタイム決済の最大の違いは「周期性」にあります。
- ワンタイム決済 (Payment Intents):1回の決済で終了
- サブスクリプション (Subscriptions):カード登録後、Stripeが毎月同日に自動課金を実行。顧客が解約するまで継続
サブスクリプションの主要要素:
- Product (商品):例「Vibe Tutorプレミアム会員」
- Price (価格):単なる数値ではなく「周期(Recurring)」属性を持つオブジェクト。例「月額299円」
- Customer (顧客):カード情報と連絡先を紐付ける必須オブジェクト
- Subscription (契約):CustomerとPriceを結びつける契約関係
2. Stripeダッシュボードでプラン作成
コーディング前に、Stripe管理画面で商品と価格を設定しましょう。
- 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バックエンドにチェックアウトURL生成APIを作成します。 ワンタイム決済との主な違いは2点:
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初期化(.env.localにSecret Keyを設定)
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:顧客紐付け用メール
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:Webhookでユーザー特定するためのメタデータ
subscription_data: {
metadata: {
vibe_user_id: userId,
},
},
});
// 生成されたチェックアウトURLを返却
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
顧客がチェックアウトでカード登録後、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をDBに保存し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: サブスクリプション有効期限を1ヶ月延長
}
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 URL生成
// 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(); // データベースから取得した顧客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を呼び出すだけで、サポートコストをゼロに近づけられます!
これがSaaSプラットフォームの魅力です。一度コードを書けば、疲れ知らずのロボットが毎月自動で課金、請求書発行、解約処理を行います。さあ、最初のMRRを受け取る準備はできましたか?🚀