Stripeでの使用量課金:実践的なデータモデル
Stripeでの使用量課金には、きれいなイベント保存と突合せが必要です。シンプルなスキーマ、Webhookフロー、バックフィル、二重計上対策を学びます。

実際に作っているもの(そしてなぜ壊れるのか)
使用量ベースの課金はシンプルに聞こえます:顧客の利用量を測り、価格を掛けて、期間末に請求するだけ。しかし実務では小さな会計システムを作っているのと同じです。データが遅れて届いたり、二重に届いたり、あるいは全く届かない場合でも正確である必要があります。
多くの失敗はチェックアウトやダッシュボードで起きるわけではありません。メータリングのデータモデルで起きます。「この請求書にどの使用イベントがカウントされ、なぜそうなったか」を自信を持って答えられないと、いずれ過請求・過少請求・信頼の喪失につながります。
使用量課金が壊れるパターンは予測可能です:障害でイベントが欠落する、リトライで重複ができる、遅れて到着したイベントが合計算出後に現れる、あるいはシステム間で不一致が生じて差分を突合できない等です。
Stripeは価格設定、請求書、税金、回収に優れていますが、あなたのプロダクトの生の使用量はStripeには自動的にわかりません。送るのはあなたの仕事であり、その結果「台帳はStripeか?自分のDBか?」という設計判断が必要になります。
多くのチームにとって安全な分担は次のとおりです。
- 生の使用イベントとそのライフサイクルはあなたのデータベースを根拠(source of truth)にする。
- 実際に請求され支払われたかどうかはStripeを根拠にする。
例:"APIコール"をトラッキングする場合、各コールは安定した一意キーを持つ使用イベントを生成します。請求時にはまだ請求対象になっていない適格なイベントのみを合算し、Stripeの請求アイテムを作成または更新します。取り込みのリトライやWebhookの二重到着があっても冪等性のルールで重複を無害化できます。
テーブル設計前に決めるべきこと
テーブルを作る前に、後で請求を説明可能にするための定義を固めてください。多くの「謎の請求バグ」はSQLの問題ではなく、ルールが曖昧なことが原因です。
まず、課金単位を決めます。計測が簡単で議論が起きにくいものを選んでください。"APIコール"はリトライやバッチ、失敗で厄介になります。"分"は重複やオーバーラップで面倒です。"GB"はGBとGiBの差や平均かピークかといった定義が必要です。
次に境界を定義します。イベントがどのウィンドウに属するかを正確に決めます。時間単位(時間/日)や請求期間、もしくは顧客のアクションごとに集計するのか。顧客が月途中でプラン変更した場合にウィンドウを分割するかどうかなど、これらの選択がイベントのグルーピングと合計の説明方法を決めます。
また、どのシステムがどの事実を所有するかを決めます。Stripeと組み合わせる一般的なパターンは、アプリが生イベントと派生合計を所有し、Stripeが請求書と支払いステータスを所有するというものです。履歴を黙って書き換えない運用が前提です。修正は新しいエントリとして記録し、元の記録は残します。
守るべき非妥協項目を短くまとめると:
- 追跡可能性(Traceability):請求された単位はすべて保存済みイベントに遡れること。
- 監査可能性(Auditability):なぜ請求されたかを数か月後でも説明できること。
- 可逆性(Reversibility):間違いは明示的な調整で直せること。
- 冪等性(Idempotency):同じ入力が二度カウントされないこと。
- 明確な所有権(Clear ownership):各事実(使用量、価格、請求)に一つの責任者がいること。
例:もし"送信したメッセージ"で課金するなら、リトライがカウントされるか、配信失敗はどう扱うか、どのタイムスタンプ(クライアント時間かサーバー時間か)を優先するかを決めて記録し、イベントフィールドとバリデーションに落とし込んでください。
使用イベントのシンプルなデータモデル
使用量課金は会計的に扱うと最も簡単です:生の事実は追記専用(append-only)、合計は派生的に算出する。この単一の選択でほとんどの争点は防げます。どの数値がどこから来たかを常に説明できるからです。
実用的な出発点は五つのコアテーブルです(名前は変えて構いません)。
- customer:内部の顧客ID、Stripeのcustomer id、状態、基本メタデータ。
- subscription:内部のサブスクリプションID、Stripeのsubscription id、期待されるプラン/価格、開始/終了タイムスタンプ。
- meter:計測対象(APIコール、席数、ストレージGB-時間など)。安定したメーターキー、単位、集計方法(sum, max, unique)を含めます。
- usage_event:計測されたアクションごとに1行。customer_id、subscription_id(分かれば)、meter_id、quantity、
occurred_at(発生時刻)、received_at(取り込み時刻)、source(app、batch import、partnerなど)、重複排除用の安定した外部キーを保存します。 - usage_aggregate:派生合計。通常はcustomer + meter + 時間バケット(日や時間)と請求期間ごとに保管。合計数量に加え、再計算をサポートするバージョンや
last_event_received_atを持たせます。
usage_eventは不変に保ってください。後で誤りを発見したら履歴を編集するのではなく、補正イベント(例:キャンセルのための-3席)を追記します。
監査や紛争に備えて生イベントを保存してください。永続保存できない場合でも、請求の照会ウィンドウと返金/紛争ウィンドウの期間分は保持しておくべきです。
派生合計は分離しておきます。集計は請求書やダッシュボードを高速化しますが再構築可能であるべきです。バックフィル後にusage_aggregateをusage_eventからいつでも再構築できる設計にしてください。
冪等性とイベントのライフサイクル状態
使用データはノイズが多いです。クライアントはリトライするし、キューは重複を届け、StripeのWebhookは順序が前後します。データベースが「この使用イベントはすでにカウントされた」と証明できないといずれ二重請求が発生します。
各使用イベントに安定で決定的なevent_idを与え、それに対して一意制約を設けてください。単にオートインクリメントIDだけを頼りにしないでください。良いevent_idはビジネスアクションから導出されます(例:customer_id + meter + source_record_id または customer_id + meter + timestamp_bucket + sequence)。同じアクションが再送されれば同じevent_idになり、挿入が安全なno-opになります。
冪等性は公開APIだけでなくすべての取り込み経路をカバーする必要があります。SDK呼び出し、バッチインポート、ワーカージョブ、Webhookプロセッサ―はすべてリトライされます。ルールは簡単:入力がリトライされうるなら、データベースに保存された冪等キーをチェックして、合計が変わる前に確認すること。
シンプルなライフサイクル状態モデルを持つとリトライが安全になり、サポートもしやすくなります。明示的にし、失敗時には理由を保存します。
received: 保存済み、まだ検証されていないvalidated: スキーマ、顧客、メーター、時間窓ルールを通過posted: 請求合計にカウント済みrejected: 永久に無視(理由コード付き)
例:ワーカーがvalidatedにした後でクラッシュしposted前に止まったとします。再試行時に同じevent_idを見つけてvalidatedのまま残っていれば、そのままpostedに進み、重複イベントは作られません。
StripeのWebhookでも同じパターンを使ってください:Stripeのevent.idを保存し、一度だけ処理済みにマークすることで重複配信が無害になります。
メータリングイベントの取り込み(エンドツーエンド)
すべてのメータリングイベントを「お金」と同様に扱ってください:検証し、オリジナルを保存し、そこから合計を導出する。この順序があれば、システムがリトライや遅延データに直面しても請求は予測可能です。
信頼できる取り込みフロー
各イベントは合計を触る前に検証してください。最低限必要なのは:安定した顧客識別子、メーター名、数値の数量、タイムスタンプ、一意のイベントキー(冪等性)です。
後で集計するつもりでも、まず生イベントを書いてください。その生レコードを再処理し、監査し、推測なしに問題を修正します。
堅牢なフローは次のようになります:
- イベントを受け付け、必須フィールドを検証し、単位を正規化(例:秒→分)。
- イベントキーをユニーク制約として使い、生の使用イベント行を挿入。
- 日次や請求期間ごとのバケットに集計を反映。
- Stripeに使用量を報告する場合は、送信した内容(meter、quantity、period)とStripeの応答識別子を記録。
- 異常(拒否イベント、単位変換、遅延到着)を監査用にログ。
集計は再実行可能に保ってください。一般的な方法は:生イベントをトランザクションで挿入し、その後ジョブをキューに入れてバケットを更新する方法です。ジョブが二度実行されても、生イベントが既に適用済みであることを検出できるようにします。
顧客が"12,430回のAPIコール"についてなぜ請求されたのかを問うたとき、その請求ウィンドウに含まれる生イベントの正確な集合を提示できるべきです。
StripeのWebhookと自分のDBの突合せ
WebhookはStripeが実際に行ったことのレシートです。アプリ側でドラフトを作成し使用量を送っても、請求状態はStripeがそう述べたときにのみ現実になります。
多くのチームは請求結果に影響する少数のWebhookタイプに注力します:
invoice.created,invoice.finalized,invoice.paid,invoice.payment_failedcustomer.subscription.created,customer.subscription.updated,customer.subscription.deletedcheckout.session.completed(Checkoutでサブスクリプションを開始する場合)
受け取ったWebhookはすべて保存してください。生のペイロードに加え、Stripeのevent.id、event.created、署名検証結果、サーバー受信時刻などを保存します。問題の調査や「なぜ請求されたか?」への回答時にこの履歴が役立ちます。
堅牢で冪等な突合せパターンの例:
stripe_webhook_eventsテーブルにevent_idで一意制約を付けてWebhookを挿入。- 挿入が失敗したらそれは再試行なので処理を止める。
- 署名を検証し、結果を記録。
- StripeのID(customer、subscription、invoice)で内部レコードを検索してイベントを処理。
- レコードの状態を後退させないルールで状態変更を適用。
順序入れ替わりは普通に起きます。"最大の状態が勝つ(max state wins)"ルールとタイムスタンプを使い、レコードを後退させないようにしてください。
例:invoice.paidを受け取ったが内部の請求書行がまだ存在しない場合、まず「Stripeから確認した」として行を作り、後でStripeの顧客IDで正しいアカウントに紐づける、という流れにすれば台帳の一貫性を保ちながら二重処理を防げます。
使用合計から請求書の行項目へ
生の使用量を請求行に変換する際に重要なのはタイミングと境界です。リアルタイムで合計が必要か(ダッシュボードやアラート)、請求時のみでよいかを決めてください。多くのチームは両方を行い、イベントは継続的に書き込み、請求用の合計はスケジュールジョブで計算します。
使用の窓(ウィンドウ)はStripeの請求期間と合わせてください。暦月を勝手に推測しないこと。サブスクリプションアイテムの現在のbilling period start/endを使い、そのウィンドウに入るタイムスタンプのイベントのみを合算します。タイムスタンプはUTCで保存し、請求ウィンドウもUTCに揃えます。
履歴は不変にしてください。後で誤りが見つかったら古いイベントや過去の合計を編集するのではなく、元のウィンドウを参照する調整レコードを作成して数量を加減します。監査しやすく説明しやすくなります。
プラン変更や端数(proration)は追跡性が失われやすい箇所です。顧客が期間中にプランを切り替えたら、価格ごとの有効レンジに合わせて使用量をサブウィンドウに分割してください。請求書は2つの使用行(または1行+調整)にして、それぞれ特定の価格と時間範囲に紐づけます。
実用的なフロー:
- Stripeから請求ウィンドウのperiod start/endを取得。
- そのウィンドウ内で適格な使用イベントを集計して使用合計を作る。
- 使用合計と調整をもとに請求書の行項目を生成。
- 計算実行IDを保存して後から再現できるようにする。
バックフィルと遅延データを信頼を壊さず扱う方法
遅延データは日常です。デバイスがオフラインになる、バッチジョブが遅れる、パートナーがファイルを再送する、ログが障害後に再生される、などが起こります。鍵はバックフィルを修正作業として明示的に扱い、数値を"合わせるため"に履歴を書き換えるような運用にしないことです。
バックフィルの発生元(アプリログ、データウェアハウスのエクスポート、パートナーシステムなど)を明示してください。イベントごとにソースを記録して遅延の理由を説明できるようにします。
バックフィル時には少なくとも二つのタイムスタンプを保持します:発生時刻(請求対象にしたい時刻)と取り込み時刻です。イベントにbackfilledタグを付け、履歴を書き換えないでください。
デルタを当てるよりも生イベントから合計を再構築する方を優先してください。再実行(replay)はバグからの回復方法です。パイプラインが冪等であれば、1日分、1週間分、あるいは請求期間全体を再実行しても同じ合計が得られるはずです。
請求書が既に存在する場合の修正ポリシーを明確にしておきます:
- 請求書が確定していなければ、確定前に再計算して更新する。
- 確定済みかつ過少請求なら、追加請求(または新しいinvoice item)を作り、説明を付ける。
- 確定済みかつ過剰請求なら、クレジットノートを発行して元の請求書を参照する。
- 誤魔化すために使用量を別の期間へ移動しない。
- 修正理由(partner resend、delayed log delivery、bug fixなど)を短く保存する。
例:パートナーが1月28–29日のイベントを2月3日に送ってきた場合、occurred_atは1月にし、ingested_atは2月にしてソースを"partner"と記録する。1月の請求が既に支払済みなら、欠落分について説明付きで小額の追加請求を出す、という流れです。
二重計上を引き起こすよくあるミス
二重計上は「メッセージが到着した」を「アクションが発生した」と同一視することで起きます。リトライ、遅延Webhook、バックフィルがあると、顧客のアクションと処理を分離する必要があります。
典型的な原因:
- リトライを新たな使用と扱う。すべてのイベントが安定したアクションID(request_id、message_id)を持たず、DBで一意を保証しないと重複カウントが発生します。
- 発生時刻と処理時刻を混同する。取り込み時刻でレポートすると遅延イベントが別期間に入ったり、再再生時に再カウントされたりします。
- 生イベントを削除・上書きする。ランニングトータルだけを保持すると何が起きたか証明できず、再処理で合計が膨らみます。
- Webhookの順序を前提にする。Webhookは重複、順序入れ替え、部分的状態を表すことがあるので、StripeオブジェクトIDで突合せし、「既に処理済み」ガードを置いてください。
- キャンセル/返金/クレジットを明示的にモデル化していない。追加だけしてマイナス調整がないと、修正時に重複が発生します。
例:"10回のAPIコール"を記録し、その後障害で2回分をクレジットしたとします。もし同じ日の使用をバックフィルで再送し、かつクレジットも適用すると、顧客の表示は(10 + 10 - 2) = 18回になってしまい、本来の8回と異なります。
本番導入前の簡単チェックリスト
使用量課金を本当に顧客向けに有効化する前に、費用のかかる請求バグを防ぐ基本を最終確認してください。多くの失敗は"Stripeの問題"ではなくデータの問題(重複、日付欠落、無言リトライ)です。
短く実行可能なチェックリスト:
- 使用イベントに一意性を強制する(例:
event_idのユニーク制約)とID戦略を決める。 - 受け取ったWebhookはすべて保存し、署名を検証し、冪等に処理する。
- 生の使用は不変に扱い、調整(正負どちらでも)で修正する。
- 内部合計(顧客ごと、メーターごと、日ごと)とStripeの請求状態を比較する日次突合せジョブを走らせる。
- 欠落日、負の合計、急激なスパイク、取り込み済みイベント数と請求済みイベント数の大きな差などのアラートを設定する。
簡単なテスト:1人の顧客を選び、過去7日分の取り込みを再実行して合計が変わらないことを確認してください。変わるならまだ冪等性かバックフィルの問題があります。
例シナリオ:現実的な1か月の使用と請求
サポートチームが会話1件あたり$0.10で請求する小さなサービスを提供しているとします。データが乱れるときにどう信頼を保つかが重要です。
3月1日に請求期間が始まります。エージェントが会話をクローズするたびにアプリが使用イベントを発行します:
event_id: アプリ由来の安定したUUIDcustomer_idとsubscription_item_idquantity: 会話1件occurred_at: クローズ時刻ingested_at: 最初に見つけた時刻
3月3日にバックグラウンドワーカーがタイムアウト後に同じ会話を再送しました。event_idが一意なので二回目の挿入はno-opとなり合計は変わりません。
中旬にStripeが請求プレビューと確定済みの請求書のWebhookを送ってきます。Webhookハンドラはstripe_event_id、type、received_atを保存し、DBトランザクションがコミットされた後にのみ処理済みとマークします。Webhookが二度届いても二回目はstripe_event_idが既にあるため無視されます。
3月18日にモバイルクライアントがオフラインで溜めていた3月17日の会話35件をバッチインポートしたとします。これらは古いoccurred_atを持ちますが有効です。システムは挿入し、3月17日のデイリートータルを再計算し、その追加使用量はまだ請求期間内なら次の請求に含まれます。
3月22日にバグで同じ会話が異なるevent_idで二重記録されているのを発見した場合、履歴を削除するのではなくquantity = -1の調整イベントを記録し、理由を"duplicate detected"のように残します。こうすることで監査トレイルが保たれ、請求の変更も説明可能になります。
次のステップ:実装、監視、段階的強化
小さく始めてください:1つのメーター、1つのプラン、よく理解している顧客セグメントで。目標はシンプルな一貫性です—あなたの数値がStripeと月ごとに一致し、驚きがないこと。
小さく作り、堅牢化する
実用的な最初のローンチ案:
- 1つのイベント形状を定義(何をカウントするか、どの単位で、どの時刻を見るか)。
- すべてのイベントをユニークな冪等キーと明確なステータスで保存。
- 日次(あるいは時間単位)で集計して請求を説明可能にする。
- Stripeとの突合せをスケジュールジョブでも行い、リアルタイムだけに頼らない。
- 請求後は期間をクローズし、遅延イベントは調整パスで処理する。
ノーコードツールであってもデータ整合性は保てます。無効な状態を起こさない(冪等キーにユニーク制約、顧客/サブスクリプションへの外部キーを必須、受け入れた生イベントを更新しない)ように設計してください。
後で助かる監視
早い段階で簡単な監査画面を作っておくと初めて"なぜ今月請求が高いのか?"と聞かれた時に非常に役立ちます。便利なビューの例:顧客と期間でイベント検索、期間ごとの日別合計、Webhook処理状況の追跡、バックフィルと調整の誰が/いつ/なぜの履歴。
もしAppMaster (appmaster.io)で実装するなら、このモデルは自然に適合します:Data Designerで生イベント、集計、調整を定義し、Business Processesで冪等な取り込み、スケジュール集計、Webhook突合せを作ります。手作業で配管処理を書くことなく、実際の台帳と監査トレイルを得られます。
最初のメーターが安定したら次を追加してください。ライフサイクルのルール、監査ツールのセット、そして同じ習慣(変更は一つずつ行い、エンドツーエンドで検証)を保つことが大事です。
よくある質問
使用量課金は小さな台帳(レジャー)のように扱ってください。課金そのものより難しいのは、イベントが遅れて届いたり、重複したり、修正が必要になった場合でも、何がいつカウントされたかを正確に説明できる記録を保つことです。
安全な初期設定としては、あなたのデータベースが生データ(raw usage events)とそのステータスの信頼できる根拠(source of truth)となり、Stripeは請求や支払い結果の信頼できる根拠である、という分担が有効です。こうすることで請求の追跡性を保ちながら、価格設定・税金・回収はStripeに任せられます。
再送やリトライで同じ操作に対して同じ識別子が返るよう、安定かつ決定的なキーにしてください。よくある実装は、顧客ID+メーターキー+ソースのレコードIDの組み合わせなど、実際のビジネスアクションから導出する方法です。これにより重複送信は無害なno-opになります。
受け入れ済みの使用イベントを編集・削除しないでください。修正が必要な場合は代わりに補正用の調整イベント(必要ならマイナス数量)を記録し、元の履歴を残します。これにより後から履歴を説明できます。
生の使用イベントは追記専用(append-only)で保持し、集計は派生データとして別に保存して再構築できるようにします。集計は表示や請求作成のために速くなるように使い、監査や不正の確認、バグ修正は生データからやり直せるようにします。
少なくとも「発生時刻(occurred_at)」と「取り込み時刻(ingested_at)」の2つのタイムスタンプを保持し、ソースを記録してください。請求がまだ確定していなければ再計算して反映し、請求が確定済みであれば追加請求やクレジットで明確に修正扱いにします。履歴を別の期間へこっそり移すのは避けてください。
受け取ったすべてのWebhookペイロードを保存し、Stripeのevent idをユニークキーとして処理を冪等にしてください。Webhookは重複や順序入れ替わりが起きるので、同じevent idが既に処理済みなら再処理をスキップする設計にします。
請求期間の開始・終了はStripeのサブスクリプションアイテムが示す期間を使い、価格が変わるタイミングで使用量を分割してください。各請求行が特定の時間帯と価格に結びつくようにすれば、合計が説明可能になります。
集計ロジックがどの生イベントを含めたかを証明できるようにし、計算実行IDなどのメタデータを保存して再現性を持たせてください。同じウィンドウで取り込みを再実行して合計が変わるなら、まだ冪等性かライフサイクル管理に問題があります。
Data Designerで生使用イベント、集計、調整、Webhook受信用のテーブルを定義し、Business Processesで取り込みや突合せを実装します。ユニーク制約で冪等性を担保すれば、監査可能な台帳とスケジュールされた突合作業をコードを大量に書かずに構築できます。


