Claude Code × Stripe:Checkout・Webhooks実装をAIで加速する実践ガイド

はじめに

Stripeの実装で最初に詰まるのはWebhookだ。署名検証に失敗して400エラーが返ってくる。原因を調べると「生のリクエストボディを使わないといけない」と分かるが、Next.jsのbodyParserが邪魔をしていた——という経験をした人は多いはずだ。

Stripeの実装はCheckout Sessionの作成、Webhookの署名検証、イベント別の処理分岐と、決まったパターンの繰り返しが多い。Claude Codeはこの種の構造化されたコードを高精度に生成できる。ただし、秘密鍵の扱いやWebhookの実装上の注意点をCLAUDE.mdに書いておかないと、動かないコードや危険なコードが混入する。

本記事は「CLAUDE.mdに何を書けばStripe実装が安全・確実に生成されるか」を中心に、Next.js App RouterでのCheckout・Webhooks実装フローを解説する。


Stripe 3大APIの役割を整理する

Checkout:最速の決済フロー

Stripeがホストする決済ページにリダイレクトする方式だ。サーバー側でCheckout Sessionを作成してURLを返し、クライアントがリダイレクトするだけで決済が完結する。クレジットカード・Apple Pay・Google Payを自動対応し、PCI DSS準拠もStripe側が担う。

Claude Codeへの指示で「Stripe Checkoutを実装して」と伝えると、このフロー(Session作成API→クライアントリダイレクト)を生成してくれる。カスタムUIが不要な場面ではCheckoutが最も実装コストが低い。

PaymentIntent:カスタムUIを組む場合

決済フロー全体を表すオブジェクトで、Stripe Elementsと組み合わせて自社デザインの決済フォームを作る場合に使う。client_secretをフロントエンドに渡してStripe.jsで決済確認する流れになる。実装量がCheckoutより多いため、Claude Codeへの指示でCheckoutかPaymentIntentかを明示しないと、どちらを実装するか迷走することがある。

Webhooks:非同期イベントの受け口

決済完了・失敗・払い戻しなどの非同期イベントをHTTP POSTで受け取る仕組みだ。

- checkout.session.completed — Checkout決済完了

- payment_intent.succeeded — PaymentIntent決済成功

- payment_intent.payment_failed — 決済失敗

- charge.refunded — 払い戻し

署名検証(stripe-signatureヘッダー + stripe.webhooks.constructEvent())が必須で、これを正しく実装しないとイベントの偽装が可能になる。


実装前の準備:CLAUDE.mdとAPIキー管理

Stripeの秘密鍵(sk_live_.../sk_test_...)をコードにハードコードしてしまうのが最も危険なミスだ。Claude Codeはデフォルトでは環境変数の扱いを適切に判断できないケースがある。CLAUDE.mdに明記しておくことで防げる。

## セキュリティルール(Stripe)

- STRIPE_SECRET_KEY は絶対にコードに直書きしない
- 環境変数(.env.local)からのみ参照すること
- STRIPE_WEBHOOK_SECRET も同様に環境変数で管理
- .env.local を .gitignore に含める(必ず確認)
- テスト環境は sk_test_ / 本番は sk_live_ プレフィックスを使用
- 本番の sk_live_ はサーバーの環境変数のみに設定し、ローカルには置かない

## Stripe実装ルール

- Checkoutか PaymentIntentか、依頼時に明示する
- WebhookエンドポイントはNext.jsのbodyParserを使わず req.text() で生ボディを取得する
- constructEvent()で例外が発生した場合は必ず400を返す(Stripeがリトライするため)

環境変数の構成はこのようになる:

# .env.local
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxx
NEXT_PUBLIC_BASE_URL=http://localhost:3000

Next.js App Router + Stripe CheckoutをClaude Codeで実装する

CLAUDE.mdを整えたうえで、Claude Codeへの指示はこのように行う:

CLAUDE.mdの規約に従い、Next.js App RouterでStripe Checkoutのセッション作成APIを
実装してください。
- エンドポイント: POST /api/checkout
- 成功URL: /success?session_id={CHECKOUT_SESSION_ID}
- キャンセルURL: /cancel
- 通貨: JPY、金額はリクエストボディから取得

生成されるコードのポイントを確認しながら実装する:

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

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

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

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity }],
    mode: 'payment',
    success_url: ${process.env.NEXT_PUBLIC_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID},
    cancel_url: ${process.env.NEXT_PUBLIC_BASE_URL}/cancel,
  });

  return NextResponse.json({ url: session.url });
}

クライアント側はボタンから/api/checkoutにPOSTしてURLを受け取り、window.location.hrefでリダイレクトするだけだ。

Webhookハンドラの実装

Webhookの実装で重要なのが「生のリクエストボディ」を使う点だ。Next.jsのJSONパース後のボディでは署名検証が失敗する。

// app/api/webhook/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-06-20',
});

export async function POST(req: NextRequest) {
  const body = await req.text();  // JSONではなく生テキストで取得
  const headersList = await headers();  // Next.js 15以降でawaitが必要
  const sig = headersList.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook署名検証エラー:', err);
    return NextResponse.json({ error: 'Webhook Error' }, { status: 400 });
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      // 注文確定処理・メール送信・DB更新など
      console.log('決済完了:', session.id);
      break;
    }
    case 'payment_intent.payment_failed': {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      console.error('支払い失敗:', paymentIntent.last_payment_error?.message);
      break;
    }
  }

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

req.text()で生ボディを取得し、constructEvent()で署名検証する——この2点をCLAUDE.mdに書いておかないと、Claude Codeがreq.json()で実装してしまい、「ローカルでは動くのに本番で署名エラーが出る」という問題が起きる。


Stripe CLIでWebhookをローカルテストする

本番Webhookをローカルでテストするには、Stripe CLIが必要だ。Claude Codeにもセットアップを任せられる。

# インストール(macOS)
brew install stripe/stripe-cli/stripe

# ログイン
stripe login

# ローカルエンドポイントへのイベント転送
stripe listen --forward-to localhost:3000/api/webhook

# 出力例:
# Ready! Your webhook signing secret is whsec_xxxxxxxx
# → この値を .env.local の STRIPE_WEBHOOK_SECRET に設定する

stripe listenを起動したら、別ターミナルでイベントをトリガーして動作確認できる:

stripe trigger checkout.session.completed
stripe trigger payment_intent.payment_failed

実際にWebhookハンドラが呼ばれてログが出れば、署名検証も含めて正常に動作している。

Stripe CLIの存在をCLAUDE.mdに書いておくと、「Webhookのテスト方法を教えて」と聞いたときにstripe listenコマンドを含む手順を答えてくれるようになる。


まとめ

Stripe実装でClaude Codeを活用するとき、最初にやるべきことはCLAUDE.mdへのセキュリティルールと実装ルールの記述だ。「秘密鍵を環境変数で管理する」「Webhookは生ボディを使う」「constructEvent()の例外は400で返す」——これらを書いておくだけで、生成されるコードの品質が大きく変わる。

Stripe CLIによるローカルテストをワークフローに組み込むと、「コード生成→即動作確認→修正」のサイクルが短くなる。生成されたコードを実際のイベントで叩いてみて、エラーがあれば修正を依頼するという使い方が実践的だ。

セキュリティレビュー——APIキーの扱い、署名検証の抜け、イベント処理の漏れ——は必ず人間が確認する。決済に関わるコードは、自動生成で完成させるのではなく、生成されたコードを人間がレビューして仕上げるという位置づけで使うのが適切だ。

コメント

タイトルとURLをコピーしました