AnyHook ドキュメント

Webhook 中継サービスです。送信元の Webhook の URL を AnyHook に向けるだけで、自動リトライ、すべてを記録するイベントログ、統一された署名検証、ワンクリックでの再送が使えます。SDK は必要ありません。

クイックスタート

AnyHook には SDK もパッケージも必要ありません。送信元のダッシュボードで Webhook の URL を差し替えるだけで、ハンドラーはこれまでと同じリクエストを受け取り続けます。あらかじめ考えておくことが 1 つあります。リトライと再送には送信元の元のタイムスタンプが付いたままです。そのため、署名に有効期限がある送信元(Stripe、Slack など、/providers に許容範囲の記載があるもの)では、それらを受け入れるために AnyHook-Signature を検証してください。

最速の方法(アカウント不要):curl -X POST https://anyhook.net/api/v1/quickstart を実行すると、すぐに使える受信 URL と API キーがその場で手に入ります。AI エージェントや手早いテストに向いています。返された claim_url でアカウントに紐付けないと、エンドポイントは 7 日で期限切れになります。返された inbound_url を送信元に設定し、GET /api/v1/events と GET /api/v1/events/{id} でイベントを読み出します。各コマンドはレスポンスの next_steps に載っています。

  1. 1
    アカウントを作成する
    anyhook.net/sign-up で無料アカウントを作成します。クレジットカード登録は不要です。
  2. 2
    App を作成する
    App は Webhook の送信元ごとに作る受信設定で、送信元 1 つを表します。新しいアカウントでは、最初にこのフォームが開きます。名前(例:「Stripe Production」)と、必要に応じてお使いのサーバーの URL を入力します。AnyHook は受信した各イベントをこの URL に転送します。空欄のままにすると、App は転送せずにすべてを記録します。転送先はいつでも追加でき、Free では App ごとに最大 3 つまでです。
  3. 3
    送信元を AnyHook に向ける
    App のページには、受信 URL と、主な送信元でそれを入力する場所(例:Stripe → Developers → Webhooks)が表示されます。そこで、以前の Webhook の URL の代わりに次の受信 URL を貼り付けます。
    https://in.anyhook.net/{user-slug}/{app-slug}
    ページは受信を待ち受けているので、最初のリクエストは数秒以内にページに表示されます。まだ送信できるものがない場合は、同じページで Send a test event(テストイベントを送信)を押します。

受信 URL の形式

App にはそれぞれ、次の形式の一意な受信 URL が割り当てられます。

https://in.anyhook.net/{user-slug}/{app-slug}
  • user-slug:アカウントの識別子です。ダッシュボードの設定で確認できます。
  • app-slug:App ごとに一意な識別子です。App の名前から自動生成されますが、編集もできます。

受信エンドポイントは、あらゆる POST リクエストを受け付けます。設定時の検証ハンドシェイクにも応答します。hub.mode=subscribe を含む GET(Meta の hub.challenge チェック)には、App の設定前でも自動でそのまま値を返します。それ以外のメソッドには 405 を返します。Web ページや通常の HTML フォームからも送信できます(コードハンドラーを参照)。AnyHook は、イベントを永続キューに入れた時点で、転送先エンドポイントの速度に関係なく約 1 秒で HTTP 200 を返します。送信元が転送先を待つことはありません。

コンテンツタイプ。 application/json、application/x-www-form-urlencoded、XML、プレーンテキストなどのテキストのボディは、AnyHook がバイト単位でそのまま保存し、元の Content-Type のまま転送します。ボディはテキストとして保存されるため、バイナリのペイロードや、マルチパートフォームに含まれるファイルは、元のままの形では届きません。

対応する送信元

AnyHook は受信したヘッダーから送信元を自動で判別し、ペイロードの署名をエッジで検証します。検証を有効にするには、App の作成時に Webhook のプロバイダーを選び、そのプロバイダーの Webhook 署名シークレット(例:Stripe の whsec_...)を貼り付けます。

強く推奨します。 署名シークレットがないと、AnyHook は受信したリクエストが本当にプロバイダーから来たものかを検証できません。検証されていないリクエストもそのまま転送されます。シークレットを追加すると、偽造されたリクエストを、パイプラインに入る前にエッジで拒否できます。
送信元署名ヘッダーアルゴリズム
StripeStripe-SignatureHMAC-SHA256 + タイムスタンプ
GitHubX-Hub-Signature-256HMAC-SHA256
Meta (Instagram / Facebook / Threads)X-Hub-Signature-256HMAC-SHA256
ShopifyX-Shopify-Hmac-Sha256HMAC-SHA256 (Base64)
DiscordX-Signature-Ed25519Ed25519
SlackX-Slack-SignatureHMAC-SHA256 + タイムスタンプ
LINEx-line-signatureHMAC-SHA256 (Base64)
Xerox-xero-signatureHMAC-SHA256 (Base64)
LinearLinear-SignatureHMAC-SHA256
SentrySentry-Hook-SignatureHMAC-SHA256
Vercelx-vercel-signatureHMAC-SHA1
PaddlePaddle-SignatureHMAC-SHA256 + タイムスタンプ
SendGridX-Twilio-Email-Event-Webhook-SignatureECDSA P-256
TwilioX-Twilio-SignatureHMAC-SHA1
IntercomX-Hub-SignatureHMAC-SHA1
HubSpotX-HubSpot-Signature-v3HMAC-SHA256 (v3) / SHA-256 (v1/v2)
Standard Webhooks (Svix, OpenAI, Replicate)webhook-signature / Svix-SignatureHMAC-SHA256 + タイムスタンプ
ClerkSvix-SignatureHMAC-SHA256(Svix 経由)
ResendSvix-SignatureHMAC-SHA256(Svix 経由)
Lemon SqueezyX-SignatureHMAC-SHA256
WooCommerceX-WC-Webhook-SignatureHMAC-SHA256 (Base64)
PayPalPAYPAL-TRANSMISSION-SIGRSA-SHA256
Generic(汎用)/カスタムなし検証なし

検証に失敗したリクエストはエッジで拒否され、エンドポイントには届きません。拒否された件数は App のページに表示されます。毎日最初の 5 件は、ヘッダーとボディを含めて rejected としてイベントログに残るため、拒否の理由を確認できます。

Meta:Instagram、Facebook、Threads

3 つとも Graph API の同じ Webhook 仕様なので、App 1 つですべてに対応できます。Meta は GitHub と同じ X-Hub-Signature-256 ヘッダーで署名します。AnyHook は X-Github-Event で両者を区別します。このヘッダーは GitHub が必ず送り、Meta は決して送りません。

Meta は、コールバック URL が hub.mode=subscribe を含む GET に対して hub.challenge をレスポンスボディ全体としてそのまま返すまで、その URL を保存しません。AnyHook が代わりに応答するので、受信 URL は 1 回目で保存できます。

AnyHook 側Meta App Dashboard 側
Source:MetaApp settings → Basic
App Secretここからコピーして AnyHook に入力
Verify Token(任意)Webhooks → Verify token:同じ文字列を入力
受信 URLWebhooks → Callback URL:ここに貼り付け
Verify Token だけは向きが逆。 ほかのシークレットはすべてプロバイダーからコピーします。これだけは自分で決めた文字列を Meta に入力します。App Secret とは別物なので、同じ文字列にしないでください。設定時のハンドシェイクでクエリ文字列に含まれて届くため、署名用の鍵を URL に載せるべきではないからです。空欄のままでもサブスクリプションは完了します。設定すると、AnyHook は応答する前にその値を確認します。

Meta は失敗した配信を、間隔を広げながら 36 時間リトライし、その後は破棄します。AnyHook はエンドポイントへの配信を試す前にイベントを保存するため、デプロイや障害があっても、Meta のリトライ期間を消費することはありません。

配信の仕組み

AnyHook は非同期のキューベースのアーキテクチャで、Webhook の送信元とバックエンドを切り離します。

  1. エッジで受信:Cloudflare Workers が POST を受け付けて署名を検証し、イベントを永続化した時点で、約 1 秒で 200 を返します。
  2. キュー:イベントは永続メッセージキュー(Upstash QStash)に投入されます。この時点で、送信元の HTTP 接続はすでに閉じています。
  3. 転送:QStash がイベントを AnyHook の転送処理に届け、転送処理が元のペイロードを転送先エンドポイントに POST します。
  4. 記録:受信したヘッダーとボディ、配信ステータス、エンドポイントのレスポンスコードとボディ、レイテンシまで、すべての段階を記録します。
これが重要な理由。 送信元(Stripe、GitHub など)は約 1 秒で 200 を受け取ります。その後、バックエンドはプランのタイムアウト(Free は 60 秒、Pro は 120 秒、Scale は 5 分)まで応答に時間をかけられます。送信元は、すでに 200 を受け取っているので、リトライしません。最初の 1 秒を超えて待つと、コンピュート枠を消費します。
ペイロードのサイズ上限。 1 イベントあたりの最大ペイロードは、512 KB(Free)、2 MB(Pro)、4 MB(Scale)、カスタム(Enterprise)です。上限を超えるリクエストは、エッジで 413 Payload Too Large として拒否されます。黙って破棄されることも、課金されることもありません。その件数は App のページに表示されます。
配信モデル。 既定では、AnyHook はイベントを届いたそばから転送し、遅延も同時実行数の上限もありません。Pro 以上では、転送先ごとにレート制限(1 秒あたりの配信数)と遅延配信(最初の配信試行を遅らせる)を設定できます。エンドポイントが 429 を返すと、AnyHook は間隔を空けてリトライします。

配信の保証

保証の内容をはっきり書いておきます。保証があいまいな中継サービスは、そのあいまいさをコードに押し付けることになるからです。

  1. 配信は少なくとも 1 回(at-least-once)で、1 回限り(exactly-once)ではありません。リトライこそがこの製品の中身であり、レスポンスが失われたあとのリトライは新しい配信と区別できません。同じイベントを 2 回受け取ることがときどきあります。そうならないと約束できる中継サービスはなく、約束しているところはうまくいった場合の話をしているだけです。ハンドラーは冪等にする必要があります。重複排除のキーには、AnyHook が X-AnyHook-Delivery で転送するイベント ID を使ってください。
  2. 順序は保証しません。リトライと並行配信があるため、あとのイベントが、まだリトライ中の前のイベントより先に届くことがあります。AnyHook より上流でも、ほとんどの送信元は順序を約束していません。順序は到着時刻ではなく、ペイロード内のフィールド(バージョンや updated_at)で判断してください。
  3. 署名で重複排除をしないでください。AnyHook-Signature は、リトライが許容範囲から外れないよう、配信試行のたびにあえて新しいタイムスタンプで再署名されます。同じイベントでも、2 回の配信の署名は異なります。変わらないのはイベントの id です。
  4. 200 は保存済みを意味します。エッジが 200 を返すのは、イベントを永続化した後だけです。キューが使えなくても予備の保存先が 2 つあり、そのどちらにも保存できないときにだけ 503 を返します。受信 URL が 503 を返したときは何も保存されておらず、送信元自身のリトライが安全網になります。保存したふりをせずに 503 を返すのは、そのためです。
実務での考え方。 同じイベントを 2 回処理しても問題が起きないようにハンドラーを書き、到着順は当てにしないでください。守りの強い順に並べたパターンを「同じ Webhook が 2 回届いた:それはバグではなく仕様」にまとめています。

リトライスケジュール

転送先がリトライ対象のエラーを返したりタイムアウトしたりすると、AnyHook は指数バックオフで自動的にリトライします。

配信試行間隔プラン
1すぐにすべて
25 分後すべて
330 分後すべて
43 時間後Pro、Scale、Enterprise
518 時間後Pro、Scale、Enterprise
6-10それぞれ 24 時間後Scale、Enterprise
配信タイムアウト。 AnyHook がエンドポイントの応答を待つ時間は、最大 60 秒(Free)、120 秒(Pro)、5 分(Scale 以上)です。複数ステップの AI エージェントのように、ハンドラーにさらに時間が必要な場合は、すぐに HTTP 202 Accepted を返して非同期で処理してください。AnyHook は 202 を配信成功として扱います。

リトライされるレスポンスは?

すべてのエラーがリトライの対象になるわけではありません。AnyHook はエンドポイントのレスポンスを分類し、再度試しても直らないエラーで、配信試行もコンピュート枠も消費しないようにしています。

ステータスコード動作理由
2xx成功:配信完了エンドポイントがイベントを受け付けました。
3xxリトライしない:すぐに失敗として記録リダイレクトには従いません。転送先には最終的な URL を指定してください。
408 Request Timeoutバックオフ付きでリトライ一時的なタイムアウトです。次の配信試行で成功する可能性があります。
429 Too Many Requestsバックオフ付きでリトライエンドポイントがレート制限中です。時間を置きます。
4xx(上記以外:400、401、403、404、422…)リトライしない:すぐに失敗として記録クライアントエラーです。リクエストの形式が不正か、認証されていません。同じペイロードでリトライしても解決しません。
5xxバックオフ付きでリトライサーバーエラーです。一時的なものである可能性が高いです。
ネットワークエラー/タイムアウトバックオフ付きでリトライエンドポイントに到達できませんでした。
4xx をリトライしない理由。 400 Bad Request や 422 Unprocessable Entity は、ハンドラーがペイロードを拒否したことを意味します。リトライしても結果は変わりません。すぐに失敗として記録すれば、通知が早く届き、リトライにコンピュート枠が使われることもありません。ハンドラーを修正したら、そのイベントを再送できます。

配信試行はそれぞれ、レスポンスボディ全体とステータスコードとともに記録されます。リトライしない 4xx の失敗も、リトライ対象の失敗も、サーキットブレーカーのカウントに含まれます。種類を問わず 20 回連続で失敗すると、サーキットブレーカーが作動します。

失敗の通知

AnyHook はイベントごとではなく、決まったしきい値でメール通知を送ります。そのため、届くメールが増えすぎることはありません。

  • 1 回目の失敗:ステータスコードと転送先を記載したアラートをすぐに送ります。
  • 5 回連続の失敗:対処の提案を含むエスカレーションのアラートを送ります。
  • 20 回連続の失敗:サーキットブレーカーが作動し(後述)、そのアラートを送ります。

メールは、上に挙げたしきい値ごとに 1 通、そのあとはサーキットブレーカーの段階ごと(一時停止のたびと無効化)に 1 通です。イベントごとに 1 通送ることは決してありません。配信が 1 回でも成功すると、カウンターはリセットされます。

サーキットブレーカー

同じ転送先への配信が 20 回連続で失敗すると(リトライしない 4xx を含む)、サーキットブレーカーが作動し、段階的に停止時間を延ばしていきます。

作動回数停止時間内容
110 分転送先を一時停止、メールを送信
230 分転送先を一時停止、メールを送信
32 時間転送先を一時停止、メールを送信
424 時間転送先を一時停止、メールを送信
5無期限転送先を無効化、メールを送信

仕組み

  • 受信は続きます。受信 URL は常に有効で、Stripe や GitHub などからのイベントはそのまま受け付けて保存されます。一時停止するのは、転送先への配信だけです。
  • 停止時間が過ぎるたびに、AnyHook はハーフオープンのプローブを送ります。次のイベントで転送先を試し、成功すればサーキットはクローズに戻り、すべてがリセットされます。
  • 1 回でも成功すれば、すべてリセットされます:連続失敗回数も作動回数も 0 に戻ります。
  • 一時停止中に受信したイベントは、ステータス circuit_open で保存され、転送先が復旧したら再送できます。
  • 段階が上がるたびに、転送先 URL、失敗回数、停止時間を記載したメール通知が届きます。
  • 転送先が無効化された場合(5 回目の作動)、ダッシュボードから手動で再度有効にする必要があります。

この段階的な方式により、サーバーに復旧の時間を与えつつ、応答しないエンドポイントへのリトライでコンピュート枠が消費されるのを防ぎます。

イベントログ

受信 URL に届いたイベントは、配信の結果にかかわらずすべて記録されます。記録が始まるのは、プロバイダーの向き先を AnyHook にした時点からです。それより前に失敗したイベントは、プロバイダー側にしか残っていません(Stripe の場合は Webhook を再送する方法を参照)。各ログエントリには次の内容が含まれます。

  • 受信したヘッダーとボディの全体(送信元から受け取ったまま)
  • 配信ステータス:queued → success / retrying / failed / rejected / circuit_open / quota_exceeded。rejected は、AnyHook がイベントを拒否したこと(署名検証の失敗、または転送先 URL のブロック)を意味します。配信は一度も試行されておらず、再送できません。
  • 転送先の HTTP レスポンスコードとボディ(配信試行ごと)
  • レイテンシ(ミリ秒)
  • 配信試行の回数と次のリトライ時刻
  • 再送かどうか(is_replay: true)

イベントの保存期間はプランによって異なります。Free:3 日。その間はずっと再送でき、その後は削除されます。Pro:1 年。最初の 30 日間は再送でき、その後は読み取り専用の暗号化アーカイブになります。Scale:7 年。最初の 90 日間は再送でき、その後は読み取り専用の暗号化アーカイブになります。Enterprise:柔軟に対応。

一時停止中の App は何も保存しない。 App を一時停止している間(is_active: false)、受信 URL は 202 を返しますが、何も保存しません。そのためプロバイダーがエンドポイントを無効化することはありませんが、イベントは失われます。必要なトラフィックが届く前に、一時停止を解除してください。

ダッシュボードからの再送

ログにあるイベントは、どれでもダッシュボードからワンクリックで再送できます。再送では、保存したペイロードをバイト単位でそのまま、そのイベントの転送先に送ります。その後で転送先の URL を変更していれば、新しい URL に送ります。

  • Dashboard → Events を開く
  • イベントを見つけて Replay(再送)をクリックする
  • 元のイベントに紐付いた、is_replay: true の新しいイベントが作成される
クォータ。 再送は月間のイベントクォータにカウントされません。ただし、保留(quota_exceeded)になったイベントの再送は例外で、新しいイベントと同じく 1 回カウントされます。App のコードへの再送は、コードの実行 1 回としてカウントされます。

API からの再送(単一イベント)

POST/api/v1/events/{id}/replayID で指定したイベントを再送

元のイベントのペイロードを、再配信のために QStash に投入します。新しいイベントは replay_of で元のイベントに紐付けられます。

curl -X POST https://anyhook.net/api/v1/events/{event_id}/replay \
  -H "Authorization: Bearer {api_key}"

成功すると、新しいイベント ID とともに 200 を返します。イベントが再送可能期間を過ぎている場合は 410 Gone を返します。

未配信のイベント

GET/api/v1/apps/{slug}/events/undelivered配信に失敗したイベントを一覧表示

ステータスが failed、circuit_open、quota_exceeded のイベントを返します。

パラメーター型デフォルト説明
sinceISO 860124 時間前このタイムスタンプ以降に作成されたイベントのみ
limitinteger100返すイベントの最大数(1〜200)
curl "https://anyhook.net/api/v1/apps/stripe-prod/events/undelivered?since=2026-04-11T00:00:00Z&limit=50" \
  -H "Authorization: Bearer {api_key}"
{
  "count": 3,
  "events": [
    { "id": "evt_abc", "status": "failed", "created_at": "..." },
    ...
  ]
}

一括再送

POST/api/v1/apps/{slug}/replay-failedApp の失敗したイベントを再送

条件に一致する失敗したイベントを、古いものから順に、1 回の呼び出しで上限件数まで再送します。すでに再送済みのイベントと、再送可能期間を過ぎたイベントはスキップされます。

ボディのパラメーター型デフォルト説明
sinceISO 860124 時間前このタイムスタンプ以降に作成されたイベントのみ再送
limitinteger50再送するイベントの最大数(1〜100)
curl -X POST https://anyhook.net/api/v1/apps/stripe-prod/replay-failed \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{"since": "2026-04-11T00:00:00Z", "limit": 100}'
{
  "replayed": 3,
  "events": [
    { "original_id": "evt_abc", "new_id": "evt_xyz" },
    ...
  ]
}
1 日の再送上限。 Free:100 回/日、Pro:500 回/日、Scale:1,000 回/日、Enterprise:無制限。

ローカル開発

AnyHook は、特別なモードや CLI なしで、そのままローカル開発に使えます。方法は 2 つあります。

1. トンネル経由のライブ転送

App の転送先を、ローカルマシンを指すトンネルの URL に設定します。トンネルのサービスはどれでも使えます。

# ngrok
ngrok http 3000
# → https://abc123.ngrok.io

# cloudflared
cloudflared tunnel --url http://localhost:3000

# localhost.run
ssh -R 80:localhost:3000 nokey@localhost.run

ダッシュボードで、トンネルの URL を App の転送先にコピーします。AnyHook は、ほかのエンドポイントとまったく同じように Webhook を転送します。リトライ、ログ、署名ヘッダーもすべて同じように動作します。

ヒント。 開発中も、プロバイダーの Webhook は本番環境の受信 URL(https://in.anyhook.net/you/app)に向けたままにしておきます。実際のイベントがログに記録され、Stripe 側には触れずに、転送先をトンネルと本番環境の間で切り替えられます。

2. 記録したイベントを localhost に再送

本番環境の実際のイベントを AnyHook にためておき、必要なときにどれでもローカル環境へ再送します。テストイベントを手動で発生させる必要はありません。実際のペイロードで、1 日の再送上限が許す回数までテストできます。

  • 転送先をトンネルの URL に切り替える
  • Dashboard → Events を開き、任意のイベントで Replay をクリックする
  • または API を使う:POST /api/v1/events/{id}/replay
  • 元のペイロードがバイト単位でそのまま、現在の転送先に送られる

これは特に、エッジケースのデバッグに役立ちます。本番環境のログで問題のイベントを見つけ、ローカルに再送し、ハンドラーが正しく動くまで繰り返します。

再送はクォータにカウントされない。 再送は月間のイベントクォータにカウントされません。再送ではコンピュート枠を消費し、1 日の再送上限(Free は 100 回、Pro は 500 回、Scale は 1,000 回)にもカウントされます。

保存時の暗号化

Webhook のペイロードはすべて、データベースに届く前に AES-256-GCM で暗号化して保存されます。対象には、受信したヘッダー、リクエストボディ、転送先ごとの署名シークレットが含まれます。

暗号化には、データベースの認証情報とは別に保管している専用の鍵(AES_SECRET_KEY)を使います。暗号化の鍵なしにデータベースが侵害されても、得られるのは暗号文だけです。平文の Webhook データも、署名シークレットも、顧客情報も得られません。

暗号化される情報

  • Webhook リクエストのヘッダーとボディ(すべてのイベント)
  • プロバイダーの署名シークレット(Stripe の Webhook シークレットなど)
  • 転送先ごとの AnyHook 署名シークレット
データベースに平文のシークレットはない。 すべてのシークレットと Webhook のペイロードは、保存前に AES-256-GCM で暗号化されます。データベースに完全にアクセスできたとしても、攻撃者に見えるのは暗号文だけです。

エッジでの署名検証

AnyHook は、イベントがキューに入る前に、Cloudflare のエッジで Webhook の署名を検証します。App に署名シークレットを設定した後は、署名がない、または無効なリクエストは 401 で拒否され、クォータを消費することはありません。署名シークレットがない場合、リクエストは検証されずに転送されます。

署名方式はヘッダーから自動で判別します。App を作成して送信元の種類を選ぶと、AnyHook は署名シークレットを暗号化して保存し、そのプロバイダーに合ったアルゴリズムを使います。

無効なリクエスト。 署名検証に失敗したリクエストは HTTP 401 で拒否され、転送先には届きません。拒否された件数は App のページに表示され、毎日最初の 5 件は、ヘッダーとボディを含めてイベントログに残ります。

未対応の送信元やカスタムの送信元

Webhook の送信元が対応リストにない場合は、App の作成時に Generic(汎用)を選びます。AnyHook は次のように動作します。

  • 受信したすべての POST を受け付けて、キューに入れます。受信時の署名検証はありません。
  • ボディをバイト単位でそのまま、プロバイダーの元のヘッダーとともに転送先へ転送します。配信試行のたびに、新しい AnyHook-Signature も付けます。エッジの手前で追加されるインフラのヘッダー(cf-* など)は取り除きます。
  • AnyHook-Signature ヘッダーを付けるので、エンドポイントはリクエストが AnyHook を経由したことを検証できます。

Generic の App では、どのシークレットを設定しても、受信時の署名検証は行われません。AnyHook-Signature が証明するのはリクエストが AnyHook を経由したことで、誰が AnyHook に送ったかではありません。送信者の確認が必要なら、送信元の元のヘッダー(そのまま転送されます)の検証を自分で続けてください。送信元が Standard Webhooks(webhook-id、webhook-timestamp、webhook-signature。OpenAI や Replicate が採用)で署名している場合は、代わりにその送信元を選んでください。エッジで検証されます。

AnyHook-Signature

転送されるリクエストには、すべて AnyHook-Signature ヘッダーが付きます。これは、転送先の署名シークレットで生成した、タイムスタンプ付きの HMAC-SHA256 署名です。

AnyHook-Signature: t=1712934567,v1=5257a869...

サーバーでは次のように検証します。

// Node.js / TypeScript
import crypto from "crypto";

function verifyAnyHook(header: string, body: string, secret: string): boolean {
  const [tPart, vPart] = header.split(",");
  const timestamp = Number(tPart?.replace("t=", ""));
  // Re-signed on every attempt, so a genuine signature is never older than this.
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const signature = Buffer.from(vPart?.replace("v1=", "") ?? "", "hex");

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${body}`)
    .digest();

  // timingSafeEqual throws on a length mismatch, so check that first.
  return signature.length === expected.length && crypto.timingSafeEqual(signature, expected);
}

署名シークレットは転送先ごとに生成され、ダッシュボードの App → Destinations → Signing secret で確認できます。API でいつでもローテーションできます。

カスタムヘッダー

外向きのリクエストには、転送先ごとにカスタムヘッダーを付けられます。主な用途は次のとおりです。

  • Authorization: Bearer ...:API での認証
  • x-api-key: ...:API キーによる認証
  • エンドポイントが必要とする任意のヘッダー

カスタムヘッダーの値は暗号化して保存され、イベントログに表示されることはありません。App → Destinations → Custom headers で設定します。

元のヘッダーはそのまま。 プロバイダーの元のヘッダー(Stripe-Signature など)は、カスタムヘッダーや AnyHook-Signature と一緒に、バイト単位でそのまま転送されます。そのため、既存の SDK による検証は初回の配信で通ります。リトライと再送にはプロバイダーの元のタイムスタンプが付いており、Stripe の SDK はデフォルトで 5 分を過ぎたものを拒否します。代わりに、配信試行のたびに新しく署名される AnyHook-Signature を検証してください。

配信制御

転送先ごとに任意の設定が 3 つあり、いずれも Pro 以上で使えます。App → Destinations → Delivery controls で設定するか、PATCH /api/v1/apps/{slug} で転送先オブジェクトに指定します。Free のアカウントでも、有料プランだったときに設定した制御はそのまま残りますが、変更はできません。

{
  "destinations": [{
    "url": "https://api.example.com/hooks/stripe",
    "event_types": ["invoice.*", "checkout.session.completed"],
    "rate_limit": 20,
    "delay_seconds": 5
  }]
}

イベントタイプフィルター

event_types はパターンのリストです。転送先は、タイプがいずれかのパターンに一致するイベントだけを受け取ります。* は任意の文字列に一致するので、invoice.* は invoice のすべてのイベント、checkout.session.completed はその 1 種類だけです。照合はタイプ全体に対して行い、大文字と小文字を区別しません。転送先を 2 つにすれば、タイプごとに振り分けられます。請求関連のイベントはあるサービスへ、それ以外は別のサービスへ、という具合です。

  • 判定はエッジで、署名検証の後、クォータのカウントの前に行います。フィルターで除外されたイベントは保存も配信もされず、カウントもされません。
  • 送信元には 200 {"filtered":true,"event_type":"…"} が返ります。Stripe と Shopify では、この応答がそれぞれの Webhook ログに表示されます。
  • タイプをどこから読むかは、プロバイダーごとに異なります。GitHub、Shopify、WooCommerce、Lemon Squeezy はヘッダー、Stripe、Clerk、Resend、Vercel と Svix 形式の送信元はボディの type、Paddle と PayPal は event_type です。イベントログの Type 列に表示される値と同じです。
タイプが不明なイベントは必ず配信される。 AnyHook がタイプを読み取れないイベント(type フィールドのない汎用のボディや、プロバイダーの ping)は、フィルターにかかわらず、すべての転送先に配信されます。中継サービスとしての約束は、何も失わないことです。不明なイベントを捨てるフィルターは、その約束を破ることになります。

レート制限

rate_limit は、転送先への配信を 1 秒あたりその件数までに制限します(1〜1,000)。上限を超えたイベントはキューで待機し、空きが出しだい順番に送られます。何も破棄されず、リトライも同じ上限に含まれます。送信元がサーバーの処理能力を超える勢いでまとめて送ってくる場合に使います。たとえば、Shopify の商品の一括更新、月初に集中する Stripe のサブスクリプション更新、多数の CI イベントに広がる GitHub のモノレポへの push などです。

一括再送もこの上限に従います。上限がいちばん役立つのは、まさにこの場面です。失敗した 500 件のイベントを 1 秒あたり 20 件で再送すれば、再び失敗する一度きりの急増ではなく、25 秒間の安定したトラフィックになります。

遅延配信

delay_seconds は、最初の配信試行を遅らせます(1 秒〜1 日)。よくある理由は、送信元の Webhook が自身の API と競合する場合です。Stripe の checkout.session.completed は、成功ページが注文を書き込み終える前に届くことがあります。5 秒遅らせれば、競合は問題にならなくなります。再送が遅延されることはなく、最初の配信試行の後のリトライスケジュールも変わりません。

Slack/Discord アラート

Pro 以上では、Settings → Notifications で Incoming Webhook を 1 回設定するだけで、AnyHook がメールで送るアラートがすべてそこにも投稿されます。対象は、配信の失敗(1 回目と 5 回連続)、サーキットブレーカーの作動と無効化、拒否の急増とその解消、そして 50% / 90% / 100% のクォータ通知です。URL を保存するとテストメッセージが投稿されるので、届いたことを確認できます。Slack、Discord、Mattermost、Rocket.Chat、Teams の Incoming Webhook コネクターのいずれでも使えます。ペイロードは {"text": …}(Discord では content)です。「Email on failure」をオフにして止まるのはメールだけで、チャンネルにはアラートが届き続けます。

コードハンドラー

Pro と Scale では、App が受信するすべてのイベントで、自分のコードをサンドボックス内で実行できます。自前のサーバーは必要ありません。コードを保存すると、そのコードを実行する転送先が App に追加されます。そのため、リトライ、再送、サーキットブレーカー、イベントログはほかの転送先と同じように動作し、各実行のコンソール出力と戻り値がその配信のレスポンスになります。コードは App の Code セクションか、以下のエンドポイントで書きます。よくある処理のテンプレートは /code-templates.md にあり、Pipedream からの移行には専用のガイドがあります。

export default async function (event, { env, store }) {
  // event: { method, headers, body, raw_body, delivery_id, attempt }
  console.log(event.body);
}
  • イベント。body は、パース済みの JSON、フォームのフィールド(urlencoded、またはマルチパートフォームのテキストフィールド)、あるいは生のテキストです。raw_body は受信したままのテキストです。method は常に POST で、クエリ文字列、パス、呼び出し元の IP は保持されません。delivery_id はイベントの X-AnyHook-Delivery の ID で、attempt は 1 から数え、テスト実行では 0 です。
  • 失敗。例外を投げると配信は失敗になり、プランの配信試行回数の範囲でリトライされます(リトライスケジュール)。実行が成功したイベントのリトライでは、コードは再実行されません。一方、外部への呼び出しが通った後で失敗した実行は再実行されるため、API が対応していれば、そうした呼び出しには delivery_id を冪等キーとして使ってください。
  • Pipedream のステップ。export default defineComponent({ async run({ steps, $ }) {} }) もそのまま動きます。steps.trigger.event は上記のイベントで、$.export と $.flow.exit も使えます。data_store の prop はすべて App のストアになります。app prop の $auth.field はシークレット APP_FIELD を、それ以外の prop は同じ名前のシークレットを読み込みます(channelId なら CHANNEL_ID)。引き継げないものは Pipedream のガイドに載っています。
  • npm パッケージ。コードが import するパッケージは、保存時にインストールされます。バージョンは Pipedream と同じ書き方で固定でき(import axios from "axios@1.7.9")、最大 20 個までです。新しいパッケージをインストールする保存には、数分かかることがあります。インストールに失敗した場合は、何も保存されません。
  • シークレット。env.NAME または process.env.NAME で読み取ります。最大 20 個まで設定できます。名前には英字、数字、アンダースコアを使い、数字で始めることはできません。PATH, HOME, NODE_OPTIONS, NODE_PATH, PWD と、ANYHOOK_ で始まる名前はランタイムが使用します。値は最大 4,000 文字です。値がサーバーの外に出ることはありません。
  • ストア。store.get、set({ ttl } に秒数を指定)、has、delete、keys、entries、clear で、イベント間で状態を保持できます。ストアは App ごとに 1 つで、キーは最大 1,000 個、各キーは最大 256 文字、各値は JSON で最大 64,000 バイトです。インポートは {"entries": {key: value}} の形式で受け付け、既存のキーを上書きし、TTL なしで設定します。
  • スケジュール。schedule は 5 つのフィールドからなる cron 式で、CRON_TZ=Area/City を前に付けない限り UTC です。スケジュールが起動するたびに、anyhook.schedule イベントが App 自身の URL に投稿されます。そのため実行 1 回とイベント 1 件としてカウントされ、起動するのはコードがオンの間だけです。App の送信元は generic か auto で、署名シークレットなしにする必要があります。スケジュールで動かすコードには、専用の App を用意してください。
  • メール。どの App も {user-slug}.{app-slug}@anyhook.net でメールを受信でき、email.received イベントになります。ボディには from、to、subject、message_id、date、size と、MIME メッセージ全体の raw が入ります。スケジュールと同じく、送信元が generic か auto で、署名シークレットのない App が必要です。プランのペイロード上限を超えるメールはバウンスされます。
  • Web ページとフォーム。ページから受信 URL に fetch() できます。エッジが CORS のプリフライトに応答し、すべての応答に Access-Control-Allow-Origin: * が付きます。通常の HTML フォームには method="post" が必要です。action に ?redirect=https://your-site.com/thanks を付けると、イベントを保存した後、303 で訪問者をそのアドレスに移動させます。ただし、そのアドレスがフォームを送信したサイトと同じサイトにある場合に限ります。そうでない場合、訪問者には「Received」という短いページが表示されます。テキストフィールドは event.body に入りますが、アップロードされたファイルは残りません。
  • 上限。コードは最大 64,000 バイトで、実行時間は Pro で 105 秒、Scale で 285 秒までです。各実行では、コンソール出力の末尾 16,000 文字が保持されます。月間の実行回数はプランの表にあり、リトライ、テスト実行、再送、スケジュールの起動を含めて、すべての実行がカウントされます。超過料金はありません。月間の実行回数を超えると、コードへの配信は 429 を返して保留(circuit_open)になり、リトライはされません。翌月になるかプランを上げれば、再送できます。
  • テストとオン/オフ。POST .../code/test は、App に保存された最新のイベント、指定した event_id、または渡した body で保存済みのコードを実行し、ログ、戻り値、所要時間を返します。実行 1 回としてカウントされ、何も配信しませんが、コード自身が送信するものは実際に送信されます。イベントがコードを実行するのは、コードがオンの間だけです。ダッシュボードは、新しいコードをオフの状態で保存します。一方、PUT .../code は、"enabled": false を渡さない限り、最初のバージョンをオンの状態で保存します。その後の保存では、現在の状態が維持されます。移行中のように、別の転送先がまだ同じイベントを処理している間は、"enabled": false で保存し、その転送先を削除してからコードをオンにしてください。どちらの変更も、すべてのエッジロケーションに反映されるまで最大 1 分かかります。
GET/api/v1/apps/{slug}/codeコード、シークレット名、スケジュール、パッケージ、イベントが届くかどうか、今月の実行回数
PUT/api/v1/apps/{slug}/codeコード、シークレット(マージ。null で個別に削除)、スケジュール(null で削除)、enabled を保存
DELETE/api/v1/apps/{slug}/codeコード、シークレット、スケジュールを削除
POST/api/v1/apps/{slug}/code/test保存済みのコードを、保存されたイベントか渡したボディで今すぐ実行
GET/api/v1/apps/{slug}/storeApp のストアにある有効なキーをすべて取得
POST/api/v1/apps/{slug}/storeキーをインポート:{"entries": {key: value}}
DELETE/api/v1/apps/{slug}/store?key= で指定したキー、またはすべてのキーを削除

認証

API リクエストにはすべて、Authorization ヘッダーに Bearer トークンが必要です。

Authorization: Bearer ahk_live_xxxxxxxxxxxxxxxx

API キーは Dashboard → Settings → API Keys で発行します。キーはアカウント単位で、フルアクセスか読み取り専用かを作成時に選べます。キーはいつでも失効させられます。MCP のログイン(anyhook.net/mcp)で接続したアプリは、承認したアクセス権の範囲内で、OAuth アクセストークンを使って同じエンドポイントを呼び出せます。

レート制限。 API と MCP の呼び出しは、アカウントごとに、UTC の 1 日あたり Free は 300 回、Pro は 1,000 回、Scale は 3,000 回までです。上限を超えると、00:00 UTC まで Retry-After ヘッダー付きの 429 Too Many Requests が返ります。

AI エージェント(MCP)

AnyHook は https://anyhook.net/mcp で動く MCP サーバーです。エージェントが代わりに App の作成、イベントログの読み取り、失敗した配信の再送を行えます。Pro と Scale では、App のコードのデプロイとテストもできます(anyhook_code_deploy、anyhook_code_test、anyhook_code_get)。Claude Code、claude.ai、Codex、VS Code は URL とブラウザーでのログインだけで接続でき、キーをコピーする必要はありません。クライアントごとの手順を参照してください。

Cursor、Windsurf、Claude Desktop ではキーを使います。Dashboard → Settings → API Keys でキーを作成すると、キーが入った状態で次の設定が表示されます。

{
  "mcpServers": {
    "anyhook": {
      "command": "npx",
      "args": [
        "-y",
        "anyhook-mcp"
      ],
      "env": {
        "ANYHOOK_API_KEY": "ahk_live_xxxxxxxxxxxxxxxx"
      }
    }
  }
}

Cursor:Settings → MCP → Add new MCP server、または ~/.cursor/mcp.json。Windsurf:~/.codeium/windsurf/mcp_config.json。Claude Desktop:Settings → Developer → Edit Config。コードのツールを使うには、anyhook-mcp 0.4.0 以降(npx -y で取得されます)か、https://anyhook.net/mcp のサーバーが必要です。

そのあと、エージェントに「Stripe の Webhook 用に AnyHook の App を作成して、その URL を教えてください。」と頼みます。エージェントは、この内容をすべて /llms.txt で読めます。

App

GET/api/v1/appsすべての App を一覧表示
POST/api/v1/appsApp を新規作成
GET/api/v1/apps/{slug}App の詳細を取得
PATCH/api/v1/apps/{slug}App を更新
DELETE/api/v1/apps/{slug}App を削除

App の作成例:

curl -X POST https://anyhook.net/api/v1/apps \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe Production",
    "source": "stripe",
    "destination_url": "https://api.example.com/webhooks"
  }'
{
  "id": "app_xxxxx",
  "name": "Stripe Production",
  "slug": "stripe-production",
  "inbound_url": "https://in.anyhook.net/you/stripe-production",
  "destinations": [
    { "url": "https://api.example.com/webhooks", "enabled": true }
  ]
}

Stripe または GitHub を接続

POST/api/v1/connectプロバイダー自身のキーを使って、App の URL をプロバイダーに登録

Stripe のキーか GitHub のトークンを渡すと、AnyHook は App の受信 URL をプロバイダーに Webhook として追加し、発行された署名シークレットを保存して、最初のイベントから署名を検証します。プロバイダーはキーから判別します。キーはこれらの呼び出しにだけ使い、保存しません。app を指定しないと、新しい App が作成されます。もう一度実行すると、2 つ目を追加するのではなく、既存の登録を置き換えます。

必要なのは最小限の権限だけです。Stripe なら Webhook Endpoints: Write を付けた制限付きキー、GitHub ならリポジトリに対して Webhooks: Read and write を付けた fine-grained トークンです。

# Stripe: a restricted key, connecting an existing app
curl -X POST https://anyhook.net/api/v1/connect \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{ "key": "rk_live_...", "app": "payments" }'

# GitHub: name the repository (or an organization); no app, so one is created
curl -X POST https://anyhook.net/api/v1/connect \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{ "key": "github_pat_...", "target": "acme/api" }'

GitHub はフックが作成されるとすぐに署名付きの ping を送るので、最初のイベントは数秒以内に表示されます。その他のプロバイダーでは、POST /api/v1/apps で App を作成し、その受信 URL をプロバイダーの Webhook 設定に貼り付けます。

イベント

GET/api/v1/eventsイベントの概要を新しい順に一覧表示(ペイロードなし)
GET/api/v1/events/{id}ペイロード全体を含むイベントの詳細を取得

一覧取得のクエリパラメーターは次のとおりです。

パラメーター型説明
app_slugstringApp の slug で絞り込み
statusstringステータスで絞り込み(queued、success、retrying、failed、rejected、circuit_open、quota_exceeded)
limitinteger最大件数(デフォルト 50、最大 200)

再送(API)

POST/api/v1/events/{id}/replay単一のイベントを再送

詳細と例は、上の API からの再送を参照してください。

未配信(API)

GET/api/v1/apps/{slug}/events/undelivered失敗/未配信のイベントを一覧表示

詳細と例は、上の未配信のイベントを参照してください。

一括再送(API)

POST/api/v1/apps/{slug}/replay-failedApp の失敗したイベントをすべて再送

詳細と例は、上の一括再送を参照してください。

プランの上限

FreePro($20/月)Scale($90/月)Enterprise
イベント数/月10,00050,000250,000柔軟に対応
1 日の上限1,000なしなしなし
App 数11050柔軟に対応
App ごとの転送先数32050柔軟に対応
配信試行回数3510柔軟に対応
保存期間3 日1 年7 年柔軟に対応
再送可能期間3 日30 日90 日最大 365 日
配信タイムアウト60 秒120 秒5 分柔軟に対応
最大ペイロード512 KB2 MB4 MBカスタム
超過料金なし1k 件あたり $0.151k 件あたり $0.17契約による
1 日の再送上限1005001,000無制限
コード実行回数/月なし3,00012,000柔軟に対応
コードの実行時間なし105 秒285 秒柔軟に対応
コンピュート枠$0.15/月$3.00/月$11.00/月柔軟に対応
API と MCP の呼び出し300 回/日1,000 回/日3,000 回/日柔軟に対応

料金の詳細を見る →

使用量とクォータ

AnyHook は使用量を暦月単位(例:「2026-04」)で集計します。カウンターは毎月の初めにリセットされます。

  • 配信 1 回につき 1 件:2 つの転送先に送られたイベントは 2 件と数えます。大きなイベントは、ボディとヘッダーの 64 KB ごと(端数は切り上げ)に 1 件と数えます。転送先のない App のイベントは 1 件です。フィルターで除外されたイベントと、署名検証に失敗したイベントは数えません。Free の 1 日の上限はもっと単純で、エッジが受け付けたリクエストを、転送先の数にかかわらず 1 件と数えます。
  • 初回の配信試行のみ:リトライはクォータにカウントされません。
  • 再送はカウントされません:is_replay: true のイベントは課金の対象外です。ただし、クォータ超過で保留(held)になったイベントの再送は例外で、新しいイベントと同じく 1 回カウントされます。再送はコンピュート枠を消費し、プランの 1 日の再送上限にもカウントされます。
  • コンピュート枠:各イベントの初回の配信試行を超えて、AnyHook がアカウントのために使う費用を原価で差し引く枠です。対象は、リトライ、再送、コードの実行、パッケージのインストール、スケジュールの起動、障害訓練、そして転送先の応答を最初の 1 秒を超えて待つ時間です(1 KB のイベントのリトライで約 $0.00002、数秒のコード実行で約 $0.0008)。枠を使い切ると、翌月 1 日(UTC)までこれらは止まります。失敗した配信はリトライされず、再送とコードの実行は拒否され、スケジュールは一時停止し、転送先の応答は 1 秒しか待ちません。イベントは引き続き届き、初回の配信試行も行われます。
  • API と MCP の呼び出し:API キーまたは MCP クライアントによる呼び出しは、1 回につき 1 単位と数えます。32 KB を超えるレスポンスは、32 KB ごと(端数は切り上げ)に 1 単位と数えます。ダッシュボードからの操作は数えません。1 日の上限を超えると、API は 00:00 UTC まで Retry-After 付きで 429 を返します。GET /api/v1/usage(MCP では anyhook_usage)で、すべての上限とその使用量を確認できます。
  • 破棄せずに保留:プランに含まれるイベント数を超えると(超過料金の上限を設定している場合は、その上限も超えると)、各イベントはステータス quota_exceeded で記録され、配信されません。アップグレードするか上限を設定すれば、ダッシュボードからワンクリックで再送できます。保留にできるのは、上限で購入できる分を超えてクォータの 20% までです。それを超えると、暴走した送信元への歯止めとして、エッジは 429 で拒否し、何も保存しません。
Free の上限。 1 日の上限(1,000 件)を超えたイベントは、エッジで拒否されます。月間の上限(10,000 件)を超えたイベントは、さらにその 20% まで保留になります。イベントログにステータス quota_exceeded で表示され、アップグレード後に再送できます。それを超えた分は、1 日の上限と同じく拒否されます。従量制の超過料金を使うには、Pro にアップグレードしてください。
有料プランの超過料金。 超過分は、Pro では 1k 件あたり $0.15、Scale では 1k 件あたり $0.17 で請求し、セント単位で切り上げます。有料プランのトラフィックを黙って破棄することはありません。

超過料金

超過料金を有効にしておけば、月間クォータを使い切ってもイベントは保留されず、流れ続けます。Pro と Scale プランでのみ使えます。

仕組み

  1. Settings(設定)で、月ごとの超過料金の上限を設定します($5 / $10 / $20 / $50、または Off)。
  2. プランのイベントクォータに達すると、AnyHook は超過料金が有効になっているか、上限に達していないかを確認します。
  3. 許可されていれば、イベントは通常どおり配信され、超過分として記録されます。
  4. 翌月の初めに、超過分は Stripe の請求書に単一の明細として請求されます。Pro は 1,000 件あたり $0.15、Scale は 1,000 件あたり $0.17 です(イベント単位で計算し、セント単位で切り上げ)。請求額は設定した上限までです。

App ごとの切り替え

App にはそれぞれ超過料金の設定があり、Inherit default(デフォルトを継承)、Enabled(有効)、Disabled(無効)から選べます。デフォルトでは、App はアカウント単位の設定を引き継ぎます。App ごとに変更するには、App Settings → Overage Billing を使います。

アカウントのデフォルト

Settings → Overage Billing で Off 以外の超過料金の上限を設定すると、Inherit default に設定されたすべての App で超過料金が有効になります。既存の App も対象です。Disabled に設定した App は、オフのままです。

超過料金の上限は厳格な上限。 超過料金が上限に達すると、それ以降のイベントは配信されません。各イベントは quota_exceeded として保存され、上限を引き上げるか月が切り替われば再送できます。上限を超えた分が請求されることはありません。上限を超えたイベントは、さらにクォータの 20% まで保留になります。それを超えた分は、エッジが 429 で拒否し、何も保存しません。

API キー

API キーは、AnyHook の REST API へのリクエストを認証します。Dashboard → Settings → API Keys で管理します。

  • 本番環境用のキーには、ahk_live_ というプレフィックスが付きます。
  • 複数のキーを作成し、ラベル(例:「CI/CD」「Agent」)を付けられます。
  • キーはすぐに失効させられます。失効したキーは、次に使われたときに 401 を返します。
  • キーは作成時に 1 回だけ表示されます。安全な場所に保管してください。
GET/api/v1/api-keysすべての API キーを一覧表示(シークレットはマスク)
POST/api/v1/api-keysAPI キーを新規作成
PATCH/api/v1/api-keys/{id}/revokeAPI キーを失効

お困りですか?

hello@anyhook.net にメールしてください。すべてのメッセージに返信します。サービスの稼働状況は status.anyhook.net で確認できます。