🏧
Stripe 課金を入れてみた
はじめに
無料で課金処理を組み込みたく、stripeがよさげなので入れてみました。
素人で形式からわかっていなかったので、体系から調べました。
全体構成
Stripe 課金は以下の要素で構成されます。
- フロント(Web / Flutter)
- バックエンド(FastAPI)
- Stripe Checkout(決済UI)
- Stripe Webhook(状態通知)
- 課金状態を保存する DB
- 課金状態を返す API(/billing/status)
フロントの表示もstripeに任せ、決済完了は別経路でwebhookからもらう、が特徴的でした。

フロントの役割
フロントエンドは課金処理を行いません。
- 課金ボタンを表示する
- /billing/checkout-session を呼ぶ
- Stripe の決済画面へ遷移する
- /billing/status の結果を表示する
カード入力フォームや決済UIはフロントでは実装しません。
Stripe Checkout の役割
Stripe Checkout は、Stripe が提供する完成済みの決済画面です。
担当範囲は以下です。
- カード入力画面
- 3Dセキュア対応
- Apple Pay / Google Pay
- セキュリティ・PCI DSS 対応
一方、DB 更新やサブスクリプション状態の管理は行いません。
課金開始フロー(Checkout Session)
- フロントが /billing/checkout-session を呼ぶ
- FastAPI が Stripe API を使い Checkout Session を作成
- session_id / session_url をフロントに返却
- フロントが Stripe の決済画面へ遷移
この時点では支払いはまだ確定していません。
success_url を信用してはいけない理由
success_url は UX のための戻り先であり、
課金状態の正ではありません。
- ユーザーが画面を閉じる可能性がある
- 通信が切れる可能性がある
- 後から支払い失敗になることがある
- 解約予約や期間満了が発生する
DB 更新の根拠には使えません。
Webhook の役割
Webhook は、Stripe がサーバーに送信する状態確定通知です。
- ユーザー操作に依存しない
- 失敗時は再送される
- Stripe 側で確定した情報が届く
課金状態の正は Webhook です。
Webhook の形式
Webhook は通常の HTTP POST として届きます。
{
"id": "evt_xxx",
"type": "customer.subscription.updated",
"data": {
"object": {
"id": "sub_xxx",
"status": "active",
"current_period_end": 1735689600,
"cancel_at_period_end": false
}
}
}
バックエンド(Webhook)の責務
- Stripe-Signature による署名検証
- event.id を用いた二重処理防止
- Stripe の事実を DB に保存
フロントは Webhook 処理に関与しません。
DB設計の考え方
DB には Stripe が持つ事実のみを保存します。
- stripe_customer_id
- stripe_subscription_id
- subscription_status
- cancel_at_period_end
- current_period_end
- latest_invoice_status
- last_event_id
- last_event_at
判定結果は保存しません。
/billing/status の役割
フロントは Stripe を直接参照せず、
/billing/status の結果のみを使用します。
- プラン
- サブスクリプション状態
- 有効期限
- 解約予定
有効/無効判定は API 側で計算します。
まとめ
- 課金UIは Stripe が提供する
- フロントは遷移のみを担当
- 課金状態の正は Webhook
- DB には Stripe の事実だけを保存
- フロントは /billing/status だけを見る
Discussion