はじめに
新規プロダクトの決済機能を担当することになったため、
本記事では決済方式の方針決定から設計の考え方までを整理します。
「Stripeを使うのは決めたが、どこまで任せるべきか」「どういう判断軸で選んだのか」
といった点が、これから決済機能を実装する方の参考になれば幸いです。
※なお私だけでなく、チームでもんだ上での設計です
決済システムを作る上での大前提
クレジットカード決済を扱う上で、まず押さえておくべき前提があります。
決済を利用する開発者としては、PCI DSSに準拠した外部サービスを用いて、
カード情報を一切触らずに決済システムを構築するのが基本です。
PCI DSSとはクレカ情報を扱うシステムが守らないといけない世界共通のセキュリティ基準です。
この基準を自前で満たすには、インフラ・運用・監査体制など、非常に高いハードルを越える必要があります。
そのため、クレジットカード決済を導入したい多くの企業にとって自前実装は現実的ではありません。
外部サービスを利用し、カード情報を一切扱わない構成を取ることで
セキュリティリスクと運用コストを最小化な構成を実現するというのが
クレカ決済を利用する開発者として、ベストな構成かと思います。
参考:
なぜStripeを採用したの?
決済サービスは色々有り、PayPalなどもありますが、Stripeを採用しています。
理由は以下の通りです。
- 日本語ドキュメントが丁寧
- 学習コストが高くない
- SDKが充実(Nodeあり)
- StripeコネクターがありSaleforceの連携ができる
- Checkout / Billing など、決済管理用のUIが標準で提供されている
- チームにStripeの知見あるエンジニアがいた
技術的な優位性だけでなく、チームとしての運用・学習コストが低いことも大きな判断材料でした。
どこまで決済UIをStripeに任せるか?
決済ロジック自体は Stripe に任せるとして、
設計上の論点になったのが「決済画面をどこまで自前で作るか」です。
カード番号などを入力する決済画面については、主に以下の選択肢があります。
- 候補1: Stripe Checkout
- Stripe が提供する決済画面をそのまま利用する
- 候補2:Stripe Elements
- 入力コンポーネントのみ Stripe 管理のものを使い、UIは自前で構築する
Stripe Checkoutのメリット
- 決済UIを実装・保守する必要がない
- PCI DSS 対応をより強く Stripe に委譲できる
Stripe Elementsのメリット
- UIの自由度が高い
- 決済画面に柔軟な表現や導線を持たせられる
Stripe Elementsは、決済画面自体を
プロダクトのUXとして作り込みたい場合に有効な選択肢です。
一方で Stripe Checkoutでも、プラン選択などの画面のみを自前で実装し、
「どのプランを選択したか」だけを Stripe に渡す、という構成が可能です。
今回のプロダクトでは、
決済画面にそこまで高い柔軟性は求められていなかったため、
決済画面はStripe Checkoutに任せる構成を採用しました。
図示して決済フローを整理する
公式が提唱するCheckoutのライフサイクルに沿って設計した処理フローの図です。
- 顧客情報(email)などをsession情報とし、リダイレクトさせる決済画面のURLを作成する
- 決済画面(カード入力)はStripeに任せる
- 支払いの確定はWebhookを正とし、その後にアプリ側のDB側操作を行う
- フロントは「完了したか」をバックエンドに問い合わせて表示する
より細かいダイアグラムで書くとこんな感じです。
実際には署名チェックなども行っています。
Webhookの設計
Stripeでは、支払い完了・失敗といった決済結果は
Webhook を通じて非同期に通知されます。
画面遷移(success_url など)はあくまでユーザー体験のためのものであり、
支払いが確定したかどうかの正はWebhook として扱うのが前提になります。
本実装では、以下の Stripe公式ドキュメントを参考に Webhook 設計を行いました。
- 支払いイベントの取り扱い
- Webhook のベストプラクティス
Webhook設計で意識したポイント
Webhook 設計で特に意識したのは、以下の点です。
- Webhookを支払い確定の正とする
- イベントは必要最小限に絞る
- 署名検証と冪等性を前提にする
- リトライは「治るものだけ」許可する
1. 支払い確定の正はStripeとする
本構成では、支払いの正は Stripe が管理する決済状態とし、
Webhook はその状態変化をバックエンドへ通知する手段として扱います。
バックエンドでは、Webhook を通じて受け取ったイベントを基に
決済結果を記録し、以降の処理を進めます。
フロントでは、状態を管理せず問い合わせるのみです。
これにより、
- 画面リロード
- ブラウザ離脱
- 二重遷移
といったケースでも、決済状態が不整合を起こさない構成になります。
各レイヤーの責務整理
-
Stripe
- 決済の状態管理
- 価格・プランなどの決済関連マスタの管理
-
バックエンド / DB
- Webhook イベントを基にした決済結果の記録
- ユーザー権限や契約状態の作成・更新
-
フロントエンド
- 状態を持たず、表示のみに専念
- 決済結果はサーバーへ問い合わせて取得する
2. 受け取るイベントは最小限に絞る
Webhook で受け取るイベントは、必要なものだけに限定しました。
- 支払い成功:
invoice.paid - 支払い失敗:
invoice.payment_failed
成功・失敗の判定に不要なイベントは設定段階で受信しないことで、
実装の複雑さと誤処理のリスクを下げています。
設定しないとシンプルにすべてのイベントがWebhookとして飛んできます。
サーバーに不要な不可がかかるので、Webhookの作成時に設定しましょう
3. ヘッダー署名の検証を必須にする
Webhookは外部から直接呼ばれるエンドポイントのため、
Stripe が付与する署名ヘッダーを必ず検証しています。
署名が検証できないリクエストは処理せず、
正当なStripeからの通知のみを受け付けるようにしました。
4. 重複イベントを前提にし、冪等に処理する
StripeのWebhookは、同じイベントが複数回送信される可能性があります。
そのため、
- イベントIDを基準に重複処理を防止
- すでに処理済みのイベントはスキップ
という形で、冪等性を前提とした実装にしています。
5. リトライ方針を明確に分ける
Webhookはアプリ側でエラーが起こり500のレスポンスを返した場合、200が変えるまでリトライします。
これはハンドリングとしてありがたいのですが、何らかの不具合でWebhookから欲しいデータが送られてこなかった場合、リトライしても意味がないです。
Webhook では「再送してほしいケース」と「再送しても意味がないケース」を
明確に分けて扱いました。
-
ネットワークエラー・一時的な障害
- 4xx,5xxを返してStripe側にWebhookを再送させる
-
データ欠損・想定外フォーマット
- 200を返して再送を止める
- Slackに通知して手動対応
再送で解決しないケースを無限にリトライさせない、
という運用面を意識しています。
まとめ
新規プロダクトにおけるStripe Checkoutを用いた決済フローについて、
方針決定から設計の考え方までを整理しました。
Stripe Checkout のライフサイクルや公式のベストプラクティスに沿って設計したことで、
Stripe の思想に沿ったシンプルで安全な構成を取ることができたと感じています。
特に良かった点は以下です。
- Stripe が決済状態・価格などのマスタを一元管理してくれるため、アプリ側は「通知を受けて記録する」責務に集中できた
- 日本語ドキュメントが非常に充実しており、設計段階で迷った点の多くが公式情報で解決できた
- API やイベントがすべてオブジェクト単位で整理されており、Stripe 側の状態遷移を追いやすかった
一方で、Stripe 側のオブジェクト構造が明確である分、アプリ側で必要な情報だけを切り出す実装はやや手間に感じる部分もありました。
ただしこれは、責務を明確に分離した結果とも言えるため、トレードオフとして受け入れられる範囲だと考えています。
Billing や課金ロジックの詳細については、今回は決済フローの本質から外れるため割愛しましたが、
機会があれば別記事としてまとめたいと思います。

