🏧

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)

  1. フロントが /billing/checkout-session を呼ぶ
  2. FastAPI が Stripe API を使い Checkout Session を作成
  3. session_id / session_url をフロントに返却
  4. フロントが 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