← ブログ一覧

Stripe 決済連携の実装ポイント:実務ガイド

Stripe を使った決済機能の実装方法を解説。Checkout Session・Webhook・サブスクリプション・テスト戦略まで、受託開発で即使える実践ガイド。

#API#TypeScript#Next.js#セキュリティ
Stripe 決済連携の実装ポイント:実務ガイド

Stripe 決済連携の実装ポイント:実務ガイド

Stripe は世界中で使われる決済プラットフォームですが、実装時には「どのフローを選ぶか」「Webhook をどう扱うか」「テスト環境をどう整えるか」といった判断が求められます。本記事では、受託開発・自社開発の現場で即使える Stripe 実装のベストプラクティスを、コード例とチェックリストを交えて解説します。


1. Stripe 決済フローの選定

1.1 主な決済フロー

Stripe には複数の決済フローがあり、要件に応じて使い分けます。

| フロー | 特徴 | 適用ケース | |--------|------|------------| | Checkout Session | Stripe ホスト画面で決済完了 | EC サイト、SaaS の単発決済 | | Payment Intents | 自前 UI で決済フォーム構築 | ブランド重視、カスタム UI 必須 | | Setup Intents | カード情報のみ登録(即課金なし) | サブスク開始前の登録、無料トライアル | | Subscription | 定期課金 | 月額 SaaS、年間プラン |

1.2 受託開発での選定基準

必須項目チェックリスト

  • [ ] 決済完了までの UI を自前で作る必要があるか
  • [ ] サブスクリプション(定期課金)が必要か
  • [ ] カード情報の事前登録が必要か
  • [ ] 3D セキュア(SCA)対応が必須か
  • [ ] Apple Pay / Google Pay 対応が必要か

推奨フロー

  • MVP・単発決済: Checkout Session(最短実装)
  • ブランド重視・カスタム UI: Payment Intents
  • 定期課金: Subscription + Checkout Session or Payment Intents

2. Checkout Session 実装(最短パターン)

2.1 サーバー側実装

Checkout Session は、Stripe ホスト画面にリダイレクトして決済を完了させる最もシンプルな方法です。

// app/api/checkout/route.ts (Next.js App Router)
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-11-20.acacia',
});

export async function POST(req: NextRequest) {
  try {
    const { priceId, userId } = await req.json();

    // Checkout Session 作成
    const session = await stripe.checkout.sessions.create({
      mode: 'payment', // 単発決済
      payment_method_types: ['card'],
      line_items: [
        {
          price: priceId, // Stripe ダッシュボードで作成した Price ID
          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}/payment/cancel`,
      metadata: {
        userId, // 注文と紐づける独自 ID
      },
    });

    return NextResponse.json({ url: session.url });
  } catch (error) {
    console.error('Checkout Session 作成エラー:', error);
    return NextResponse.json(
      { error: 'セッション作成に失敗しました' },
      { status: 500 }
    );
  }
}

2.2 フロントエンド実装

// components/CheckoutButton.tsx
'use client';

import { useState } from 'react';

interface Props {
  priceId: string;
  userId: string;
}

export function CheckoutButton({ priceId, userId }: Props) {
  const [loading, setLoading] = useState(false);

  const handleCheckout = async () => {
    setLoading(true);
    try {
      const res = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ priceId, userId }),
      });

      const { url } = await res.json();
      window.location.href = url; // Stripe 画面にリダイレクト
    } catch (error) {
      console.error('決済エラー:', error);
      alert('決済に失敗しました');
    } finally {
      setLoading(false);
    }
  };

  return (
    <button onClick={handleCheckout} disabled={loading}>
      {loading ? '処理中...' : '購入する'}
    </button>
  );
}

3. Webhook による決済完了通知の実装

3.1 Webhook の役割

success_url だけでは不十分な理由

  • ユーザーがブラウザを閉じると success_url に戻ってこない
  • 決済完了を確実に検知できない
  • 商品の提供や DB 更新が漏れる

Webhook で実現すること

  • Stripe → サーバーへの確実な通知
  • 決済完了後の DB 更新(注文ステータス、ユーザー権限など)
  • 冪等性の担保(同じ通知が複数回来ても安全)

3.2 Webhook エンドポイント実装

// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { headers } from 'next/headers';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-11-20.acacia',
});

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: NextRequest) {
  const body = await req.text();
  const headersList = headers();
  const signature = headersList.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    // 署名検証(必須:偽装リクエスト防止)
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
  } catch (err) {
    console.error('Webhook 署名検証エラー:', err);
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // イベントごとの処理
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      console.log('決済完了:', session.id);

      // DB 更新処理(例:注文ステータスを「完了」に変更)
      await updateOrderStatus(session.metadata?.userId, 'completed');
      break;
    }
    case 'payment_intent.payment_failed': {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      console.error('決済失敗:', paymentIntent.id);
      // エラー通知やリトライ処理
      break;
    }
    default:
      console.log(`未処理イベント: ${event.type}`);
  }

  return NextResponse.json({ received: true });
}

// ダミー関数(実際は DB 更新処理を実装)
async function updateOrderStatus(userId: string | undefined, status: string) {
  console.log(`User ${userId} のステータスを ${status} に更新`);
}

3.3 Webhook のテスト方法

ローカル開発での Webhook 受信

# Stripe CLI をインストール
brew install stripe/stripe-cli/stripe

# ログイン
stripe login

# ローカルサーバーに Webhook を転送
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# 表示される webhook secret を .env.local に設定
STRIPE_WEBHOOK_SECRET=whsec_xxxxx

# テストイベントを送信
stripe trigger checkout.session.completed

4. サブスクリプション実装

4.1 Checkout Session でサブスク開始

// mode を 'subscription' に変更
const session = await stripe.checkout.sessions.create({
  mode: 'subscription', // 定期課金
  payment_method_types: ['card'],
  line_items: [
    {
      price: 'price_monthly_plan', // 月額プランの Price ID
      quantity: 1,
    },
  ],
  success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/subscription/success`,
  cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/subscription/cancel`,
  metadata: { userId },
});

4.2 サブスク関連 Webhook イベント

| イベント | タイミング | 処理内容 | |---------|-----------|----------| | customer.subscription.created | サブスク開始 | ユーザーに権限付与 | | invoice.payment_succeeded | 課金成功(毎月) | 利用期限を延長 | | invoice.payment_failed | 課金失敗 | リトライ通知、権限停止 | | customer.subscription.deleted | サブスク解約 | 権限剥奪 |

case 'customer.subscription.created': {
  const subscription = event.data.object as Stripe.Subscription;
  await grantUserAccess(subscription.metadata.userId);
  break;
}

case 'invoice.payment_failed': {
  const invoice = event.data.object as Stripe.Invoice;
  await notifyPaymentFailure(invoice.customer as string);
  break;
}

5. テスト戦略とセキュリティ

5.1 テストカード番号

| 用途 | カード番号 | 結果 | |------|-----------|------| | 成功 | 4242 4242 4242 4242 | 決済成功 | | 3D セキュア成功 | 4000 0027 6000 3184 | 認証フロー完了 | | 残高不足 | 4000 0000 0000 9995 | declined | | カード期限切れ | 4000 0000 0000 0069 | expired_card |

5.2 セキュリティチェックリスト

必須項目

  • [ ] Webhook の署名検証を実装している
  • [ ] Secret Key を環境変数で管理(Git にコミットしない)
  • [ ] HTTPS 環境でのみ本番 API を使用
  • [ ] フロントエンドに Secret Key を含めない(Publishable Key のみ使用)
  • [ ] 決済完了の判定を Webhook で行う(success_url だけに依存しない)

推奨項目

  • [ ] Webhook エンドポイントに IP 制限を設定
  • [ ] 決済失敗時のリトライロジックを実装
  • [ ] 金額の検証(サーバー側で価格を再計算)
  • [ ] idempotency key を使った冪等性の担保
// 冪等性キーの使用例
await stripe.paymentIntents.create(
  {
    amount: 1000,
    currency: 'jpy',
    payment_method: 'pm_card_visa',
    confirm: true,
  },
  {
    idempotencyKey: `payment-${orderId}`, // 同じ注文は重複作成されない
  }
);

6. 本番環境への移行手順

6.1 環境変数の切り替え

# .env.local (開発)
STRIPE_SECRET_KEY=sk_test_xxxxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx

# .env.production (本番)
STRIPE_SECRET_KEY=sk_live_xxxxx
STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_yyyyy

6.2 本番 Webhook の設定

  1. Stripe ダッシュボード → 開発者 → Webhook
  2. 「エンドポイントを追加」
  3. URL: https://yourdomain.com/api/webhooks/stripe
  4. イベント選択: checkout.session.completed, invoice.payment_succeeded など
  5. Webhook 署名シークレットを .env.production に設定

6.3 本番切り替えチェックリスト

  • [ ] テスト環境で全フローを確認済み
  • [ ] 本番 Secret Key に切り替え
  • [ ] 本番 Webhook エンドポイントを登録
  • [ ] 決済テスト(少額の実決済で動作確認)
  • [ ] エラー通知の設定(Sentry、Slack 通知など)
  • [ ] 返金・キャンセルフローの確認

7. よくあるトラブルと対処法

7.1 Webhook が届かない

原因と対処

  • サーバーが 200 を返していない → ログで HTTP ステータス確認
  • 署名検証で失敗 → STRIPE_WEBHOOK_SECRET の設定ミス
  • HTTPS でない → Stripe は HTTPS エンドポイントのみ対応

7.2 決済完了後に success_url に戻らない

原因

  • ユーザーがブラウザを閉じた
  • リダイレクト先 URL の設定ミス

対処

  • Webhook で確実に決済完了を検知する設計にする
  • success_url は「確認画面」として扱い、本処理は Webhook で行う

7.3 テストカードで 3D セキュアが表示されない

原因

  • テストカード番号が 3D セキュア対応のものでない

対処

  • 4000 0027 6000 3184 を使用
  • Checkout Session の payment_method_options で SCA を強制
payment_method_options: {
  card: {
    request_three_d_secure: 'any', // 常に 3D セキュア表示
  },
},

まとめ

本記事では、Stripe 決済連携の実装パターンを実務視点で解説しました。

重要ポイント

  • Checkout Session が最短実装(MVP 向け)
  • Webhook で確実に決済完了を検知(success_url だけでは不十分)
  • 署名検証 は必須(セキュリティ)
  • テストカード で全フローを検証してから本番移行

Stripe の公式ドキュメントは充実していますが、実装時には「どのフローを選ぶか」「Webhook をどう扱うか」といった判断が求められます。本記事のチェックリストとコード例を参考に、安全で確実な決済機能を実装してください。


受託開発・技術支援のご相談

Yureate では、Stripe を含む決済機能の実装支援、MVP 開発、技術顧問サービスを提供しています。「決済周りの設計に不安がある」「テスト環境の構築を手伝ってほしい」といったご相談がありましたら、お気軽にお問い合わせください。

お問い合わせはこちら

この内容について相談する他の記事を見る