システム開発10分で読めます

Stripeサブスク課金の実装【2026年版】Webhook・冪等性・従量課金の設計と落とし穴

コセケン

コセケン

テクラル合同会社

#Stripe#サブスクリプション#従量課金#Webhook#冪等性#決済実装#SaaS開発#システム開発
Stripeサブスク課金の実装【2026年版】Webhook・冪等性・従量課金の設計と落とし穴

Stripeでサブスク課金を実装するときに本当に難しいのは、決済画面の組み込みではなく「Webhookの取りこぼしと冪等性をどう設計するか」です。Stripeは配信を「最低1回(at-least-once)」しか保証しないため、同じイベントが2回届く前提でコードを書かないと、二重課金・二重発行・状態のずれが本番で必ず起きます。本記事では2026年6月時点の公式仕様をもとに、サブスクリプションと従量課金の実装で外せない設計判断と、つまずきやすい落とし穴を整理します。

サブスク課金の全体像:状態の真実はStripe側にある

まず押さえるべき結論は、「契約状態の真実(source of truth)はStripe側にあり、自社DBはそのコピーにすぎない」という設計原則です。理由は、カード更新・支払い失敗・解約・プラン変更といったイベントが、ユーザー操作だけでなくStripe内部のリトライや締め処理でも発生するためです。自社DBを真実とみなして書くと、Stripe側の状態と乖離した瞬間に整合性が壊れます。

実装の骨格は次の3層に分かれます。

役割 主なStripeオブジェクト
決済の入口 カード登録・初回課金 Checkout Session / Payment Element
契約の管理 プラン・更新・解約 Customer / Subscription / Price
状態の同期 課金結果を自社DBへ反映 Webhook(Event)

Checkout Sessionで入口を作るのは簡単です。難所は3層目の「状態の同期」で、ここを甘く作ると後述の冪等性・整合性の問題が一気に噴出します。

Webhookは「最低1回」しか保証されない

Webhookの設計で最初に受け入れるべき事実は、Stripeが配信を保証するのは「最低1回」であり、「ちょうど1回」ではないということです。つまり同じイベントIDが複数回届くことを正常系として扱う必要があります。公式ドキュメントも、ネットワーク再送やリトライによって重複配信が起こりうると明記しています(Stripe公式: Webhooks)。

加えて運用上重要な制約が2つあります。

  • Stripeは2xxレスポンスを最大10秒待ちます。10秒を超えると、たとえ処理が後で完了しても配信は失敗扱いになりリトライ対象になります。
  • リトライは指数バックオフで一定期間続きますが、エンドポイントが返し続けて失敗すると最終的に無効化されることがあります。

ここから導かれる実装方針は明確です。Webhookハンドラは「受け取ったら即座に200を返し、重い処理は非同期キューに逃がす」のが鉄則です。

// Next.js (App Router) の Webhook 受信例 — Stripe Node SDK v17 系
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: Request) {
  const body = await req.text(); // 署名検証は生のボディが必須
  const sig = req.headers.get("stripe-signature")!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, endpointSecret);
  } catch (err) {
    // 署名検証に失敗 = 不正リクエスト。400 を返す
    return new Response("Invalid signature", { status: 400 });
  }

  // 重い処理はここでやらない。キューに積んで即 200 を返す
  await enqueueStripeEvent(event);
  return new Response("ok", { status: 200 });
}

ここでよくある落とし穴が、署名検証にパース済みJSONを渡してしまうことです。constructEvent は生のリクエストボディ(バイト列)でなければ検証に失敗します。フレームワークがbodyを自動でJSONパースする設定だと署名が必ず通らなくなるため、Webhookルートだけは生ボディを取得する必要があります。

冪等性:イベントIDで重複を弾く

重複配信に対する正解は、「処理済みイベントIDをDBに記録し、同じIDが来たら何もしない」ことです。理由は、Webhookハンドラの中で行う処理(メール送信・利用枠の付与・自社の課金状態更新)は、2回実行すると副作用が二重になるためです。

実装の肝は「DBのUNIQUE制約による原子的な重複検知」です。アプリ側で「存在するか確認してから挿入する」と、並列ワーカーが同時に処理した瞬間にすり抜けます。挿入の一意制約違反をもって「他のワーカーが処理済み」と判断するのが安全です。

-- 処理済みイベントを記録するテーブル
create table processed_stripe_events (
  event_id text primary key,          -- Stripe の event.id
  processed_at timestamptz not null default now()
);
async function handleStripeEvent(event: Stripe.Event) {
  // 原子的に挿入。重複ならユニーク制約違反で弾かれる
  const inserted = await db.insertProcessedEvent(event.id);
  if (!inserted) {
    // すでに処理済み。冪等に終了する
    return;
  }
  // ここから先は「このイベントについて高々1回」しか走らない
  await applyEvent(event);
}

なお、StripeへのAPIリクエスト側(サブスク作成・更新など)にも別途「冪等キー(Idempotency-Key)」を付けます。これはネットワークエラーで再送したときに、サブスクや課金が二重に作られるのを防ぐためです。公式の推奨はV4 UUIDなど十分なエントロピーを持つ文字列で、保存されたキーは24時間後に失効します。失効までの間は同じキーでの再送に最初のレスポンスがそのまま返るため、再送が安全になります(Stripe公式: Idempotent requests)。

await stripe.subscriptions.create(
  { customer: customerId, items: [{ price: priceId }] },
  { idempotencyKey: `sub-create-${customerId}-${planId}` }
);

「Webhook側の冪等性(イベントID)」と「APIリクエスト側の冪等性(Idempotency-Key)」は別物で、両方そろって初めて二重課金を防げます。片方だけでは穴が残るのが、設計レビューで最も見落とされる点です。

Webhook失敗時の整合性をどう担保するか

Webhookは落ちる前提で、整合性は「再同期できる仕組み」で担保します。理由は、エンドポイントのデプロイ中・障害中・10秒タイムアウトなどで、イベントを取りこぼす瞬間が必ず存在するからです。Webhookだけに頼ると、その間の状態変化が永久に欠落します。

実務で効く担保策は次の3つです。

担保策 内容 主に防ぐ事故
即時ACK+非同期処理 受信後すぐ200、処理はキューで実行 10秒タイムアウトによる失敗
イベントの永続化 受信イベントを生のまま保存してから処理 処理中クラッシュによる消失
定期リコンサイル Stripe APIから最新状態を取得して突合 取りこぼし・順序逆転の補正

特に3つ目の「リコンサイル(突合)」は軽視されがちですが、本番運用では不可欠です。Webhookは順序保証もないため、customer.subscription.updatedcreated より先に届くようなケースが起こります。最終的な状態を保証するには、定期的にStripeの Subscription を引き直して自社DBを上書きする処理を持っておくと、取りこぼしや順序逆転を自動で吸収できます。

サブスクのライフサイクルで最低限ハンドルすべきイベントは以下です。

  • checkout.session.completed:初回契約の確定(フルフィルメントの起点)
  • customer.subscription.updated:プラン変更・更新・ステータス遷移
  • customer.subscription.deleted:解約の確定
  • invoice.payment_failed:支払い失敗(サブスクが past_due に遷移)
  • invoice.paid:継続課金の成功

支払い失敗時にサブスクが past_due になり、設定によっては最終的に unpaid や解約に至る一連の遷移を、自社のアクセス権限とどう連動させるかは設計判断そのものです(Stripe公式: サブスクのWebhook)。

従量課金(メーター)の設計が2026年で変わった

従量課金を実装するなら、2026年時点では「Billing Meter(メーター)」を使う前提で設計してください。理由は、Stripeが従量課金の仕組みをメーター中心に刷新し、APIバージョン 2025-03-31.basil 以降では旧来の使用量レコードAPI(usage_records)が利用できなくなったためです。古い記事や旧実装の usage_type: 'metered' をそのまま写すと、現行APIでは動きません。

新しいメーターの流れは、(1) メーターを作成してイベント名を定義し、(2) そのメーターに紐づく従量Priceを作り、(3) 利用が発生するたびにメーターイベントを送る、という3ステップです。利用イベントは event_name と顧客・数量を含む payload で送ります。

// 1. メーターを作成(イベント名と集計方法を定義)
const meter = await stripe.billing.meters.create({
  display_name: "API calls",
  event_name: "api_calls",
  default_aggregation: { formula: "sum" },
});

// 2. 利用が発生するたびにメーターイベントを送信
await stripe.billing.meterEvents.create({
  event_name: "api_calls",
  payload: {
    stripe_customer_id: customerId,
    value: "1",
  },
});

メーター方式には旧方式に無い利点があります。サブスクが存在しなくても先にイベントを送れるため、無料トライアル中の利用量を計測しておき、有料転換後にまとめて反映する、といった設計が自然にできます。Stripeは2026年3月にLLMのトークン課金向けの機能(トークン数・モデル呼び出し回数・エージェントのタスク数などをメーターで課金する仕組み)も発表しています。ただし発表時点ではプレビュー(ウェイトリスト経由)での提供であり、全アカウントが即利用できるわけではないため、採用を前提に設計する前に自社で利用可否を確認してください(Stripe公式: Meters API)。

ここで設計判断が必要になります。従量課金は「いつ・どの粒度で・どこまで正確に」利用量を計測するかで、実装の難易度とコストが大きく変わります。

計測方針 メリット 注意点
リアルタイム送信 ダッシュボードに即反映 送信失敗時の再送・冪等の設計が必須
バッチ集計後に送信 送信回数を抑えられる 集計のずれ・締め時刻の扱いが難所
自社で二重計測 Stripe障害時も突合可能 集計ロジックの二重メンテが発生

メーターイベントの送信もネットワーク越しの処理なので、Webhookと同じく「失敗したら再送する」「再送しても二重計上しない」設計が必要です。利用量の課金は金額に直結するため、ここの取りこぼしや二重計上はそのまま売上のずれ・顧客クレームになります。

サブスクか従量課金か、ハイブリッドか

料金体系の選択は、課金実装の難易度を左右する最初の分岐点です。結論として、定額のみが最も実装が軽く、従量・ハイブリッドになるほど計測と整合性の負担が跳ね上がります。

課金モデル 実装の重さ 主な設計論点
定額サブスク プラン変更・日割り・解約タイミング
従量課金(メーター) 利用量計測の正確性・冪等な送信・締め処理
ハイブリッド(基本料+従量) 最重 上記2つの合算・請求書の整合・上限/下限の扱い

事業としては「基本料金+使った分だけ」のハイブリッドが魅力的に見えますが、実装では定額と従量の課題を両方抱え込みます。料金体系をプロダクト都合だけで決めると、課金実装が想定の数倍に膨らむことは珍しくありません。料金設計は実装難易度とセットで意思決定すべき論点です。

テストと運用:本番に乗せる前のチェック

課金は「テスト環境で動いた」では本番に乗せられません。理由は、本番特有の事象(カード更新の失敗、リトライ、Webアプリのデプロイ中のイベント取りこぼし、Stripe側の締め処理)が、テストでは再現しにくいからです。

最低限、次の観点を本番投入前に検証してください。

  • Webhookの重複配信を意図的に発生させ、二重課金・二重付与が起きないか(Stripe CLIで同一イベントを再送して確認)
  • 署名検証が生ボディで正しく動くか(デプロイ後のルーティングで壊れていないか)
  • 支払い失敗(invoice.payment_failed)からの復帰フローが、アクセス権限と正しく連動するか
  • リコンサイル処理が、Webhook停止中の状態変化を後から補正できるか
  • 従量課金の計測値が、自社の二重計測とずれなく一致するか

Stripeは本番に近い挙動を再現するためのテストカードやテストクロック(Stripe公式: Billingのテスト)を提供しています。更新タイミングや支払い失敗の遷移は、テストクロックで時間を進めて検証するのが確実です。

まとめ:課金は「設計」から判断する領域

Stripeのサブスク課金実装で品質を分けるのは、決済画面ではなく「冪等性・Webhook失敗時の整合性・従量課金の計測設計」という、表に見えない設計の部分です。本記事で見たように、最低1回配信の前提、イベントIDとIdempotency-Keyの二段構えの冪等性、リコンサイルによる整合性の担保、2026年のメーター方式への移行など、判断ポイントは決済の組み込みコードよりはるかに深いところにあります。

ここで内製と外注の判断軸が浮かび上がります。決済画面の組み込みだけならドキュメント通りで内製しやすい一方、「二重課金を絶対に出さない冪等設計」「Webhook取りこぼし時の自動補正」「従量課金の正確な計測」までを本番品質で作り切るには、課金特有の事故パターンを知った設計が要ります。料金体系(定額/従量/ハイブリッド)の選択が実装難易度を数倍に変えることも踏まえると、課金はコードを書き始める前の設計段階こそが勝負どころです。プロダクトの収益の根幹に関わる領域だからこそ、設計判断の確からしさを最優先に、内製と外注の線引きを決めるのが現実的な進め方です。

この記事を書いた人

コセケン

コセケン

テクラル合同会社

スタートアップでのCTO経験を経て、現在はテクラル合同会社にてシステム開発全般を牽引しています。アプリおよびWebの開発から、バックエンド、インフラ構築に至るまで幅広い技術領域に対応可能です。スピード感を持った品質の高いシステム開発を得意としており、新規プロダクトの立ち上げを一気通貫で支援します。本ブログでは実践的な開発ノウハウを発信していきます。

関連記事