Stripe Webhookをやめてローカル DB 同期に切り替える手順


Stripeを使っていると、Webhookの運用が地味にしんどくなる瞬間がある。イベントが順番通りに届かない、再送処理を考慮した冪等性の実装、エンドポイントが落ちたときのリカバリ。こうした「Webhook地獄」から抜け出す方法として、StripeのデータをAPIで定期取得して自前のDBに同期するアプローチがある。

結論から言うと、この方式は「リアルタイム性がそこまで必要ない」かつ「運用の複雑さを減らしたい」ケースでかなり有効だ。ただし、設計上の制約もあるので、自分のプロダクトに合うかどうかは見極めが必要になる。

Webhookの「運用上のクセ」を知っておく

まず、なぜWebhookをやめたくなるのかを整理しておく。

Stripeの公式ドキュメントによると、Webhookには以下のような特性がある。

  • イベントの順序は保証されない
  • ライブモードでは配信失敗時に最大3日間、指数バックオフで自動リトライされる
  • 手動再送はダッシュボードから15日、CLIから30日まで

つまり、Webhookを受け取る側は「順番バラバラで届くかもしれない」「同じイベントが何回も届くかもしれない」という前提でコードを書く必要がある。冪等性の担保、タイムスタンプ比較による古いデータの破棄、署名検証の実装…。これらを全部正しくやるのは、思った以上に手間がかかる。

特に署名検証は厄介で、Stripeから届いた生のリクエストボディをそのまま使わないと検証に失敗する。フレームワークが自動でJSONパースしてしまう環境だと、ハマりやすいポイントだ。

APIで同期する方式のメリット

Webhookをやめて、Stripe APIから定期的にデータを取得する方式に切り替えると、以下のメリットがある。

  • 自分のタイミングでデータを取りに行ける(エンドポイントを公開しなくていい)
  • 順序の制御が自分側でできる
  • 再送・冪等性の考慮がシンプルになる

バッチ処理として5分ごと、あるいは1時間ごとにStripeのAPIを叩いて、差分をDBに反映する。Webhookのように「いつ届くかわからない」状態から解放されるのは、精神衛生上もいい。

切り替えの手順

実際にWebhookからAPI同期に移行する場合、以下の順序で進めると安全だ。

1. 同期対象のリソースを決める

Stripeには大量のリソースがあるが、自分のサービスで本当に必要なものだけに絞る。よく使うのは SubscriptionsInvoicesPaymentIntents あたりだろう。全部同期しようとすると、データ量もAPI呼び出し回数も膨れ上がる。

2. 初回フル同期を実行する

Stripeのlist系APIはページングが基本だ。デフォルトでは1ページ最大10件で、limit パラメータで調整できる。レスポンスに has_more: true が含まれていたら、最後のオブジェクトのIDを starting_after に指定して次のページを取得する。

let hasMore = true;
let startingAfter = null;

while (hasMore) {
  const params = { limit: 100 };
  if (startingAfter) {
    params.starting_after = startingAfter;
  }
  
  const response = await stripe.subscriptions.list(params);
  
  // DBに保存する処理
  await saveToDatabase(response.data);
  
  hasMore = response.has_more;
  if (response.data.length > 0) {
    startingAfter = response.data[response.data.length - 1].id;
  }
}

このカーソル管理をサボると、途中でスクリプトが落ちたときに取りこぼしが発生する。最終同期位置を保存しておく仕組みは必須だ。

3. 増分同期の仕組みを作る

初回同期が終わったら、以降は差分だけを取得する。Events v2のAPIを使う場合、created.gtcreated.gte で時刻フィルタができるので、前回同期時刻以降のイベントだけを取得できる。

ただし、Events v2は最大30日分までしか遡れないという制約がある。これは重要なポイントで、30日以上同期が止まっていた場合は、イベントベースでは整合性が取れなくなる。そういうケースに備えて、週次や月次で全件再同期をかける仕組みを入れておくと安心だ。

4. 移行期間はWebhookと並行運用する

いきなりWebhookを切るのはリスクが高い。移行期間中は両方を動かして、Webhookで受け取ったデータとAPI同期で取得したデータを突合する。差異が出たら原因を調査して、API同期側のロジックを修正する。

この期間が1〜2週間あれば、だいたいの問題は洗い出せる。

5. 監視を入れる

同期バッチがコケたまま気づかない、というのが一番怖いパターンだ。最低限、以下は監視しておきたい。

  • 同期処理の成功/失敗
  • 前回同期からの経過時間
  • Stripe側の最新データとの差異(件数ベースでもいい)

6. Webhookを停止する

並行運用で問題がなければ、Stripe Dashboardからエンドポイントを無効化する。削除ではなく無効化にしておけば、万が一のときに戻しやすい。

この方式が向かないケース

正直に書くと、API同期が向かないケースもある。

リアルタイム性が必要な場合。たとえば「支払い完了と同時にコンテンツを解放する」みたいな機能は、数分の遅延が許容できないならWebhookのほうが適している。

同期対象が多すぎる場合。API呼び出しにはレートリミットがあるし、データ量が膨大だとバッチ処理自体が重くなる。

そういうケースでは、Webhookを残しつつ「取りこぼしのリカバリ用にAPI同期も併用する」というハイブリッド構成が現実的だ。

まとめ

Webhookの運用がつらくなったら、API同期への切り替えを検討する価値はある。順序保証なし・再送あり前提の実装から解放されるのは大きい。ただし、30日制限やリアルタイム性のトレードオフは理解した上で選択してほしい。

移行は一気にやらず、並行運用期間を設けてじっくり検証するのがコツだ。

参考リンク