# Stripe Billing 実装ガイド（2026年版・公式準拠）：サブスク・従量課金（Billing Meters / Metronome）・顧客ポータル・プロレーションを実コードで

> Stripe Billing公式準拠の実装ガイド。サブスク開始（Checkout / Subscriptions API）、従量課金（Billing Meters）、Customer Portal、プロレーション、Webhook冪等処理をNext.js 16 + TypeScriptの実コードで解説。

- 公開日: 2026-06-24
- 著者: 友田 陽大
- タグ: Stripe, 決済, サブスクリプション, Next.js, TypeScript, B2B SaaS, 冪等性
- URL: https://tomodahinata.com/blog/stripe-billing-subscriptions-usage-based-customer-portal-guide

## 要点

- 構成要素は Customer→Subscription→Price/Product→Invoice の5つで、アクセス権は Subscription.status に集約する
- サブスク開始はまず Checkout（mode: subscription）、フル制御が要るときだけ Subscriptions API を default_incomplete で叩く
- 従量課金は2026年は Metronome が公式の第一推奨。既存の Billing Meters は identifier でメーターイベントを冪等送信する
- 解約・カード更新 UI は自作せず Customer Portal へ委譲し、プロレーションは proration_behavior と invoices.createPreview で扱う
- 真実源は DB ではなく Webhook。invoice.paid で付与・unpaid/deleted で剥奪し、支払い失敗は即時剥奪せず Smart Retries に委ねる

---

単発の決済が「お金を1回受け取る」問題なら、サブスクリプションは「**時間軸に沿って、正しい金額を、毎回、欠かさず**受け取る」問題です。難しさの本質は、状態が時間とともに変わり続けること——トライアルが切れ、カードが期限切れになり、プランが途中でアップグレードされ、リトライが走り、解約が予約される——にあります。ここを構造で押さえないと、「請求漏れ」「過剰請求」「アクセス権の不整合」が静かに積み上がります。

この記事は、**Stripe公式ドキュメントの最新仕様に忠実**でありながら、「どの場面で・なぜ・どう使うか」がわかるように書いた **Billing（サブスク／請求）専門**の実装ガイドです。執筆時点の最新 API バージョンは **`2026-05-27.dahlia`**。サンプルは **Next.js 16（App Router / RSC / Server Actions）+ TypeScript strict** で示します。

> **決済受け入れの基礎（Checkout Sessions・署名検証・Webhookの2層冪等モデル）は本記事では再説明しません。** そちらは姉妹記事「[Stripe決済を本番品質で実装する完全ガイド（2026年版）](/blog/stripe-checkout-sessions-payments-production-guide-2026)」に分離しています。本記事は **サブスク特有の論点**——構成要素・従量課金・プロレーション・顧客ポータル・ライフサイクルWebhook——に集中します。

> **筆者の実務背景**：私は **Next.js 16.1 + Stripe 17** で本番サブスクリプション基盤を構築しました。Webhookは `event.id` の一意制約＋`event.created` による順序保証＋再帰的なPII墨消しで冪等化、マルチチャネルの料金計算は**純粋関数**として実装、銀行振込のサブスク状態を**状態機械**でモデル化し、**433本のテスト**で守っています。加えて、環境分野のサーバーレス決済プラットフォームで信頼性レイヤーを主導し **本番二重課金0件** を達成しました。本文のStripe仕様はすべて公式ドキュメントで裏取りし、MRR・チャーン・ROIといった未確認数値は扱いません。

---

## 1. サブスクリプションの構成要素

Stripe Billing は5つのオブジェクトの組み合わせです。ここを曖昧にしたまま実装に入ると、後で「どこにアクセス権の真実があるのか」が分からなくなります。

| オブジェクト | 役割 | 重要ポイント |
| --- | --- | --- |
| `Customer` | 請求情報の入れ物 | 支払い方法・住所・次回以降の請求先を保持。あなたのDBの user に1対1で紐づける |
| `Product` | 売る対象（プラン名など） | 「Proプラン」「Businessプラン」といった概念。Entitlements（機能アクセス）と紐づく |
| `Price` | recurring な金額・通貨・周期 | 「月額 ¥1,980」「年額 ¥19,800」。**1つのProductに複数Price**（月/年、通貨違い）を持てる |
| `Subscription` | Customer と Price を結ぶ継続関係 | ライフサイクル（trialing→active→past_due…）の主体。**アクセス権判定はここ** |
| `Invoice` | 各請求周期の請求書 | 毎周期 Stripe が自動生成。`PaymentIntent` を内包し、支払い結果が `status` に反映される |

関係を図にすると次の通りです。

```text
Customer
 └─ Subscription ── Price (recurring) ── Product
       └─ Invoice(毎周期) ── PaymentIntent(支払い処理)
```

**`Subscription.status` の遷移**は実装の生命線です。公式の定義を要約します。

| status | 意味 | アクセス権 |
| --- | --- | --- |
| `trialing` | トライアル中（まだ課金なし） | 付与（試用） |
| `active` | 正常。最新請求書が支払い済み | **付与** |
| `incomplete` | 初回支払い未確定（最大23時間の猶予） | 保留 |
| `incomplete_expired` | 初回支払いが23時間以内に確定せず失効 | 付与しない |
| `past_due` | 最新請求書の支払い失敗。リトライ中 | 要ポリシー判断（猶予 or 制限） |
| `unpaid` | リトライ枯渇。最新請求書が未払いのまま | **剥奪** |
| `canceled` | 解約済み（最終・不変） | **剥奪** |
| `paused` | トライアル終了時に支払い方法が無く一時停止 | 剥奪 |

> 設計の勘所：**自前のDBに「有効/無効」フラグを二重に持たない**。真実は `Subscription.status` であり、あなたのDBはそのキャッシュです。Webhookで同期し、判定ロジックは1箇所に集約します（§7・§8）。

---

## 2. サブスク開始：Checkout か Subscriptions API か

サブスクの「開始」には2つの道があります。公式の立場は明快で、**Checkout が最小実装・推奨**、Subscriptions API直接叩きは**フル制御が要るときのみ**です。

| 観点 | Checkout（`mode: 'subscription'`） | Subscriptions API 直接 |
| --- | --- | --- |
| 実装量 | **最小** | 大（支払い方法の収集・確定を自前で） |
| 支払いUI | Stripeホスト型 / 埋め込み型 | Payment Element を自前で組む |
| 税・割引・支払い方法保存 | **組み込み** | 手動設定 |
| 向く場面 | MVP・標準フロー・早く安全に | 決済UXが事業の核・独自の状態管理が必要 |

### 2-A. 推奨：Checkout Session（mode: 'subscription'）

決済受け入れの基礎（リダイレクト型/埋め込み型・`success_url`・署名検証）は[基礎記事](/blog/stripe-checkout-sessions-payments-production-guide-2026)に譲ります。サブスク化で変わるのは、**`mode: 'subscription'` にし、`line_items` に recurring な Price を渡す**点だけです。

```ts
// app/actions/start-subscription.ts
"use server";

import Stripe from "stripe";
import { redirect } from "next/navigation";

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

export async function startSubscription(formData: FormData): Promise<void> {
  const priceId = String(formData.get("priceId"));
  // 顧客は自サイトの認証済みユーザーから解決する（クライアントの値は信用しない）
  const customerId = await resolveStripeCustomerId(); // 自前: users → cus_xxx

  const session = await stripe.checkout.sessions.create(
    {
      mode: "subscription",
      customer: customerId,
      line_items: [{ price: priceId, quantity: 1 }],
      // トライアルや支払い方法保存はここで指定（§5）
      subscription_data: { trial_period_days: 14 },
      success_url: `${process.env.APP_URL}/billing/return?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.APP_URL}/pricing`,
    },
    // 同一ユーザーの二重作成を防ぐ決定的キー（送信側冪等性）
    { idempotencyKey: `sub-start:${customerId}:${priceId}:v1` },
  );

  if (!session.url) throw new Error("Checkout session URL missing");
  redirect(session.url); // Next.jsのredirectはthrowするので戻り値はvoidで良い
}
```

ポイントは3つ。**①顧客IDはサーバーの認証情報から解決**する（`priceId` ですらクライアント由来なら、許可されたPrice集合に対してホワイトリスト検証すべきです）。**②`idempotencyKey` は決定的**にし、ダブルクリックやリトライでサブスクが二重生成されるのを防ぐ。**③確定はこのリダイレクトではなくWebhook**で行う（§7）。

### 2-B. フル制御：Subscriptions API 直接

支払いUIを自前のPayment Elementで完全制御したい場合は、サブスクを `default_incomplete` で作り、初回 Invoice の `confirmation_secret` をクライアントに渡して確定させます。

```ts
const subscription = await stripe.subscriptions.create(
  {
    customer: customerId,
    items: [{ price: priceId }],
    // 初回支払いが確定するまで incomplete に留める（推奨パターン）
    payment_behavior: "default_incomplete",
    payment_settings: { save_default_payment_method: "on_subscription" },
    // 初回InvoiceのPaymentIntent確定情報を展開して取得
    expand: ["latest_invoice.confirmation_secret"],
  },
  { idempotencyKey: `sub-create:${customerId}:${priceId}:v1` },
);

// この confirmation_secret をクライアントへ渡し、Payment Element で確定する
const invoice = subscription.latest_invoice;
// invoice.confirmation_secret をフロントの stripe.confirmPayment() に渡す
```

`payment_behavior: "default_incomplete"` は**重要**です。これにより「サブスクは作ったが初回課金は未確定」という状態を安全に表現でき、3Dセキュア（`requires_action`）にも自然に対応できます。確定の合図はやはりWebhook（`invoice.paid`）です。

---

## 3. 従量課金（usage-based）：2026年の正解

ここは**2026年に推奨が変わった**ポイントなので、古い記事を鵜呑みにすると誤ります。公式ドキュメントの現在の立場は明確です。

> 「Metronome は Stripe の主力の従量課金プラットフォームであり、**すべての新規実装で推奨**されます。」
> 「基本的な従量課金（Billing Meters）は、**すでに Billing Meters で顧客に課金している場合にのみ**継続してください。」

つまり判断は次の通りです。

| 状況 | 推奨 |
| --- | --- |
| これから従量課金を新規実装する | **Metronome**（Stripeの新しい従量課金基盤） |
| 既存の定額サブスクに従量を足すが、Connect / Checkout / Adaptive Pricing / Workflows との完全互換が必要 | Billing Meters（Metronomeは一部サポートが限定的） |
| すでに Billing Meters で課金中 | そのまま Billing Meters を継続 |

> **本記事の方針**：新規なら Metronome を検討する、という公式の推奨を尊重します。一方で多くの既存システムは Billing Meters 上にあるため、ここでは **Billing Meters のメーターイベント**の考え方を、冪等性に絞って解説します。API シグネチャの最終形は[公式ドキュメント](https://docs.stripe.com/billing/subscriptions/usage-based)で必ず確認してください（後述の理由で、ここでは挙動を保守的に説明します）。

### Billing Meters の考え方

Billing Meters では、**集計（aggregation）はStripe側が担当**します。あなたの責務は「いつ・誰が・どれだけ使ったか」を表す**メーターイベント**を送ることだけ。Price は「メーターに紐づくPrice」として定義され、請求周期末にStripeがイベントを合算して請求します。

メーターイベント送信で**最重要なのが冪等性**です。使用量の記録は「API呼び出しがネットワークで重複する」前提で設計しなければ、二重計上＝過剰請求につながります。メーターイベントには**冪等用の識別子**（`identifier`）を付与でき、同一識別子のイベントは重複排除されます。

```ts
// lib/usage/report.ts —— 使用量の冪等記録
import Stripe from "stripe";

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

type UsageEvent = {
  readonly eventName: string;        // メーター作成時に決めた event_name
  readonly stripeCustomerId: string; // cus_xxx
  readonly value: number;            // 今回の使用量（整数で扱う）
  readonly occurredAt: Date;         // 使用が発生した時刻
  readonly dedupeKey: string;        // 自システム側の決定的キー（例: 集計バッチID + 行ID）
};

export async function reportUsage(e: UsageEvent): Promise<void> {
  await stripe.billing.meterEvents.create({
    event_name: e.eventName,
    // payload のキーは作成したメーターの設定に合わせる（value / stripe_customer_id 等）
    payload: {
      value: String(e.value),
      stripe_customer_id: e.stripeCustomerId,
    },
    // 使用が発生した実時刻（請求周期の帰属を正しくするため）
    timestamp: Math.floor(e.occurredAt.getTime() / 1000),
    // ★冪等キー：同一値の重複イベントは排除される。リトライ安全。
    identifier: e.dedupeKey,
  });
}
```

設計上の鉄則は3つです。**①`identifier` を決定的に**（タイムスタンプやUUIDのランダム生成ではなく、「集計バッチID＋行ID」のように**再送しても同じ値**になるキー）。**②`value` は整数**で扱い、丸めはアプリ側で確定させてから送る。**③`timestamp` は使用発生時刻**——送信時刻ではなく発生時刻を使うことで、月末ギリギリの使用が翌月にズレ込む事故を防げます。

> なぜここまで慎重か：私が運用してきた決済基盤で痛感したのは、**「お金に関わるイベントは exactly-once を構造で保証する」**という一点です。私のサブスク基盤ではマルチチャネルの料金計算を純粋関数化し、Webhookを `event.id` で冪等化しました。従量課金も同じ思想で、「再送・重複は正常系」として `identifier` で吸収します。

---

## 4. トライアル & プロレーション（日割り）

### トライアル

最も簡単なのは `trial_period_days`。Checkoutなら `subscription_data.trial_period_days`、Subscriptions APIなら直接指定します（§2のコード参照）。トライアル中は `status: trialing` で、終了3日前に `customer.subscription.trial_will_end` が飛ぶので、ここで支払い方法の有無を確認するのが定石です。

### プロレーション：途中のアップグレード/ダウングレード

月の途中でプランを変えたとき、Stripeは**未使用分のクレジット**と**新プランの残り期間請求**を自動計算します。挙動は `proration_behavior` で制御します。

| 値 | 挙動 |
| --- | --- |
| `create_prorations`（既定） | 差額のプロレーション明細を作成（次回請求にまとめる場合あり） |
| `always_invoice` | 変更時点で**即時に請求書を発行・課金** |
| `none` | 日割りなし。次回周期から新価格 |

例：月額 ¥1,000 → ¥2,000 に、ちょうど折り返し地点でアップグレードした場合——旧プラン未使用分 −¥500、新プラン残り期間 +¥1,000、**差し引き +¥500** が請求されます。

```ts
// プランをアップグレードし、即時に差額請求する
const updated = await stripe.subscriptions.update(
  subscriptionId,
  {
    items: [{ id: subscriptionItemId, price: newPriceId }],
    proration_behavior: "always_invoice",
    // 後述のプレビューと「同じ proration_date」を渡すと金額が一致する
    proration_date: prorationDate,
  },
  { idempotencyKey: `sub-upgrade:${subscriptionId}:${newPriceId}:v1` },
);
```

**変更前に金額をユーザーへ提示**するには `invoices.createPreview` を使います。サブスクを変更せずに「次の請求書がこうなる」を返してくれます。

```ts
// 確定せずに差額を見積もる（UIで「+¥500を本日請求します」と表示できる）
const prorationDate = Math.floor(Date.now() / 1000);

const preview = await stripe.invoices.createPreview({
  customer: customerId,
  subscription: subscriptionId,
  subscription_details: {
    items: [{ id: subscriptionItemId, price: newPriceId }],
    proration_date: prorationDate,
  },
});

// preview.lines のうち proration: true の明細が日割り（クレジット/デビット）
```

> 実装の落とし穴：プロレーションは**秒単位で計算**されるため、プレビューと実行で `proration_date` がズレると金額が変わります。**プレビューと更新で同じ `proration_date` を渡す**ことを徹底してください。

---

## 5. 顧客ポータル（Customer Portal）：解約UIを自作しない

サブスクで最も「車輪の再発明」になりがちなのが、**解約・プラン変更・カード更新・請求書ダウンロード**のUIです。これらを自作すると、PCI境界・3Dセキュア・多言語・税表示・領収書をすべて自前で背負うことになります。

**結論：作らない。** Stripeの **Customer Portal** へリダイレクトします。`stripe.billingPortal.sessions.create` で短命のセッションURLを発行し、そこへ飛ばすだけです。許可する操作（解約可否・変更可能なPrice）はDashboardの**ポータル設定**で制御します。

```ts
// app/actions/open-billing-portal.ts
"use server";

import Stripe from "stripe";
import { redirect } from "next/navigation";

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

export async function openBillingPortal(): Promise<void> {
  const customerId = await resolveStripeCustomerId(); // 認証済みユーザーから解決

  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    // 操作完了後に自サイトへ戻すURL
    return_url: `${process.env.APP_URL}/settings/billing`,
    // 任意: 特定フロー（解約確認など）に直接入る場合は flow_data を指定
  });

  redirect(session.url); // 短命セッション。都度発行する
}
```

ポータルセッションは**短命で都度発行**が原則です。URLをキャッシュ・再利用してはいけません。

UI側はServer Actionを呼ぶだけ。アクセシビリティ観点では、**リダイレクトが起きることを事前に伝える**のが親切です（スクリーンリーダー利用者は突然のページ遷移に戸惑います）。

```tsx
// app/settings/billing/portal-button.tsx
import { openBillingPortal } from "@/app/actions/open-billing-portal";

export function BillingPortalButton() {
  return (
    <form action={openBillingPortal}>
      <button
        type="submit"
        // 外部のStripe管理画面へ遷移する旨をAT利用者に明示
        aria-label="お支払い情報の管理画面（Stripe）へ移動します"
      >
        お支払い・プランの管理
      </button>
    </form>
  );
}
```

> なぜ自作を避けるのか：解約フローは**ビジネスにとってもユーザーにとっても繊細**で、Stripeはここを継続的に改善し、各国の法令（クーリングオフ等）にも追従しています。自作は初期コストだけでなく**永続的な保守債務**になります。差別化要素でない限り、委譲が正解です。

---

## 6. Billingのライフサイクルを Webhook で処理する

サブスクの状態は時間とともに勝手に変わります（更新・失敗・リトライ・解約予約の発火）。**これを知る唯一の方法がWebhook**です。署名検証と**2層の冪等モデル（`event.id` の一意制約＋`event.created` の順序比較）**は[基礎記事](/blog/stripe-checkout-sessions-payments-production-guide-2026)で詳述しているのでそちらに従ってください。ここでは**どのイベントを・どう扱うか**に絞ります。

| イベント | 意味 | 推奨アクション |
| --- | --- | --- |
| `customer.subscription.created` | サブスク作成（`incomplete` の場合あり） | DBに `sub_id` 記録。statusを保存 |
| `customer.subscription.updated` | プラン変更・status遷移など | `status` を同期。`active`化でアクセス付与判定 |
| `customer.subscription.deleted` | 解約完了（最終） | **アクセス剥奪** |
| `customer.subscription.trial_will_end` | トライアル終了3日前 | 支払い方法の有無を確認し通知 |
| `invoice.paid` | 請求書の支払い成功 | **アクセス付与**（`status` が `active`） |
| `invoice.payment_failed` | 支払い失敗 | 顧客に通知・カード更新を促す（dunning） |
| `invoice.upcoming` | 次回請求が近い | 必要なら明細を追加 |

**アクセス制御の真実源を1つに**します。私のおすすめは「**`invoice.paid` で付与、`customer.subscription.deleted` / `status === 'unpaid'` で剥奪**」というルールをハンドラ1箇所に集約することです。

```ts
// app/api/stripe/webhook/route.ts（抜粋。署名検証・冪等化は基礎記事の実装を前提）
import type Stripe from "stripe";

async function handleBillingEvent(event: Stripe.Event): Promise<void> {
  switch (event.type) {
    case "invoice.paid": {
      const invoice = event.data.object as Stripe.Invoice;
      // 支払い成功 → アクセス付与。subscription IDで自DBを更新
      await grantAccessForSubscription(invoice);
      break;
    }
    case "customer.subscription.updated": {
      const sub = event.data.object as Stripe.Subscription;
      // statusをDBへ同期（active/past_due/unpaid…）
      await syncSubscriptionStatus(sub);
      if (sub.status === "unpaid") await revokeAccess(sub.id);
      break;
    }
    case "customer.subscription.deleted": {
      const sub = event.data.object as Stripe.Subscription;
      await revokeAccess(sub.id); // 解約 → 剥奪
      break;
    }
    case "invoice.payment_failed": {
      const invoice = event.data.object as Stripe.Invoice;
      // dunning: 通知のみ。剥奪はせず Smart Retries に委ねる
      await notifyPaymentFailed(invoice);
      break;
    }
    // それ以外は記録のみ（未知のイベントで500を返さない）
  }
}
```

### dunning（督促）と revenue recovery

支払い失敗時にすぐアクセスを切るのは**ほぼ常に間違い**です。カードの一時的な失敗・期限切れは日常的に起き、Stripeの **Smart Retries**（データに基づく最適なタイミングでの自動再試行）が回収してくれます。実装側は次のスタンスが堅牢です。

- `invoice.payment_failed` → **通知のみ**。statusは `past_due` になるが、即時剥奪はしない（猶予ポリシーを設ける）。
- Smart Retries が枯渇し `status` が `unpaid`（または `canceled`）になったら **初めて剥奪**。
- リトライ・督促メールのスケジュールは**Dashboardで設定**（コード不要）。アプリ側で再実装しない。

> `past_due` の扱いはプロダクト判断です。BtoBなら「猶予を与えて関係を守る」、無料枠への降格で対応する、などの選択肢があります。**剥奪トリガーを `unpaid` / `deleted` に固定**しておくと、ポリシー変更がハンドラ1箇所で済みます。

---

## 7. 設計のコツ（本番で効く7原則）

1. **IDを自DBに保存する。** `cus_xxx`（Customer）と `sub_xxx`（Subscription）を user に紐づけて保存。これが無いとWebhookと自システムを照合できません。
2. **Stripeを真実源にする。** 自DBの status は**キャッシュ**。乖離したらWebhookで上書き同期。二重管理の「権威」をDBに置かない。
3. **アクセス判定を1箇所に集約。** `status` から「使える/使えない」を導く関数を1つにし、UI・API・バッチが同じ関数を呼ぶ。
4. **お金は整数。** 金額・使用量は最小通貨単位の整数で扱う。手数料率はベーシスポイントの整数で持ち、丸めをアプリ側で確定。浮動小数を請求計算に挟まない。
5. **クライアントを信用しない。** `priceId` はホワイトリスト検証、`customerId` は認証情報から解決。金額・数量をクライアントから受け取らない。
6. **冪等性を二段で。** 送信側は決定的 `idempotencyKey` / `identifier`、受信側は `event.id` の一意制約。リトライ・重複は正常系として吸収。
7. **時間依存をテスト可能に。** 更新・失効・リトライは **Test Clocks** で仮想的に時間を進めて検証。料金・プロレーション計算は純粋関数に切り出し、ユニットテストで固める（私のサブスク基盤の433テストはこの方針です）。

---

## 8. よくある質問（FAQ）

**Q. サブスクの「解約」はどう実装するのが正解？**
A. **自作しないのが正解**です。Customer Portal（§5）にリダイレクトし、解約・即時/期末解約・再開をStripeに任せます。どうしてもアプリ内で完結したい場合のみ `stripe.subscriptions.cancel(id)`（即時）か `update(id, { cancel_at_period_end: true })`（期末）を使い、結果は `customer.subscription.deleted` / `updated` のWebhookで自DBへ反映します。

**Q. 途中でプランを変えたときの日割りは？**
A. `proration_behavior` で制御します（§4）。即時に差額課金するなら `always_invoice`、次回周期からなら `none`。**変更前に `invoices.createPreview` で金額を提示**し、プレビューと更新で同じ `proration_date` を渡すのが事故防止の鉄則です。

**Q. 従量課金の集計は自分でやるの？**
A. いいえ。**集計はStripe側**が行います。あなたは使用が発生するたびにメーターイベントを送るだけ（§3）。ただし**新規実装は Metronome が公式推奨**で、Billing Meters は既存利用者向けの位置づけに変わりました。どちらでも「使用量イベントを `identifier` で冪等に送る」という設計思想は共通です。

**Q. アクセス権はDBのフラグで管理していいですか？**
A. DBは**キャッシュ**として持って構いませんが、**真実源は `Subscription.status`** です。`invoice.paid` で付与、`unpaid` / `deleted` で剥奪、というルールをWebhookハンドラ1箇所に集約し、DBはそれを反映するだけにします。二重に「権威」を持たせると必ず乖離します。

**Q. 支払いが失敗したらすぐアクセスを切るべき？**
A. いいえ。一時的な失敗は日常的に起きます。`invoice.payment_failed` では**通知のみ**にして Smart Retries に回収を委ね、`status` が `unpaid`（リトライ枯渇）または `canceled` になって**初めて剥奪**します（§6）。

**Q. 1ヶ月後の更新やトライアル終了をテストしたい。**
A. **Test Clocks** で仮想時計を進めれば、更新・失効・リトライ・`trial_will_end` を数秒で再現できます。料金・プロレーションのロジックは純粋関数に切り出し、ユニットテストで網羅するのが堅牢です。

---

## まとめ：サブスクの正しさは「状態」と「冪等性」で作る

サブスクリプションの実装は、APIの呼び方ではなく **「時間とともに変わる状態を、信頼できない外部（クライアント・ネットワーク・リトライ）の中で、正しく1箇所に保ち続ける」** ことです。要点を畳みます。

- **構成要素**：`Customer`→`Subscription`→`Price`/`Product`→`Invoice`。アクセス判定は `Subscription.status` に集約。
- **開始**：まず Checkout（`mode: 'subscription'`）。フル制御時のみ `default_incomplete` で Subscriptions API。
- **従量課金**：新規は **Metronome** が公式推奨。既存は Billing Meters のメーターイベントを `identifier` で冪等送信。
- **プロレーション**：`proration_behavior` ＋ `invoices.createPreview` で確定前に金額提示。
- **顧客ポータル**：解約・カード更新UIは自作しない。`billingPortal.sessions.create` に委譲。
- **Webhook**：`invoice.paid` で付与、`unpaid`/`deleted` で剥奪。dunningは即時剥奪しない。冪等は2層。

私はこの水準を、**Next.js 16.1 + Stripe 17 のサブスク基盤**（冪等Webhook・純粋関数の料金計算・銀行振込の状態機械・433テスト）と、**本番二重課金0件**を達成した決済信頼性レイヤーで実装・運用してきました。サブスク／請求プラットフォームの新規構築、従量課金（Metronome / Billing Meters）の導入、既存サブスクの信頼性立て直し（プロレーション・dunning・Webhook設計・アクセス権の整合）をご検討でしたら、**要件定義から本番運用・テスト容易性の担保まで、この記事の水準でお引き受けします**。一人 × 生成AI（Claude Code）を verification gate で運用し、この品質を速く・安全にお届けします。

> 本記事のWebhook冪等処理・PII墨消し・状態機械の**実コードレベルの深掘り**は、関連事例「サブスク学習プラットフォームのアーキテクチャ徹底解剖」で公開しています。決済受け入れの基礎は姉妹記事「[Stripe決済を本番品質で実装する完全ガイド（2026年版）](/blog/stripe-checkout-sessions-payments-production-guide-2026)」をご覧ください。

*（本記事のStripe仕様は2026年6月時点の公式ドキュメント／API版 `2026-05-27.dahlia` に基づきます。従量課金の推奨（Metronome / Billing Meters）やメーターイベントの正確なシグネチャは進化が速い領域です。最新の挙動は必ず[公式ドキュメント](https://docs.stripe.com/billing/subscriptions/overview)で確認してください。）*
