第三章:完璧なWebhook処理とデータベース同期
前章では、ユーザーをStripeの決済ページに送る方法を学びました。 しかし、決済業界で最も重要な格言を覚えておく必要があります:「フロントエンド画面の『支払い成功』は、決して実際の支払いの根拠として扱ってはいけない」
ユーザーがカード決済に成功した瞬間、ネットワークが切断されたりブラウザタブを閉じたりする可能性があり、success_urlページに到達しない場合があります。データベースへの書き込みロジックをフロントエンドに置いていると、ユーザーは「カードは引き落とされたが、商品が受け取れない」という悲惨なクレームに直面することになります。
唯一信頼できる方法は、Webhookを使用することです。これはStripeのサーバーがバックグラウンドであなたのサーバーに「ねえ、お金を受け取ったよ。この注文の詳細は...」と電話をかけるようなものです。この「電話」はユーザーのブラウザの状態に影響されず、最も安定かつ安全な決済確認メカニズムです。
🔒 1. Webhook署名(Signature):セキュリティの核心
WebhookがStripeから送信されるHTTP POSTリクエストであるなら、ハッカーがStripeを装って「100万ドル支払った」という偽リクエストを送ってきたらどうしますか?
Stripeの解決策は:**Webhook署名(Signature)**です。
StripeダッシュボードでWebhook URLを設定する際、Stripeは専用のWebhook Secret(whsec_で始まる)を発行します。
Stripeがリクエストを送るたびに、HTTPヘッダーにこのSecretで計算された「署名」が含まれます。私たちはリクエストを受け取ったら、同じSecretを使ってこの署名を検証し、一致しない場合はハッカーによる偽造と判断して拒否します!
💻 2. Stripe Webhook APIの実装
Next.js App Routerでは、このAPIを作成します:src/app/api/stripe/webhook/route.ts。
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { createClient } from '@supabase/supabase-js'; // ここではAdmin権限で書き込む必要があることに注意
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
// 決済Webhookは非常に特殊で、署名を正しく検証するためには「生のバイトデータ(Raw Body)」を受け取る必要がある
// Next.jsではデフォルトのJSON解析を無効にする必要がある(ただしApp Routerでは直接request.text()を読めば良い)
export async function POST(request: Request) {
const payload = await request.text();
const sig = request.headers.get('stripe-signature');
let event: Stripe.Event;
try {
// 1. コア防御:署名検証!この行が通れば、確実に本物のStripeからのリクエスト
event = stripe.webhooks.constructEvent(
payload,
sig!,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err: any) {
console.error('⚠️ Webhook署名検証に失敗しました.', err.message);
return NextResponse.json({ error: 'Webhook signature verification failed' }, { status: 400 });
}
// 2. 高権限のデータベース接続を初期化(Webhookはバックグラウンドで実行されるため、現在のユーザーのCookieがない)
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
// 3. イベントタイプに基づいて処理
try {
switch (event.type) {
// イベントA:チェックアウトプロセス完了(一回限りの支払いまたはサブスクリプション初回)
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
// 前章で密かに保存したuserIdを取得
const userId = session.metadata?.userId;
if (userId) {
console.log(`💰 支払い成功!ユーザー: ${userId}, 金額: ${session.amount_total}`);
// データベースに書き込み、正式に権限を付与!
await supabaseAdmin.from('vt_purchases').insert({
user_id: userId,
item_id: 'vip-all-access', // 全サイトアクセスパスと仮定
order_no: session.id,
amount: session.amount_total ? session.amount_total / 100 : 0 // Stripeの金額は最小単位(セント)で計算されるため、100で割る
});
}
break;
}
// イベントB:サブスクリプションの月次課金成功(Recurring Payment)
case 'invoice.paid': {
const invoice = event.data.object as Stripe.Invoice;
// 毎月自動更新機能を実装する場合は、ここでユーザーのサブスクリプション有効期限を更新!
break;
}
// イベントC:サブスクリプション課金失敗(カード期限切れまたは残高不足)
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
// ここでユーザーのVIP権限を一時停止し、カード更新を通知
break;
}
default:
console.log(`未処理のイベントタイプ ${event.type}`);
}
// 4. 【極めて重要】必ず200 OKをStripeに返す
// 返さない場合、Stripeはサーバーが故障したと判断し、その後3日間APIを叩き続ける
return NextResponse.json({ received: true });
} catch (err) {
console.error('Webhookロジック処理中にエラーが発生しました:', err);
return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 });
}
}
🚧 3. LocalhostでWebhookをテストする方法
前章のECPayセクションで説明した「大きな落とし穴」:ECPayのサーバーはあなたのlocalhostにアクセスできません。
では、Stripeの開発段階で、1行コードを書くたびにVercelにデプロイしてテストしなければならないのでしょうか?
Stripeは、世界最高の開発者体験を提供する企業としての実力を発揮し、公式ツールを提供しています:Stripe CLI。
- あなたのPCにStripe CLIをインストール。
- ターミナルでStripeアカウントにログイン:
stripe login - コマンドを実行し、StripeのWebhookをローカルネットワーク経由で直接localhostに転送:
stripe listen --forward-to localhost:3000/api/stripe/webhook - このコマンドを実行すると、ターミナルに次の行が表示されます:
Your webhook signing secret is whsec_xxxxxxxx...これがローカル開発用のSTRIPE_WEBHOOK_SECRETなので、.env.localに貼り付けましょう!
これで、ローカルで決済ページからカード決済を行うと、StripeのクラウドからCLI経由でWebhook信号が送られ、ファイアウォールを越えてあなたのPCのAPIに正確に届きます!このような最高級の開発体験こそ、世界中の開発者がStripeを愛する理由です。
🏆 結語
Stripe国際決済の連携が完了しました! あなたは今、「Apple Payクイック決済」、「多通貨対応」、「安全なWebhook決済確認」、「サブスクリプション対応」を備えた国際級SaaSインフラを手に入れました。
ソフトウェアに国境はありません。あなたのコードにも国境があってはいけません。準備を整えて、あなたのソフトウェアを世界中に販売しましょう!🚀