Claude Code で Stripe 決済を組み込む
Claude Code を使って Next.js アプリに Stripe Checkout、webhook、カスタマーポータルを繋ぎ込みます。最初のプロンプトから本番決済まで 1 セッションで。
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。
問題:Stripe の連携には 7 つの可動部品があります。checkout、webhook、カスタマーポータル、環境変数、テストキー、本番キー、そして CLI です。たいていのチュートリアルはそのうち 3 つしか扱わず、残りは真夜中にデバッグさせられる羽目になります。
手っ取り早い解決策:Stripe MCP サーバーを 1 コマンドで Claude Code に追加すれば、ビルドしながら Claude がリアルタイムで顧客・価格・サブスクリプションを調べられます:
claude mcp add --transport http stripe https://mcp.stripe.com/この 1 行で、Claude にあなたの Stripe アカウントへの直接アクセスを与えられます。ライブデータを読み、製品が存在することを確認し、設定ミスが webhook ハンドラに届く前に捕まえられます。
始める前に必要なもの
3 つそろえておく必要があります。
Stripe アカウント に、製品と価格を少なくとも 1 つ作っておきます。MCP サーバー経由で、Dashboard に触れずに Claude に作らせることもできます。MCP が接続されていれば、「$29/月 の継続価格で Pro Plan という製品を作って」というプロンプトで十分です。
Claude Code に Stripe MCP を接続したもの(上記のコマンド)。代替手段は Stripe のベストプラクティス skill で、これはセッションに Stripe のドキュメントを注入します:
npx skills add https://docs.stripe.com --yesアクティブな開発には MCP サーバーのほうが良い選択です。Claude はセッションの途中でこれを呼び出し、価格を一覧したり、メールで顧客を検索したり、サブスクリプションの現在のステータスを取得したり、webhook シークレットが Dashboard の表示と一致するか確認したりできます。skill が与えるのはドキュメントのコンテキストだけです。アカウントへの問い合わせはしません。
この 2 つは排他的ではありません。両方を走らせるチームもあります。ライブの問い合わせには MCP、ベストプラクティスの指針には skill、という具合です。制限付き API キー(rk_test_...)を使った MCP は、テストモードではより安全な構成です。制限付きキーは Claude が触れる Stripe オブジェクトを限定するからです。
4 つの環境変数 を .env.local ファイルに:
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_URL=http://localhost:3000STRIPE_SECRET_KEY はクライアントには絶対に触れさせません。NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY は公開して問題ありません。STRIPE_WEBHOOK_SECRET は、ローカルのイベント転送を開始したときに Stripe CLI から得られます(テストの章で扱います)。NEXT_PUBLIC_URL は success_url と return_url のパラメータに使われます。
Checkout Sessions 対 Payment Intents
決済を受け取れる Stripe の API は 2 つあります。間違ったほうを選ぶと、何週間もの作業が上乗せされます。
Checkout Sessions(stripe.checkout.sessions.create())は、ユーザーを Stripe ホストのページ、またはあなたのドメイン上の埋め込みフォームへリダイレクトします。Stripe がカード入力、3D セキュア、Apple Pay、通貨表示、税金を扱います。たいていの SaaS 製品では、これが正しい選択です。一度きりの購入もサブスクリプションも、どちらもこれで動きます。
Payment Intents(stripe.paymentIntents.create())はより低レベルです。Stripe Elements で自分でフォームを作り、カードデータを集め、API を呼びます。コントロールは増えますが、コードも増え、メンテナンスも増えます。ホストされたページでは実現できない完全カスタムの checkout UI が必要なときだけ使ってください。
2026 年の Checkout Sessions の推奨パターンは Embedded Checkout です。ui_mode: 'embedded' はユーザーをあなたのドメインに留め、@stripe/react-stripe-js の EmbeddedCheckoutProvider と EmbeddedCheckout コンポーネントを使い、あなたが設定した return_url に戻します。Hosted Checkout(stripe.com へのリダイレクト)も依然として動きますが、Stripe が新しい連携で勧めているのは Embedded です。
Checkout フローを作る
Claude にルートの雛形を作らせ、それからリリース前に mode パラメータを確認します。
ここで Claude に与えると良いプロンプトはこうです:「app/api/checkout/route.ts に Stripe Checkout Sessions のルートを作って。一度きりの購入には mode: payment を使い、セッション URL を JSON で返し、すべての URL は環境変数から読んで」。
一度きりの購入なら、ルートはこうなります:
// app/api/checkout/route.ts
import { 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(request: Request) {
const { priceId } = await request.json()
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
})
return NextResponse.json({ url: session.url })
}サブスクリプションなら、変えるのはちょうど 1 か所です。mode: 'payment' の代わりに mode: 'subscription' を設定します。priceId は Stripe の継続価格(一度きりの価格ではなく)を指している必要があります。ルートのそれ以外はまったく同じです。
Embedded Checkout に切り替えるには、ui_mode: 'embedded' を加え、success_url を return_url に置き換えます。レスポンスは session.url の代わりに session.client_secret を返し、これをフロントエンドで EmbeddedCheckoutProvider が使います。
Webhook:みんなが間違える部分
webhook は Stripe があなたのアプリに返事をする場所です。プロビジョニングの判断(アクセスの付与、剥奪、レシートの送信)はすべて、checkout 成功時のリダイレクトではなく webhook ハンドラに置くべきです。ユーザーはタブを閉じたり、戻るボタンを押したりします。リダイレクトは信頼できません。webhook は必ず発火します。
Next.js App Router における致命的な問題は、生のボディのパースです。Stripe の署名検証(stripe.webhooks.constructEvent)は、生のリクエストボディを文字列として必要とします。App Router はハンドラが見る前にボディをパースしてしまいます。request.json() を呼ぶと、ストリームが消費され、検証は毎回失敗します。
Claude に正確な制約を与えてください:「app/api/stripe-webhook/route.ts に Stripe の webhook ハンドラを作って。生のボディには request.json() ではなく request.text() を使い、イベントを処理する前に署名を検証して」。
代わりに request.text() を使います:
// app/api/stripe-webhook/route.ts
import { 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(request: Request) {
const rawBody = await request.text()
const sig = request.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return NextResponse.json(
{ error: 'Webhook signature verification failed' },
{ status: 400 }
)
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
// Provision access, send welcome email
await grantAccess(session.customer as string)
break
}
case 'customer.subscription.updated': {
const sub = event.data.object as Stripe.Subscription
// Update tier in DB
await updateSubscription(sub.customer as string, sub.status)
break
}
case 'customer.subscription.deleted': {
const sub = event.data.object as Stripe.Subscription
// Revoke access
await revokeAccess(sub.customer as string)
break
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice
// Send dunning email, flag in DB
await handleFailedPayment(invoice.customer as string)
break
}
}
return NextResponse.json({ received: true })
}最低限ハンドリングすべき 4 つのイベントは下の表のとおりです。残りは課金ロジックが育つにつれて追加できます。
| イベント | いつ発火するか | アクション |
|---|---|---|
checkout.session.completed | 決済またはサブスクリプション開始 | アクセスを付与、ウェルカムメール送信 |
customer.subscription.updated | プラン変更、更新、トライアル終了 | DB のティアを更新 |
customer.subscription.deleted | キャンセル | アクセスを剥奪 |
invoice.payment_failed | 継続課金の失敗 | 督促メール送信、DB にフラグ |
もう 1 つ。このルートは、ボディを読む Next.js のミドルウェアからは除外しなければなりませんし、アプリの認証チェックで包んではいけません。Stripe は未認証のリクエストとしてここに POST します。署名検証だけで保護してください。
カスタマーポータルのセットアップ
カスタマーポータルは、ユーザーが自分でサブスクリプションを管理できるようにします。キャンセル、支払い情報の更新、請求書の閲覧です。1 つのステップがほとんどの開発者をつまずかせます。
ポータルのセッションを作る前に、Stripe Dashboard でポータルを設定しておかなければなりません。 保存された設定がないと、billingPortal.sessions.create() の呼び出しはすべてエラーを返します。テストモードの設定 URL は https://dashboard.stripe.com/test/settings/billing/portal です。本番モードには別の URL https://dashboard.stripe.com/settings/billing/portal があります。どちらもデプロイ前に設定が必要です。
設定のステップは、ユーザーに許可するアクションを決める場所です。サブスクリプションのキャンセル、支払い方法の更新、プランの切り替え、請求書のダウンロードなどです。Dashboard でこれらのオプションを設定し、設定を保存すれば、API 呼び出しが動くようになります。
設定さえ済めば、セッションを作るルートは短いものです:
// app/api/portal/route.ts
import { 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(request: Request) {
const { customerId } = await request.json()
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_URL}/account`,
})
return NextResponse.json({ url: session.url })
}フロントエンドは session.url にリダイレクトします。ユーザーは自分の Stripe 課金ページを見て、変更を行い、終わったら /account に戻ります。変更は customer.subscription.updated または customer.subscription.deleted イベントとして発火し、これはあなたの webhook ハンドラがすでにカバーしています。
注意:テストモードと本番モードのポータル設定は完全に別物です。テストモードでポータルを設定しても、本番モードのセットアップにはなりません。本番に出す前に、両方の環境で同じ設定手順を踏んでください。
フロー全体をテストする
Stripe CLI は、Stripe からローカルサーバーへライブのイベントを転送します。決済をテストする前に、別のターミナルでこれを実行してください:
stripe listen --forward-to localhost:3000/api/stripe-webhookCLI は起動時に webhook 署名シークレットを表示します。その値が .env.local の STRIPE_WEBHOOK_SECRET に入ります。リスナーを再起動するたびに変わるので、ハードコードしないでください。
決済フォームをクリックして通さずに特定のイベントを発火させるには、stripe trigger を使います:
stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted
stripe trigger invoice.payment_failedシナリオ別のテストカード:
| シナリオ | カード番号 |
|---|---|
| 決済成功 | 4242 4242 4242 4242 |
| 拒否 | 4000 0000 0000 0002 |
| 3D セキュアが必要 | 4000 0025 0000 3155 |
| 残高不足 | 4000 0000 0000 9995 |
有効期限は任意の未来の日付、CVC は任意の 3 桁、ZIP コードも任意で構いません。結果を変えるのはカード番号だけです。
推奨のテスト手順はこうです。CLI のリスナーを起動し、checkout ページを開き、カード 4242 4242 4242 4242 で決済を完了し、checkout.session.completed イベントがターミナルに届くのを見て、ハンドラが走ったことを確認します(DB かログをチェック)。次に stripe trigger invoice.payment_failed を発火させ、実際の決済失敗を待たずに督促のパスが動くことを確認します。3D セキュアのカード(4000 0025 0000 3155)は最後に扱ってください。Stripe は認証ステップへリダイレクトし、それをフロントエンドが処理しなければなりません。リダイレクトが静かに失敗すると、セッションは完了せず、webhook も発火しません。
本番移行チェックリスト
テストモードから本番モードへの切り替えは、キー 1 つの差し替えではなく 4 ステップです。
第一に、本番環境の環境変数で、テストキー(sk_test_、pk_test_)を本番キー(sk_live_、pk_live_)に置き換えます。
第二に、https://dashboard.stripe.com/settings/billing/portal で本番モードのカスタマーポータルを設定します。テストモードと本番モードのポータル設定は完全に別物です。テストモードで設定したものは引き継がれません。
第三に、Stripe Dashboard で webhook エンドポイントを本番 URL に設定します。Developers > Webhooks > Add endpoint に進み、https://yourdomain.com/api/stripe-webhook を指定します。本番の webhook 署名シークレットをコピーし、本番環境の STRIPE_WEBHOOK_SECRET に設定します。
第四に、価格 ID を確認します。テストモードの価格(price_test_...)は本番モードには存在しません。ハードコードされた価格 ID は、本番の価格 ID に更新するか、環境変数から読むようにする必要があります。
連携を完了とする前に、実際のカードで本物の決済を 1 回行い(その後で返金して)ください。CLI のトリガーはハンドラのテストには良いものですが、実際の課金ほどフロー全体を確かめてくれるものはありません。
Build This Now が初めから組み込んで出荷するもの
このフローを書いてデバッグするのは、たいていの開発者にとって丸一日かかります。Stripe の checkout エッジ関数、webhook ハンドラ、カスタマーポータルのルート、そして 4 つの環境変数はすべて、Build This Now で初日から事前に配線されています。連携は Supabase Edge Functions にデプロイされ、テスト済みで、あなたが最初のカスタム機能を書く前から動いています。
Stripe は難しい部分ではありません。すべてのピースを正しい順番で繋ぐことが難しいのです。一度やってしまえば、どこに落とし穴があるかわかります。
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。