午前2時にデータベースがダウンしました。目を覚まして直すまでに、3 時間分の Webhook がすでに送られ、拒否されていました。今は Slack を見つめながら、処理されていない Stripe の支払いがいくつあるのか、正しい CI ジョブを起動しなかった GitHub の push がいくつあるのかを考えています。
この記事では、取りこぼした Webhook を復旧する実際の手順を、プロバイダーごとに説明します。そのうえで、これを日曜の午後をつぶす作業ではなく、クリック 1 回で済む問題に変えるパターンを紹介します。
TL;DR
- Stripe:ダッシュボードからイベントごとに手動で再送するか、Events API を使ってスクリプトで処理します。一括操作の UI はありません。
- GitHub:配信ごとに再配信ボタンがあります。こちらも、Redelivery API をスクリプトで叩かない限り 1 件ずつです。
- Shopify:4 時間で 8 回リトライし、その後イベントは消えます。照合には Orders API に改めて照会します。
- クリック 1 回の再送には、配信前に上流で記録しておくことが必要です。Webhook 中継サービスは、まさにそのために設計されています。
Stripe
Stripe では、ダッシュボードから Webhook イベントを手動で再送できます。
- Stripe ダッシュボードで Developers → Events(開発者 → イベント)を開きます
- 再送したいイベントをクリックします
- Webhook attempts(Webhook の試行)までスクロールし、再送する(Resend)をクリックします
これは 1 件ずつしかできず、イベントの作成から 15 日以内に限られます。300 件を取りこぼしたなら、ボタンを 300 回クリックすることになります。Stripe CLI のコマンド(stripe events resend <event_id>)もありますが、やはり各イベントの ID を知っている必要があります。
Stripe のイベントを一括で再送する
公式の一括再送 UI はありません。回避策は次のとおりです。
- Stripe API で障害期間中のイベントを一覧し、ステータスで絞り込み、1 件ずつ自分でエンドポイントに POST し直すスクリプトを書きます。イベントオブジェクトから Webhook を組み立て直すことになり、形はまったく同じにはなりません。
- あるいは、障害の上流で Webhook を記録していたなら、自分のログから再送します
本番運用しているチームが最終的に作るのは、上流でログを取る方法です。そしてそれは、Webhook 中継サービスがまさに代わりにやってくれることでもあります。
GitHub
GitHub では、リポジトリの Settings → Webhooks → 対象の Webhook → Recent Deliveries(最近の配信)に、配信ごとの再配信ボタンがあります。それぞれの横にある Redeliver(再配信)をクリックします。
問題は同じで、1 件ずつです。GitHub で再配信できるのは過去 3 日間の配信だけで、まとめて再送するなら、やはり Redelivery API を叩くスクリプトが必要です。そして手元にあるのは、そのログだけです。GitHub は失敗した配信を自分ではリトライしないからです。
Shopify
Shopify は、4 時間の間に 8 回リトライしてから諦めます。その後に復旧する唯一の方法は、取りこぼした分を Shopify API に改めて照会することです。Webhook そのものは消えています。
これが特につらいのは、Shopify で復旧できる期間が、主要 3 社の中で群を抜いて短いからです。金曜の夜にサーバーがダウンし、月曜まで気づかなければ、Orders API から全面的に照合し直すことになります。
一般的なパターン
プロバイダーにかかわらず、中継サービスなしでの復旧手順は次のようになります。
- 障害の期間を特定します。サーバーが 2xx を返さなくなったのはいつですか?
- 影響を受けたイベントを取得します。たいていはプロバイダーごとに、ページネーション付きの API 呼び出しです
- 各イベントを自分のエンドポイントに POST して発火し直すか、正となるデータソースに照会し直して、手作業で状態を同期します
- 重複を排除します。ハンドラーは冪等でなければならず、イベントの一部はすでに配信されているかもしれないからです
これには何時間もかかります。しかもチャンスは 1 回だけです。リトライ期間を過ぎてイベントを取りこぼせば、それは消えてしまいます。
クリック 1 回で済む方法
AnyHook があるのは、これをクリック 1 回に変えるためです。Webhook の送信先を in.anyhook.net/you/app にすると、次のようになります。
- すべてのイベントが配信前に永続化され、保存時に暗号化されます
- サーバーが 5xx を返すと、AnyHook は指数バックオフでリトライします(配信試行は Free が 3 回、Pro が 5 回、Scale が 10 回)
- サーバーのダウンが長引いてリトライを使い切ると、イベントは失敗として記録されますが、ペイロードはログに残っています
- App(Webhook の送信元ごとに作る受信設定)のページを開き、失敗したイベントのバナーで期間(直近 1 時間、直近 24 時間、または再送可能期間全体)を選んで、Replay(再送)をクリックします。古いものから順に、1 回に 100 件ずつ送られます。それだけです。
失敗したイベントの再送は、月間クォータにはカウントされませんが、プランのコンピュート枠は消費します。プランごとに 1 日の再送上限があり(Free は 100 件、Pro は 500 件、Scale は 1,000 件)、クォータ超過で保留(held)になったイベントは、再送したときに新しいイベントとしてカウントされます。再送では新しい署名を付けてエンドポイントに改めて送信しつつ、元の受信時のボディとヘッダーはそのまま保たれます。そのため、ハンドラーにはプロバイダーが送ったとおりのイベントが届きます。
API なら、POST 1 回で同じことができます。
curl -X POST https://anyhook.net/api/v1/apps/stripe-prod/replay-failed \
-H "Authorization: Bearer $ANYHOOK_KEY" \
-H "Content-Type: application/json" \
-d '{"since": "2026-04-18T02:00:00Z", "limit": 100}'
1 回の呼び出しで最大 100 件を再送でき、ダッシュボードでクリックする必要もありません。
設計への示唆
再送のコストを下げるのは、配信前のエッジでの永続化です。Webhook のログがオリジンから始まるなら、再送できるのはオリジンが受け取ったものだけです。ログがネットワークのエッジ、つまりオリジンが失敗しうる手前から始まるなら、届いたものは何でも再送できます。
試行と試行の間に走るスケジュールは、それ自体が設計の問題です。AnyHook のリトライ間隔は固定で、ジッターはありません。「Webhook リトライの指数バックオフとジッター」では、そうした固定の間隔だと障害の後にすべてのクライアントが同じ秒に押し寄せる理由と、それでも積極的なリトライより低コストの再送のほうが勝る理由を説明しています。そして頻繁に再送に頼っているなら、その前に問うべきは、この境界にそもそも Webhook が必要だったのかどうかです。それについては「Webhook かポーリングか」で書いています。
ログを上流に移し、再送を冪等にすれば、最悪の Webhook 障害も、週末をつぶす作業ではなく 1 分の復旧作業になります。
必要になる前に読んでおく価値のある続編が 2 つあります。要になるのは後半の「再送を冪等にする」のほうです。リトライが同時に届くようになると、重複排除のテーブルだけでは済まなくなります:「同じ Webhook が 2 回届いた」。そして Stripe がすでに諦めてエンドポイントを無効化しているなら、リトライはもう止まっています。30 日の期間内に、Events API から補完することになります:「Stripe の Webhook エンドポイントが無効化されたとき」。
試してみてください:anyhook.net。