🪝

個人開発者のためのStripe超入門②基本実装-単発決済(後半)

に公開

個人開発者のためのStripe超入門②基本実装-単発決済(後半)

第2章(後半) Webhook処理の実装

概要

Stripe決済における重要な機能の一つがWebhookです。決済完了・失敗、サブスクリプション更新などのイベントをStripeからリアルタイムで受け取り、アプリケーションの状態に反映させることができます。

Webhookとは

WebhookはStripeからアプリケーションに送信されるHTTPリクエストです。決済関連のイベントが発生すると、Stripeが自動的にアプリケーションのエンドポイントに、あらかじめ設定したPOSTリクエストを送信します。

主要なWebhookイベント(Checkout関連)

  • checkout.session.completed: Checkout決済完了
  • checkout.session.expired: Checkout セッション期限切れ
  • checkout.session.async_payment_succeeded: 非同期決済成功(銀行振込等)
  • checkout.session.async_payment_failed: 非同期決済失敗
  • payment_intent.succeeded: 決済成功(Payment Intent使用時)
  • payment_intent.payment_failed: 決済失敗(Payment Intent使用時)

ここでは最初の2つを使用します。

Webhook対応 Edge Functionの実装

Stripe Webhook処理用のSupabase Edge Functionを作成します。

// supabase/functions/stripe-webhook/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
import Stripe from 'https://esm.sh/stripe@14.21.0?target=deno';

const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') || '', {
  apiVersion: '2023-10-16',
});

const supabase = createClient(
  Deno.env.get('SUPABASE_URL') ?? '',
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
);

Deno.serve(async (req) => {
  const signature = req.headers.get('stripe-signature');
  const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET');

  if (!signature || !webhookSecret) {
    return new Response('Missing signature or webhook secret', { status: 400 });
  }

  try {
    const body = await req.text();
    
    // Webhook署名がStripe由来かを検証
    const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
    
    // 重複処理がないかチェック
    const { data: existingEvent } = await supabase
      .from('webhook_events')
      .select('id')
      .eq('stripe_event_id', event.id)
      .single();

    if (existingEvent) {
      return new Response('Event already processed', { status: 200 });
    }

    // イベントログ記録
    await supabase
      .from('webhook_events')
      .insert({
        stripe_event_id: event.id,
        event_type: event.type,
      });

    // イベント種別に応じて処理
    switch (event.type) {
      case 'checkout.session.completed':
        await handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session);
        break;
      
      case 'checkout.session.expired':
        await handleCheckoutSessionExpired(event.data.object as Stripe.Checkout.Session);
        break;
        
      default:
        console.log(`未処理のイベント: ${event.type}`);
    }

    // 処理完了マーク
    await supabase
      .from('webhook_events')
      .update({ processed: true })
      .eq('stripe_event_id', event.id);

    return new Response('OK', { status: 200 });
  } catch (error) {
    console.error('Webhook処理エラー:', error);
    return new Response('Webhook Error', { status: 400 });
  }
});

// 決済セッション完了処理
async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) {
  const customerId = session.customer as string;
  
  // 顧客情報取得
  const { data: customer } = await supabase
    .from('customers')
    .select('*')
    .eq('stripe_customer_id', customerId)
    .single();

  if (!customer) {
    console.error('顧客が見つかりません:', customerId);
    return;
  }

  // 単発決済の処理
  const paymentIntent = session.payment_intent as string;
  
  await supabase
    .from('payments')
    .insert({
      customer_id: customer.id,
      stripe_payment_intent_id: paymentIntent,
      amount: session.amount_total!,
      currency: session.currency!,
      status: 'completed',
    });
    
  console.log(`単発決済完了: ${paymentIntent}`);
}

// 決済セッション期限切れ処理
async function handleCheckoutSessionExpired(session: Stripe.Checkout.Session) {
  const customerId = session.customer as string;
  
  // 顧客情報取得
  const { data: customer } = await supabase
    .from('customers')
    .select('*')
    .eq('stripe_customer_id', customerId)
    .single();

  if (!customer) {
    console.error('顧客が見つかりません:', customerId);
    return;
  }

  // 期限切れログ記録(必要に応じて)
  console.log(`決済セッション期限切れ: ${session.id}`);
}

Webhook設定

Stripeダッシュボードでの設定

  1. Stripeダッシュボード開発者Webhook
  2. エンドポイントを追加をクリック
  3. エンドポイントURL: https://your-project.supabase.co/functions/v1/stripe-webhook
  4. 監視するイベント(単発決済用):
    • checkout.session.completed
    • checkout.session.expired
  5. エンドポイントを追加をクリック

Webhook Secretの取得と設定

作成したWebhookエンドポイントから署名シークレットを取得し、環境変数に設定します:

  1. 作成したWebhookエンドポイントをクリック
  2. 署名シークレット表示をクリック
  3. whsec_...で始まるシークレットをコピー
  4. Supabaseプロジェクトの環境変数に設定:
# Supabase CLI
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxx

# または Supabaseダッシュボード → 設定 → Edge Functions → Secrets

重要な注意点

  • Webhook Secretは、Webhookエンドポイントを作成した後にStripeから取得
  • テスト環境本番環境でそれぞれ別のWebhookエンドポイントが必要
  • 各環境で異なるWebhook Secretが発行される
  • 環境変数は環境に応じて適切に設定する:
    • テスト: whsec_test_...
    • 本番: whsec_live_...

セキュリティ考慮事項

  • 署名検証: 必ずWebhook署名がStripe由来であることを検証
  • 重複処理防止: 同一イベントIDの重複処理をチェック
  • エラーハンドリング: 処理失敗時のリトライ機構
  • ログ記録: 処理履歴の保存と監視

まとめ

Webhook処理により、Stripe決済とアプリケーションの状態を確実に同期できます。重複処理防止と適切なエラーハンドリングが重要です。

次章では、サブスクリプション決済の実装について説明します。


前:個人開発者のためのStripe超入門②基本実装-単発決済

Discussion