導入
「自分のWebサービスで月額課金を実装したい」「ワンタイム決済のフォームを設置したい」と考えたとき、多くの人がぶつかる壁が「決済システムの実装」です。決済処理にはセキュリティ・税務・カード情報の取り扱いなど、専門知識が必要に思えるからです。ですが、Stripeを使えばこれらの面倒を引き受けてくれます。さらにClaudeCodeに依頼すれば、Checkout統合・Webhook処理・サブスクリプション管理といった複雑な実装も会話ベースで進められます。本記事ではStripeの基礎から、サブスク課金の実装、Webhookセキュリティまでを初心者向けに徹底解説します。APIキーの取り扱いには特に注意が必要なので、安全な扱い方も繰り返しお伝えします。
結論
ClaudeCodeとStripeを使えば、サブスクリプション課金つきのWebアプリを1日で実装できます。Stripeは決済画面(Checkout)、顧客管理(Customer Portal)、サブスク管理、決済通知(Webhook)まで一通り揃っており、PCI DSS準拠の安全な決済を簡単に組み込めます。ClaudeCodeはStripeの公式SDKに精通しており、Checkout Sessionの作成、Webhookの署名検証、サブスクの状態管理などのコードを正確に生成します。重要なのは3点です。第一に、シークレットキー(sk_live_...)は絶対にソースコードに書かず、必ず環境変数(.envやCloudflare Secrets、Vercel環境変数)で管理すること。第二に、Webhookは必ず署名検証を行い、なりすましを防ぐこと。第三に、本番反映前に必ずStripeのテストモード(sk_test_...)とstripe listenコマンドで検証することです。これらを守れば、安全で安定した課金システムが作れます。
Stripeの基礎を理解する
Stripeはアメリカ発の決済プラットフォームで、世界中の開発者に愛用されています。日本でも法人・個人事業主が利用でき、JCB・VISA・MasterCard・Amex・Apple Pay・Google Payに対応しています。月額固定費は不要で、決済が発生した時のみ手数料(国内カードで3.6%)が引かれる従量課金モデルです。
Stripeの強みは大きく4つです。第一に、Checkoutというホスト型決済画面が用意されており、自前でカード入力フォームを作る必要がありません。これによりPCI DSS(クレジットカード業界のセキュリティ基準)への準拠が大幅に楽になります。第二に、サブスクリプション管理機能がフル装備で、定期課金・無料トライアル・プロモコード・税率計算まで対応します。第三に、Customer Portalという顧客自身が支払い方法やプランを変更できる画面を、たった数行のコードで埋め込めます。第四に、Webhookでイベント(支払い成功、解約、失敗)を受け取り、自動的に処理を回せます。
ClaudeCodeと組み合わせると、ドキュメントの読解・SDKの選定・実装・テストまでが圧倒的に高速化します。Stripe Docsは英語が主軸で量も多いですが、ClaudeCodeに「Stripe Subscriptionの公式ベストプラクティスで実装して」と指示すれば、適切な手順を提示してくれます。
ClaudeCodeでStripeアカウントとAPIキーを準備する
最初の準備はStripeアカウントの作成です。stripe.comで登録し、ダッシュボードにログインします。ダッシュボード右上にはテストモード/本番モードのトグルがあります。開発中は必ずテストモードで作業しましょう。テストモードでは実際のカードが請求されず、テスト用カード番号(4242 4242 4242 4242)で決済シミュレーションができます。
ダッシュボードの「開発者」→「APIキー」から2種類のキーが取得できます。
- 公開可能キー(
pk_test_.../pk_live_...): クライアント側のJSに埋め込んで構いません - シークレットキー(
sk_test_.../sk_live_...): 絶対に公開してはいけません
シークレットキーがあれば、Stripe上のすべてのデータを読み書きできてしまいます。ソースコードに直接書く、GitHubにコミットする、Slackで共有するは厳禁です。 必ず.envファイルに記述し、.gitignoreに.envを追加してください。本番環境では、Cloudflare WorkersのSecrets、Vercel Environment Variables、AWS Secrets Managerなどに格納します。
# .env.local(絶対にコミットしない)
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxx
ClaudeCodeに依頼するときは、最初に次のように伝えておきましょう。
プロンプト例1: 初期セットアップ宣言
これから Stripe を Next.js(App Router、TypeScript)プロジェクトに統合します。
ルール:
- シークレットキーは絶対にソースコードに直接書かない
- 必ず process.env.STRIPE_SECRET_KEY 経由で参照する
- クライアントコードからは公開可能キーのみ使う
- .gitignore に .env, .env.local を追加する
これを守って実装してください。
このように最初に「セキュリティ要件」を宣言しておくと、ClaudeCodeはその後の実装すべてでこのルールを守ってくれます。
Stripe Checkoutでワンタイム決済を実装する
まずはサブスクの前に、シンプルなワンタイム決済から始めます。Checkout SessionはStripeがホストする決済画面で、ユーザーはこの画面でカード情報を入力します。あなたのサーバーがカード番号を扱うことはないので、セキュリティ上きわめて安全です。
プロンプト例2: ワンタイム決済の実装
Next.js App Router で、5000円の商品を Stripe Checkout で決済できるエンドポイントとボタンを実装してください。
- POST /api/checkout で Checkout Session を作成
- 成功時に /success?session_id={CHECKOUT_SESSION_ID} に戻る
- キャンセル時に /cancel に戻る
- 通貨は JPY
- TypeScript
- stripe ライブラリは npm の公式 stripe を使う
ClaudeCodeは以下のような実装を返します。
// app/api/checkout/route.ts
import Stripe from "stripe";
import { NextResponse } from "next/server";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-11-20.acacia",
});
export async function POST() {
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [
{
price_data: {
currency: "jpy",
product_data: { name: "プレミアム1回購入" },
unit_amount: 5000,
},
quantity: 1,
},
],
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 });
}
// app/page.tsx の一部
"use client";
const handleCheckout = async () => {
const res = await fetch("/api/checkout", { method: "POST" });
const { url } = await res.json();
window.location.href = url;
};
これでボタンを押すとStripeの決済画面に遷移し、決済完了後にあなたのサイトに戻ってきます。テストカード4242 4242 4242 4242(CVCは任意の3桁、有効期限は未来の日付)で動作確認しましょう。
Stripe Subscriptionでサブスク課金を実装する
サブスクリプションを実装するには、まずStripeダッシュボードで**商品と価格(Product & Price)**を作成します。「製品カタログ」→「製品を追加」で、たとえば「Proプラン」を作り、価格を月額1000円(JPY)の定期支払いに設定します。作成するとprice_xxxxというIDが発行されます。
プロンプト例3: サブスク実装
Stripe Subscription を実装してください。
- Price ID は環境変数 STRIPE_PRICE_PRO_MONTHLY に格納
- POST /api/subscribe で Checkout Session(mode=subscription)を作成
- Firebase Authentication でログイン済みユーザーのみ
- 顧客の Stripe Customer ID を Firestore の users/{uid} に保存して使い回す
- 既存 Customer がいれば再利用、なければ新規作成
実装例:
// app/api/subscribe/route.ts
import Stripe from "stripe";
import { NextResponse } from "next/server";
import { adminDb } from "@/lib/firebaseAdmin";
import { verifyIdToken } from "@/lib/auth";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-11-20.acacia",
});
export async function POST(req: Request) {
const { uid, email } = await verifyIdToken(req);
const userRef = adminDb.collection("users").doc(uid);
const snap = await userRef.get();
let customerId = snap.data()?.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({ email, metadata: { uid } });
customerId = customer.id;
await userRef.set({ stripeCustomerId: customerId }, { merge: true });
}
const session = await stripe.checkout.sessions.create({
mode: "subscription",
customer: customerId,
line_items: [{ price: process.env.STRIPE_PRICE_PRO_MONTHLY!, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?status=success`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
});
return NextResponse.json({ url: session.url });
}
ポイントは「Customer IDを使い回す」ことです。ユーザーがアプリ内で2回目の支払いをしたとき、毎回新しいCustomerを作ってしまうと、顧客データが分散して管理不能になります。必ず初回作成時にFirestoreなどに保存し、以降は同じIDを使います。
Webhookで決済イベントを受け取る
Stripeで支払いが成功・失敗したとき、サブスクが更新・解約されたときなど、サーバー側で処理を回したいケースは必ず発生します。これを実現するのがWebhookです。Stripeから自分のサーバーへ「これが起きました」というHTTPリクエストが届く仕組みです。
プロンプト例4: Webhook実装
Stripe Webhook を /api/webhook(Next.js App Router)に実装してください。
- 署名検証を必ず行う(環境変数 STRIPE_WEBHOOK_SECRET)
- 受け取るイベント: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed
- 受信したら Firestore の users/{uid} の subscriptionStatus を更新
- ボディは raw bytes で受け取る必要がある(Next.js では route segment config に注意)
実装例:
// app/api/webhook/route.ts
import Stripe from "stripe";
import { NextResponse } from "next/server";
import { adminDb } from "@/lib/firebaseAdmin";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-11-20.acacia",
});
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(req: Request) {
const sig = req.headers.get("stripe-signature");
const body = await req.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig!, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err: any) {
return new NextResponse(`Webhook Error: ${err.message}`, { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const customerId = session.customer as string;
await updateUserByCustomer(customerId, { subscriptionStatus: "active" });
break;
}
case "customer.subscription.updated": {
const sub = event.data.object as Stripe.Subscription;
await updateUserByCustomer(sub.customer as string, { subscriptionStatus: sub.status });
break;
}
case "customer.subscription.deleted": {
const sub = event.data.object as Stripe.Subscription;
await updateUserByCustomer(sub.customer as string, { subscriptionStatus: "canceled" });
break;
}
case "invoice.payment_failed": {
const inv = event.data.object as Stripe.Invoice;
await updateUserByCustomer(inv.customer as string, { subscriptionStatus: "past_due" });
break;
}
}
return NextResponse.json({ received: true });
}
async function updateUserByCustomer(customerId: string, data: any) {
const q = await adminDb.collection("users").where("stripeCustomerId", "==", customerId).get();
q.forEach((doc) => doc.ref.set(data, { merge: true }));
}
Webhookのテストはstripe listenコマンドで行います。ローカルでサーバーを起動し、別ターミナルで次を実行します。
stripe listen --forward-to localhost:3000/api/webhook
すると擬似的なWebhookシークレットが発行され、ターミナルにイベントログが流れます。stripe trigger checkout.session.completedで任意のイベントを発火できるので、本番リリース前の徹底テストが可能です。
Webhookセキュリティの徹底
Webhookは「外部から自由にPOSTできるエンドポイント」なので、悪意のある攻撃者がなりすましリクエストを送る可能性があります。これを防ぐのが**署名検証(Signature Verification)**です。Stripeはリクエストにstripe-signatureヘッダーを付与しており、stripe.webhooks.constructEventに渡すことで「これは本当にStripeから来たリクエストか」を検証できます。
セキュリティの重要ポイントは次の通りです。
第一に、必ず署名検証を行うこと。検証を省くと、誰でも偽のイベントを送れてしまい、「支払いが成功した」と偽った状態でユーザーにアクセスを与えてしまいます。
第二に、Webhookエンドポイントは認証不要にすること。逆説的ですが、Stripeからのリクエストには認証ヘッダーがないため、認証ミドルウェアでブロックしないよう注意します。Cloudflare Accessを使う場合は、Webhook用のパスを除外設定にします。
第三に、リプレイ攻撃対策。Stripeは署名にタイムスタンプを含めており、constructEventが古いリクエスト(デフォルトで5分以上前)を拒否します。
第四に、STRIPE_WEBHOOK_SECRETはテストモードと本番モードで異なります。デプロイ環境ごとに適切な値を環境変数に設定しましょう。
プロンプト例5: セキュリティチェック依頼
私の /api/webhook の実装をセキュリティの観点でレビューしてください。
- 署名検証は適切か
- リプレイ攻撃対策は十分か
- エラー時のレスポンスから攻撃者に情報が漏れていないか
- 本番とテストでシークレットが切り替わるか
ファイルの内容は次の通りです:
(コードを貼り付け)
ClaudeCodeはコードを読んで、抜け漏れがあれば具体的な修正案を提示してくれます。
顧客が自分でプラン管理できるCustomer Portal
ユーザーがプラン変更・解約・支払い方法変更を行う画面を自前で作るのは大変ですが、StripeにはCustomer Portalという標準UIが用意されています。たった1つのAPI呼び出しで、ユーザー専用のポータルURLを生成できます。
// app/api/portal/route.ts
import Stripe from "stripe";
import { NextResponse } from "next/server";
import { getCustomerIdForUser } from "@/lib/users";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-11-20.acacia",
});
export async function POST(req: Request) {
const customerId = await getCustomerIdForUser(req);
const portal = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard`,
});
return NextResponse.json({ url: portal.url });
}
ダッシュボードに「プラン管理」ボタンを置き、押すとPortalに遷移します。これでユーザー対応の問い合わせが大幅に減ります。
本番反映前のチェックリスト
リリース前に必ず確認しましょう。
- すべての環境変数(
STRIPE_SECRET_KEY、STRIPE_WEBHOOK_SECRET)が本番モードの値に切り替わっているか - Webhookエンドポイントが本番URL用にStripeダッシュボードで登録済みか
- テストモードで全パターン(成功・失敗・解約・更新)の動作を確認したか
- 失敗時のメール送信やログ出力が機能しているか
- 利用規約・特定商取引法表示・プライバシーポリシーがサイトに掲載されているか
- Stripeダッシュボードでビジネス情報・銀行口座の登録が完了しているか
特に6番を忘れると、決済はできても入金が遅れる原因になります。
実践チュートリアル: 月額1,000円のSaaSを作る
ここまでの内容を統合し、ログインユーザーが月額1,000円のサブスクに加入できるSaaSを作ります。
ステップ1. Stripeダッシュボードで商品「Proプラン」を作成し、月額1,000円の価格を発行。price_xxxをメモ。
ステップ2. Next.jsプロジェクトに Firebase Authentication と Stripe SDK を導入。
ステップ3. ClaudeCodeにまとめて依頼:
次の仕様で SaaS を実装してください。
1. Firebase Authentication(メール/Google)
2. ログイン後 /dashboard で現在のプラン状況を表示
3. 未加入なら「Proに加入」ボタン → /api/subscribe で Checkout Session
4. /api/webhook で subscriptionStatus を Firestore に同期
5. 加入済みなら「プラン管理」ボタン → Customer Portal
6. すべての秘密鍵は環境変数で管理
7. TypeScript + App Router
ステップ4. stripe listenでWebhookをテストし、4242 4242 4242 4242で支払いシミュレート。
ステップ5. Vercel または Cloudflare Workers にデプロイし、本番モードのキーをSecretsに登録。
ステップ6. 本番Webhookエンドポイントを登録(ダッシュボード→開発者→Webhooks)。
これで月額課金つきSaaSのMVPが完成します。
FAQ
Q1. シークレットキーを誤ってGitHubにコミットしてしまったらどうすればいいですか?
A. 直ちにStripeダッシュボードの「APIキー」から該当キーをローテーション(無効化+再発行)します。コミット履歴からの削除(git filter-repo等)は不完全なので、キーローテーションを最優先で行ってください。新しいキーを各環境のSecretsに反映し直します。
Q2. テストモードと本番モードはどう切り替えますか? A. Stripeダッシュボード右上のトグルで切り替えます。各モードでAPIキー・Webhookシークレット・Price IDがすべて別物になるので、環境変数の管理を厳格にしましょう。Vercelなら「Production」と「Preview/Development」で別の値を設定するのが定石です。
Q3. Webhookが届かないときは何を疑えばいいですか?
A. まずStripeダッシュボードの「開発者」→「Webhooks」→該当エンドポイントの「試行」タブで配送ログを確認します。401や404が出ていればエンドポイントURLや認証設定の問題、500ならサーバーコードの問題です。ローカルならstripe listenが起動しているか確認してください。
Q4. JPY決済で気をつけることはありますか?
A. 日本円は「最小通貨単位」が1円なので、unit_amountはそのまま金額を入れます(USDなら1ドル=100です)。また、消費税の自動計算はStripe Taxを有効化すれば対応できますが、別途審査が必要です。
Q5. ユーザーがクレジットカードを変更したいと言ってきたら? A. Customer Portalを案内すれば、ユーザー自身が支払い方法を変更できます。自前で実装する必要はありません。
Q6. プラン変更(アップグレード/ダウングレード)はどう処理しますか?
A. Customer Portalで処理させるのが最も簡単です。プログラム的に行う場合はstripe.subscriptions.updateでitems[0].priceを差し替えますが、按分計算(プロレーション)の設定に注意が必要です。
Q7. 返金処理はどうしますか?
A. Stripeダッシュボードから手動で返金できます。プログラム的にはstripe.refunds.createを使います。返金時にはcustomer.subscription.deletedまたはcharge.refundedのWebhookが発火するので、それを受けてアプリ側のステータスを更新します。
まとめ
ClaudeCodeとStripeを使えば、サブスクリプション課金つきWebアプリを1日で構築できます。本記事ではCheckout統合、Subscription実装、Webhook処理、署名検証、Customer Portalまでをカバーしました。最重要事項はシークレットキーを環境変数で管理することとWebhookで必ず署名検証を行うことの2点です。これらを守れば、安全な課金システムが作れます。ClaudeCodeに依頼する際は、最初に「セキュリティ要件」を宣言してから実装に入ると、生成コードの質が大きく上がります。テストモードで徹底的に検証し、本番反映時には環境変数の切り替え漏れがないかチェックリストで確認しましょう。