結論
Webhook を「通知」ではなく 非同期の事実ストリーム として扱うと設計が安定します。
Stripe の公式仕様上も、署名検証、raw body、再送、手動再処理は避けて通れません。
タイトル通り、実務で本当に多い失敗を 10 個に分けて整理します。
起きやすい失敗 10 個
- 署名検証を入れていない
- raw body を壊してから検証している
- CLI 用 secret と Dashboard 用 secret を混同している
- イベント保存前に副作用を起こしている
- event ID 単位の重複防止がない
- 同期レスポンスと Webhook の両方で注文確定している
- イベントが順番通りに来る前提で内部状態を組んでいる
- 遅延決済のイベントを処理対象に入れていない
- 手動再処理導線がなく、障害時に API と SQL を直叩きする
- Connect で「自分のアカウント用」と「接続アカウント用」の Webhook を分けていない
最初に潰すべき 4 つ
1. 署名検証を入れていない
Stripe は Stripe-Signature ヘッダーでイベントを検証できます。
これを外すと、到達した JSON をそのまま信じる設計になります。
2. raw body を壊している
Stripe は 生のリクエスト本文 で署名検証します。
express.json() で先に JSON へ変換すると、同じ内容に見えても検証に失敗します。
3. イベント保存前に副作用を起こしている
メール送信、在庫引当、権限付与を先にやると、途中失敗時に再実行できません。
まず event ID と payload を保存して、その後に業務処理へ進める方が安全です。
4. event ID 単位の重複防止がない
Stripe は未配信イベントを自動再送します。
同じ event.id を二度処理しても副作用が一回で止まるようにしないと、二重反映が起きます。
実装順のおすすめ
Step 1: 受信して署名検証し、まず保存する
受信直後に業務処理を全部やるより、受けたイベントをまず保存してから後段に渡す方がトラブルに強いです。
保存時点で最低限持ちたいもの:
event.idevent.typelivemodeaccount(Connect の場合)- 受信時刻
- 処理ステータス
- 失敗理由
Step 2: 副作用は一箇所に寄せる
メール送信、在庫反映、契約更新などの副作用が複数箇所に散ると、再送時に事故ります。
Step 3: 「未処理」「処理中」「処理済み」を持つ
Stripe の手動再処理ガイドでも、処理中・処理済みの記録を DB に持って重複を防ぐ考え方が出てきます。
Step 4: 運用画面を先に作る
本番で強いのは、イベント単位で「受信」「処理済み」「失敗」「再実行」を見られる画面です。
見落としやすい失敗
順不同イベントを直列フローだと思い込む
たとえば invoice.paid より先に customer.subscription.updated を見たり、payment_intent.succeeded より先に周辺イベントを見ることがあります。
イベントの到着順ではなく、取得したオブジェクトの現在状態 を正にしてください。
success ページを正とする
Checkout の戻り先ページは UI の完了表示であって、バックエンドの確定処理そのものではありません。
同期レスポンスで確定し、さらに Webhook でも確定すると二重計上の温床になります。
Connect の Webhook を普通の Webhook と同じだと思う
Connect では「自分のアカウントのイベント」と「接続アカウントのイベント」の 2 系統があります。
接続アカウントのイベントにはトップレベルの account プロパティが付きます。
再送を待つだけで手動回復手順がない
Stripe は未配信イベントを最大 3 日程度自動再送しますが、それとは別に undelivered events を手動処理する手順を用意しておくべきです。
監視で最低限ほしいもの
| 項目 | 目的 |
|---|---|
| 受信件数 | 受信停止の検知 |
| 失敗件数 | 異常増加の検知 |
| 再送件数 | 下流不安定の検知 |
| 最終処理時刻 | 遅延の検知 |
livemode 別件数 | test / live 混入の検知 |
account 別失敗率 | Connect 事故の局所化 |
併読推奨
よくある質問
Webhook は順番通りに来る前提でよいですか?
その前提は危険です。内部状態をイベント順に依存させすぎない方が安全です。