単発の決済が「お金を1回受け取る」問題なら、サブスクリプションは「時間軸に沿って、正しい金額を、毎回、欠かさず受け取る」問題です。難しさの本質は、状態が時間とともに変わり続けること——トライアルが切れ、カードが期限切れになり、プランが途中でアップグレードされ、リトライが走り、解約が予約される——にあります。ここを構造で押さえないと、「請求漏れ」「過剰請求」「アクセス権の不整合」が静かに積み上がります。
この記事は、Stripe公式ドキュメントの最新仕様に忠実でありながら、「どの場面で・なぜ・どう使うか」がわかるように書いた Billing(サブスク/請求)専門の実装ガイドです。執筆時点の最新 API バージョンは 2026-05-27.dahlia。サンプルは Next.js 16(App Router / RSC / Server Actions)+ TypeScript strict で示します。
決済受け入れの基礎(Checkout Sessions・署名検証・Webhookの2層冪等モデル)は本記事では再説明しません。 そちらは姉妹記事「Stripe決済を本番品質で実装する完全ガイド(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 に反映される |
関係を図にすると次の通りです。
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・署名検証)は基礎記事に譲ります。サブスク化で変わるのは、mode: 'subscription' にし、line_items に recurring な Price を渡す点だけです。
// 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 をクライアントに渡して確定させます。
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 シグネチャの最終形は公式ドキュメントで必ず確認してください(後述の理由で、ここでは挙動を保守的に説明します)。
Billing Meters の考え方
Billing Meters では、集計(aggregation)はStripe側が担当します。あなたの責務は「いつ・誰が・どれだけ使ったか」を表すメーターイベントを送ることだけ。Price は「メーターに紐づくPrice」として定義され、請求周期末にStripeがイベントを合算して請求します。
メーターイベント送信で最重要なのが冪等性です。使用量の記録は「API呼び出しがネットワークで重複する」前提で設計しなければ、二重計上=過剰請求につながります。メーターイベントには冪等用の識別子(identifier)を付与でき、同一識別子のイベントは重複排除されます。
// 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 が請求されます。
// プランをアップグレードし、即時に差額請求する
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 を使います。サブスクを変更せずに「次の請求書がこうなる」を返してくれます。
// 確定せずに差額を見積もる(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のポータル設定で制御します。
// 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を呼ぶだけ。アクセシビリティ観点では、リダイレクトが起きることを事前に伝えるのが親切です(スクリーンリーダー利用者は突然のページ遷移に戸惑います)。
// 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 の順序比較)は基礎記事で詳述しているのでそちらに従ってください。ここではどのイベントを・どう扱うかに絞ります。
| イベント | 意味 | 推奨アクション |
|---|---|---|
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箇所に集約することです。
// 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原則)
- IDを自DBに保存する。
cus_xxx(Customer)とsub_xxx(Subscription)を user に紐づけて保存。これが無いとWebhookと自システムを照合できません。 - Stripeを真実源にする。 自DBの status はキャッシュ。乖離したらWebhookで上書き同期。二重管理の「権威」をDBに置かない。
- アクセス判定を1箇所に集約。
statusから「使える/使えない」を導く関数を1つにし、UI・API・バッチが同じ関数を呼ぶ。 - お金は整数。 金額・使用量は最小通貨単位の整数で扱う。手数料率はベーシスポイントの整数で持ち、丸めをアプリ側で確定。浮動小数を請求計算に挟まない。
- クライアントを信用しない。
priceIdはホワイトリスト検証、customerIdは認証情報から解決。金額・数量をクライアントから受け取らない。 - 冪等性を二段で。 送信側は決定的
idempotencyKey/identifier、受信側はevent.idの一意制約。リトライ・重複は正常系として吸収。 - 時間依存をテスト可能に。 更新・失効・リトライは 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年版)」をご覧ください。
(本記事のStripe仕様は2026年6月時点の公式ドキュメント/API版 2026-05-27.dahlia に基づきます。従量課金の推奨(Metronome / Billing Meters)やメーターイベントの正確なシグネチャは進化が速い領域です。最新の挙動は必ず公式ドキュメントで確認してください。)