Webhook

イベント発生時にサーバーが指定された URL に HTTP リクエストを送信する、プッシュ型の通知メカニズム

APIイベント駆動
「Webhook」の技術書を見る →

Webhook とは

Webhook は、イベントが発生した際にサーバーが事前に登録された URL に HTTP POST リクエストを送信するプッシュ型の通知メカニズムである。「逆 API」とも呼ばれ、クライアントがポーリングする代わりに、サーバーがクライアントに通知する。

GitHub の Push イベント、Stripe の支払い完了、Slack のメッセージ投稿など、SaaS の連携で広く使われている。

ポーリングとの比較

ポーリングは「まだ新しいイベントはないか」と問い合わせ続ける方式なので、イベントの発生頻度が低いほど空振りのリクエストが増える。Webhook は発生した瞬間に 1 回だけ送るため、この空振りが構造的に消える。代わりに受信側は、常時待ち受ける URL を公開し、届いた通知が本物かどうかを自力で確かめる責任を負う。

ポーリング: クライアントが定期的に「新しいデータある?」と問い合わせ
  [クライアント] → GET /events → [サーバー] → "なし"
  [クライアント] → GET /events → [サーバー] → "なし"
  [クライアント] → GET /events → [サーバー] → "あり!"

Webhook: サーバーがイベント発生時に通知
  [サーバー] → POST /webhook → [クライアント] → "受信!"
観点Webhookポーリング
リアルタイム性即座ポーリング間隔に依存
サーバー負荷イベント時のみ定期的にリクエスト
実装の複雑さ受信エンドポイントが必要シンプル
信頼性配信の失敗は送信側のリトライ設計に依存1 回取りこぼしても次の問い合わせで拾える

セキュリティ

署名検証

受信 URL は公開されているため、その URL を知った誰でも POST できる。送信元が正当であることは、送信側と共有した秘密鍵で計算した HMAC 署名を照合して確かめる。

import { createHmac, timingSafeEqual } from 'crypto';

function verifySignature(body: string, signature: string, secret: string): boolean {
  const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
  const received = Buffer.from(signature);
  const computed = Buffer.from(expected);
  // timingSafeEqual は長さの違う入力に例外を投げるため、先に長さを比較する
  if (received.length !== computed.length) return false;
  return timingSafeEqual(received, computed);
}

実装でつまずく点が 2 つある。1 つは比較の方法で、=== や == は違いが出た位置で処理を打ち切るため、応答時間の差から正解を先頭 1 バイトずつ探る余地が残る。timingSafeEqual は同じ長さの入力を一定時間で比較するが、長さが違うと例外になるので、上のように長さの検査を前に置く。

もう 1 つは検証に渡す本文で、署名は送信側が送り出したバイト列そのものに対して計算されている。フレームワークが JSON をオブジェクトへ変換したあと JSON.stringify で組み立て直すと、キーの順序や空白、数値の表記が変わって署名が合わなくなる。生のリクエスト本文を保持したまま検証に回す設定が必要になる。

リプレイ対策

署名が正しいことを確かめても、過去に届いた通知をそのまま送り直す攻撃は防げない。その本文に対する署名は正しいままだからである。そこで送信側は、署名の対象にタイムスタンプを含める。Stripe は Stripe-Signature ヘッダーの t= にタイムスタンプを置き、署名対象の文字列をタイムスタンプと本文の連結として組む。タイムスタンプを書き換えれば署名が壊れるので、受信側は「署名が正しく、かつ時刻が現在から離れすぎていない」ことを受け入れの条件にできる。公式ライブラリの許容差は既定で 5 分で、許容差を 0 にすると鮮度の検査そのものが無効になると注意書きがある (2026 年 8 月時点の公式ドキュメント)。受信側のサーバー時刻が狂うと正当な通知まで落ちるため、時刻同期も前提になる。

信頼性の確保

重複と順序

Webhook は同じイベントが複数回届く前提で組む (at-least-once)。受信側が処理を終えたあとで応答だけが失われても、送信側からは失敗と見分けが付かないため再送が起きる。処理済みのイベント ID を記録しておき、既知の ID なら副作用を起こさずに成功を返す。

届く順序も発生順とは限らない。Stripe はイベントが生成された順序で配信することを保証しないと明記している (2026 年 8 月時点の公式ドキュメント)。後で起きたイベントを先に受け取っても壊れない処理にするか、届いた ID を手掛かりに必要な状態を API から取り直す形にしておく。

リトライの設計

配信が失敗したときの扱いは送信側のサービスごとに違うため、連携先の仕様を確認してから受信側を設計する。2026 年 8 月時点の公式ドキュメントでは、Stripe は本番環境で指数バックオフを使い最長 3 日間配信を試み、再試行のたびに新しい署名とタイムスタンプを生成する。GitHub は失敗した配信を自動では再送せず、管理画面や REST API から手動で再送する形を取り、受信側が 10 秒を超えて応答しない配信は失敗として記録される。前者は「重複が来る」前提が要で、後者は「落としたら自分で拾い直す」仕組みが要になる。

自分が送信側を作る場合の骨格は次のようになる。

  1. 即座にリトライ (1 回)
  2. 指数バックオフでリトライ (1 分、5 分、30 分のように間隔を広げる)
  3. 上限に達した配信はデッドレターキューへ退避し、受信側の復旧後に流し直せるようにする
  4. 管理画面で手動再送を提供する

上限に達した通知をそのまま捨てると、受信側が数分止まっただけで両者の状態がずれたまま戻らない。退避先を用意することが、リトライ回数を増やすことより効く。

SQS を挟むパターン

Webhook を直接処理すると、処理が遅い場合にタイムアウトする。SQS を挟んで非同期処理にする。

[外部サービス] → POST → [API Gateway] → [Lambda A: SQS に送信] → 200 OK
                                          ↓
                                        [SQS] → [Lambda B: 実際の処理]

この形にするときは、署名検証を前段の Lambda A で済ませる。検証せずにキューへ入れると、URL を知った誰でも後段の処理を起動できる入口になる。検証だけを前段に置けば受信は短時間で終わり、時間のかかる処理は後段が自分のペースで進められる。後段で失敗したメッセージはキューに残るので、送信側のリトライを待たずに再処理できる。

似た仕組みとの使い分け

Webhook はサーバー同士の連携に使う。受信側が公開 URL を持てることが前提なので、ブラウザーで動くクライアントは受け手になれない。画面へリアルタイムに反映したい場合は、サーバーから一方向に流す SSE か、双方向にやり取りする WebSocket を使い、外部サービスからの Webhook は自前のサーバーで受けてから画面へ中継する。

実務での活用方法は関連書籍にも詳しい。

共有:

この記事は役に立ちましたか?

関連用語

関連する記事