CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-05-03上級

Claude API × Stripe で月額課金SaaSを構築する実装ガイド — 2026年版

Claude API・Stripe・Cloudflare Workers を組み合わせて月額課金SaaSを実装する手順を、Webhook・KV・ペイウォール制御まで本番環境で動く形でまとめた完全実装ガイドです。

Claude API115Stripe15Cloudflare Workers14SaaS14実装

前編のロードマップで書いた通り、個人開発SaaSの収益化は「絞り込み」と「コスト構造」がほぼ全てです。本記事はその続編として、Claude API・Stripe・Cloudflare Workersを組み合わせた月額課金SaaSの完全な実装ガイドをお届けします。

私自身、4つのAI技術ブログ(Claude Lab・Gemini Lab・Antigravity Lab・Rork Lab)でこの構成を実運用しており、解約処理・Webhook失敗・エッジキャッシュとの戦いまで含めて、個人開発で詰まりがちな箇所をかなり踏み抜いてきました。本記事はその実戦経験を体系化したものです。

コードはNext.js 16 (App Router) + Cloudflare Workers + Stripe v15 を前提にしています。一部はOpenNextが必要ですが、ロジックは他フレームワークでも応用可能です。

前提構成と全体アーキテクチャ

最終的に作るのは、次のような構成です。

  • ユーザー認証: NextAuth.js または Clerk(ここではNextAuthを使用)
  • 決済: Stripe Checkout(サブスクリプション)+ Stripe Customer Portal
  • アクセス制御: Stripe Webhook → Cloudflare KV にアクティブ化フラグ書き込み
  • API実行層: Cloudflare Workers + Anthropic SDK
  • ペイウォール: ミドルウェア層でKVを参照し、未契約ユーザーをブロック

KVをアクセス制御の真実の源(source of truth)とするのが個人開発で最も軽量です。Stripe APIに毎リクエスト問い合わせる構成は、レートリミットの観点でもレイテンシの観点でも筋が悪いです。Webhookで状態変化があった瞬間にKVを更新し、API実行時はKVだけ見る、というルールに統一します。

ステップ1: Stripe側のプロダクトと価格を設計する

Stripe Dashboardで「プロダクト」を作成し、月額プランの価格を登録します。私は通常、次のように構成します。

  • Pro月額(日本円): 商品名「Pro」、価格 ¥580/月、recurring=monthly、price_id を STRIPE_PRICE_PRO_JPY として保管
  • Pro月額(USD): 商品名「Pro」、価格 $5/月、price_id を STRIPE_PRICE_PRO_USD として保管
  • Premium買い切り(JPY/USD): ¥2,480 / $15、price_id を STRIPE_PRICE_PREMIUM_* として保管

通貨ごとにプロダクトを分けるのは、Stripe Checkoutに表示される商品名・説明文をロケール別に出し分けるためです(参照: STUMBLING_POINTS #79)。ロケール混在の1商品で済ませようとすると、英語ユーザーに日本語の説明文が出てしまうという事故が起きます。

価格IDは Cloudflare Workers の環境変数として wrangler.toml に登録します。秘密鍵(STRIPE_SECRET_KEY STRIPE_WEBHOOK_SECRET)は wrangler secret put で別管理にしてください。

ステップ2: Checkoutセッション生成APIを実装する

Next.js App Routerでは app/api/checkout/route.ts にPOSTハンドラを置きます。下のコードはサブスクリプション + 単発購入(記事買い切り)を1つのエンドポイントで扱う例です。

import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { getPriceIds, getProductData } from '@/config/pricing';
 
export const runtime = 'edge';
 
export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.email) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
  }
 
  const { plan, locale, articleSlug } = await req.json();
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2025-10-31',
  });
 
  const priceIds = getPriceIds(locale);
  const product = getProductData(plan, locale);
 
  const params: Stripe.Checkout.SessionCreateParams = {
    mode: plan === 'pro' ? 'subscription' : 'payment',
    success_url: `${process.env.SITE_URL}/${locale}/thanks?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.SITE_URL}/${locale}/membership`,
    customer_email: session.user.email,
    line_items: [{
      price_data: {
        currency: locale === 'ja' ? 'jpy' : 'usd',
        recurring: plan === 'pro' ? { interval: 'month' } : undefined,
        unit_amount: product.amount,
        product_data: {
          name: product.name,
          description: product.description,
          images: [product.imageUrl],
        },
      },
      quantity: 1,
    }],
    metadata: {
      plan_type: plan,
      user_email: session.user.email,
      article_slug: articleSlug ?? '',
    },
  };
 
  const checkoutSession = await stripe.checkout.sessions.create(params);
  return NextResponse.json({ url: checkoutSession.url });
}

ここで重要なのは metadata.plan_type を必ず付けることです。Webhookで受け取ったときに、サブスク契約と単発購入を区別する手がかりになります。プレミアム永久購入が「チップ」と取り違えられるバグは個人開発で最も多い事故の1つです(STUMBLING_POINTS #14.5)。

ステップ3: Stripe Webhookを処理する

Webhookは個人開発SaaSの心臓部です。決済完了 → KV書き込み の流れをここで完結させます。

// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getCloudflareContext } from '@opennextjs/cloudflare';
 
export const runtime = 'edge';
 
export async function POST(req: NextRequest) {
  const sig = req.headers.get('stripe-signature');
  const body = await req.text();
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2025-10-31',
  });
 
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig!,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (e) {
    return NextResponse.json({ error: 'invalid signature' }, { status: 400 });
  }
 
  const { env } = getCloudflareContext();
  const kv = env.MEMBERSHIP_KV;
 
  switch (event.type) {
    case 'checkout.session.completed': {
      const s = event.data.object as Stripe.Checkout.Session;
      const planType = s.metadata?.plan_type;
      const email = s.customer_email ?? s.metadata?.user_email;
      if (!email) break;
 
      if (planType === 'pro') {
        // 月額契約: 契約期限まで有効
        const subscriptionId = s.subscription as string;
        const subscription = await stripe.subscriptions.retrieve(subscriptionId);
        await kv.put(
          `site:claudelab:member:${email}`,
          JSON.stringify({
            plan: 'pro',
            current_period_end: subscription.current_period_end,
            subscription_id: subscriptionId,
          }),
          { expirationTtl: subscription.current_period_end - Math.floor(Date.now() / 1000) + 86400 * 3 }
        );
      } else if (planType === 'premium') {
        // 永久プラン: 10年TTLで実質永続
        await kv.put(
          `site:claudelab:member:${email}`,
          JSON.stringify({ plan: 'premium' }),
          { expirationTtl: 86400 * 365 * 10 }
        );
      } else if (planType === 'article') {
        // 記事単体購入: 既存スラッグリストに追加
        const slug = s.metadata?.article_slug;
        if (slug) {
          const key = `site:claudelab:article:${email}:${slug}`;
          await kv.put(key, '1', { expirationTtl: 86400 * 365 * 10 });
        }
      }
      // tip は何もしない(記録のみで権限は付与しない)
      break;
    }
    case 'customer.subscription.deleted': {
      const sub = event.data.object as Stripe.Subscription;
      const customer = await stripe.customers.retrieve(sub.customer as string);
      if ('email' in customer && customer.email) {
        await kv.delete(`site:claudelab:member:${customer.email}`);
      }
      break;
    }
  }
  return NextResponse.json({ received: true });
}

特に押さえるべきは expirationTtl の設計です。月額プランは「次回更新日 + 3日のバッファ」をTTLにすることで、Webhookの遅延や失敗時にも自動的に失効する保険になります。永続プランは10年と長く取りますが、これは「実質永久」を表現するための値です。

ステップ4: ペイウォール側でKVを参照する

ユーザーがプレミアム機能を呼び出したとき、KVを引いてアクセスを判定します。Next.jsのRoute HandlerやServer Componentで使う共通関数として lib/premium.ts に切り出すのが整理しやすいです。

import { getCloudflareContext } from '@opennextjs/cloudflare';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
 
export async function canViewPremium(): Promise<boolean> {
  const session = await getServerSession(authOptions);
  if (!session?.user?.email) return false;
  try {
    const { env } = getCloudflareContext();
    const raw = await env.MEMBERSHIP_KV.get(`site:claudelab:member:${session.user.email}`);
    if (!raw) return false;
    const data = JSON.parse(raw);
    return data.plan === 'pro' || data.plan === 'premium';
  } catch (e) {
    // KVアクセス不可時はdeny by default(重要)
    return false;
  }
}
 
export async function getArticleAccess(slug: string): Promise<boolean> {
  const session = await getServerSession(authOptions);
  if (!session?.user?.email) return false;
  try {
    const { env } = getCloudflareContext();
    const raw = await env.MEMBERSHIP_KV.get(
      `site:claudelab:article:${session.user.email}:${slug}`
    );
    return raw === '1';
  } catch (e) {
    return false; // deny by default
  }
}

return false がフォールバック挙動なのは絶対に変えないでください。return true にした瞬間、KVアクセスエラーが出るたびに全プレミアム記事が無料公開になります。アクセス制御は deny by default が鉄則です(STUMBLING_POINTS #89)。

ステップ5: Claude API実行層の利用上限を実装する

前編で触れたハイブリッドプラン(月額固定 + 超過従量)を実装するには、ユーザーごとの月間利用回数を別途カウントする必要があります。これもKVを使います。

// lib/usage.ts
import { getCloudflareContext } from '@opennextjs/cloudflare';
 
const PLAN_LIMITS: Record<string, number> = {
  free: 5,
  pro: 100,
  premium: 9999,
};
 
export async function checkAndIncrementUsage(
  email: string,
  plan: 'free' | 'pro' | 'premium'
): Promise<{ allowed: boolean; remaining: number; overage: number }> {
  const { env } = getCloudflareContext();
  const month = new Date().toISOString().slice(0, 7); // YYYY-MM
  const key = `site:claudelab:usage:${email}:${month}`;
 
  const raw = await env.MEMBERSHIP_KV.get(key);
  const current = raw ? parseInt(raw, 10) : 0;
  const limit = PLAN_LIMITS[plan];
 
  // 月末に自動失効するTTL
  const now = new Date();
  const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 1);
  const ttl = Math.floor((monthEnd.getTime() - now.getTime()) / 1000) + 86400;
 
  await env.MEMBERSHIP_KV.put(key, String(current + 1), { expirationTtl: ttl });
 
  if (current >= limit) {
    return { allowed: plan === 'pro', remaining: 0, overage: current + 1 - limit };
  }
  return { allowed: true, remaining: limit - current - 1, overage: 0 };
}

ハイブリッド設計では、Pro契約者が月間上限を超えても allowed: true を返し、超過分を別途記録します。月末に集計してStripe Usage Records APIで報告するのが理想ですが、個人開発の範囲では「Pro契約者は超過分も実行できる代わりに、翌月にメールで超過レポートを送る」という運用でも十分回ります。

ステップ6: Anthropic SDKを呼び出す

利用上限チェックを通過したら、いよいよClaude APIを呼びます。Cloudflare Workersでは @anthropic-ai/sdk をesmで読み込みます。

import Anthropic from '@anthropic-ai/sdk';
 
export const runtime = 'edge';
 
export async function POST(req: Request) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.email) {
    return new Response('unauthorized', { status: 401 });
  }
  const isMember = await canViewPremium();
  const plan = isMember ? 'pro' : 'free';
 
  const usage = await checkAndIncrementUsage(session.user.email, plan);
  if (!usage.allowed) {
    return Response.json(
      { error: 'usage_limit_exceeded', remaining: 0 },
      { status: 429 }
    );
  }
 
  const { prompt } = await req.json();
  const anthropic = new Anthropic({
    apiKey: process.env.ANTHROPIC_API_KEY,
  });
 
  const message = await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 2000,
    messages: [{ role: 'user', content: prompt }],
  });
 
  return Response.json({
    text: message.content[0].type === 'text' ? message.content[0].text : '',
    remaining: usage.remaining,
    overage: usage.overage,
  });
}

max_tokens は必ず明示してください。個人開発の最も痛い事故は「無制限のmax_tokensでループ呼び出しが発生し、1日で月の予算を使い切る」というパターンです。Anthropic Console側でも月間予算アラートを必ず設定しておきます。

ステップ7: 解約フローとエッジケース

本番運用で必ず詰まるのが解約周りです。Stripe Customer Portal を使えば解約画面はStripe側で持ってくれますが、解約後の挙動はあなたが設計する必要があります。

「契約解除直後の駆け込み利用」問題 — ユーザーが解約ボタンを押した瞬間、現在の課金期間の終わりまでは引き続きアクセスできるべきです。Webhookでは customer.subscription.deleted ではなく customer.subscription.updated(cancel_at_period_end が true になる)を聴くと、解約予約状態を正しく扱えます。完全に失効するのは customer.subscription.deleted イベント受信時です。

「失効後の自動ダウングレード」 — KVのTTL設計(次回更新日 + 3日)に頼ると、Webhook失敗時もある程度は自動で消えます。ただし完璧ではないので、月初に「先月失効したユーザーをStripeから取得してKVを再同期」するバッチをCron Triggers で動かしておくと安全です。

「重複Webhookによる二重課金」 — Stripeは同一イベントを複数回送ることがあります。event.id をKVに保存しておき、すでに処理済みならスキップする冪等性チェックを必ず入れてください。

const eventKey = `site:claudelab:webhook:${event.id}`;
const seen = await kv.get(eventKey);
if (seen) {
  return NextResponse.json({ received: true, duplicate: true });
}
await kv.put(eventKey, '1', { expirationTtl: 86400 * 7 });
// ... 通常の処理

ステップ8: Cloudflare CDNキャッシュとの戦い

これは個人開発SaaSでもっとも見落とされる罠です。Cloudflare WorkersでHTMLをキャッシュすると、ログイン状態や課金状態に関係なく同じHTMLが配信されてしまいます。

対策は2層です。1つ目は cache-worker.jspremium_token article_purchases などのCookieを持つリクエストはキャッシュをバイパスすること。2つ目はクライアント側でfetch('/api/premium-status') を発行して、最新の状態を取得してUIを差し替えること。

// cache-worker.js
addEventListener('fetch', event => {
  const req = event.request;
  const cookie = req.headers.get('cookie') || '';
 
  // 認証Cookie保持者はバイパス
  if (cookie.includes('premium_token') || cookie.includes('article_purchases')) {
    event.respondWith(fetch(req));
    return;
  }
 
  // それ以外はエッジキャッシュを使う
  event.respondWith(handleCachedRequest(event));
});

これを忘れると「課金してもメンバーシップCTAが消えない」というクレームを大量に受けることになります(STUMBLING_POINTS #73)。

全体を振り返って — 本番投入前のチェックリスト

実装が終わったら、本番デプロイ前に必ず次の項目を確認してください。

  • Stripe Webhookエンドポイントが本番URLを指している
  • STRIPE_WEBHOOK_SECRET が本番モードのものに切り替わっている
  • KVの key prefix がサイトごとに site:{name}: で分離されている(マルチサイト共有時の汚染防止)
  • 月間予算アラートがAnthropic Consoleで設定されている
  • Stripe Test Modeでサブスク作成 → 解約 → 再購入の3パスを通している
  • 解約後にプレミアム機能がきちんとブロックされることを確認している
  • cache-worker.jspremium_token article_purchases Cookieバイパスが効いている

ここまでくれば、Claude APIを使った月額課金SaaSとして十分に商用運用できる構成です。あとは前編で書いた通り、リテンション設計を継続的に磨き続ける勝負になります。

実装で詰まったら、私が運営するClaude Lab・Gemini Lab・Antigravity Lab・Rork Labのリポジトリ群が同じ構成で動いている実例として参考になるはずです。具体的な質問があれば、各サイトのサポートフォームから連絡をいただければ可能な範囲でお答えします。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-04-27
個人開発者がClaude APIで月収を作る完全ロードマップ — 90日で『払う人』にたどり着く実装と運用の全工程
Claude API を使った個人収益化を、アイデア出しからStripe統合、SEO、サブスク化、運用の心の管理までを90日のロードマップでまとめました。私が個人アプリ開発で12年やってきた経験と、AI API時代ならではの落とし穴を踏まえた、あえて遠回りしないための完全ガイドです。
API & SDK2026-04-25
Claude APIサービスに使用量課金を実装する — トークン計測・価格換算・Stripe Metering実装ガイド
Claude APIを組み込んだサービスで使用量に応じた課金を実装する方法を完全解説。トークン計測から価格換算ロジック、Stripe Metered Billingとの統合、利用制限の実装まで動作確認済みコードで紹介します。
API & SDK2026-04-24
Claude API × MCP で構築する有料コンサルティング SaaS の実装
個人で運営する有料コンサルティング SaaS の設計・実装・運用を完全解説。Claude API × MCP × Stripe × D1 KV を組み合わせた月10~30万円規模の収益構造、トークン従量課金の仕組み、実装コード、そして月商30万円まで成長させた際の学んだ実問題への対処法をお伝えします。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →