はじめに
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:3000Next.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キーの扱い、署名検証の抜け、イベント処理の漏れ——は必ず人間が確認する。決済に関わるコードは、自動生成で完成させるのではなく、生成されたコードを人間がレビューして仕上げるという位置づけで使うのが適切だ。

コメント