このガイドのゴール

  • Webhook を受信するサーバサイドの実装に必要な要素を理解する
  • 署名検証(Signature Verification)で不正リクエストを排除する
  • 冪等性(Idempotency)で二重処理を防ぎ、非同期処理でタイムアウトを回避する

Webhook は「外部サービスから自分のサーバへのリアルタイム通知」です。ポーリングと違い、イベント発生時に即座にデータが届きます。ただし、受信側の実装が不十分だと、セキュリティホール・二重処理・データ欠損の原因になります。

🧩 このガイドで題材にする Webhook
それぞれ署名方式と配送特性が異なる
💳 決済通知
Stripe Webhooks
HMAC-SHA256 署名 + タイムスタンプ検証。リプレイ攻撃対策が組み込まれている。
🐙 コード変更通知
GitHub Webhooks
HMAC-SHA256 署名。push・PR・Issue 等のイベントごとにペイロード構造が異なる。
💬 メッセージ通知
Slack Incoming Webhooks
署名検証(Signing Secret)+ リクエストタイムスタンプの鮮度チェック。

Webhook 受信の全体フロー

🔧 Webhook 受信の処理フロー
検証 → 冪等性チェック → 非同期処理の 3 ステップ
外部サービス Stripe / GitHub POST 受信エンドポイント ① 署名検証 ② 冪等性チェック ③ 200 OK 返却 ジョブ投入 Queue Redis / SQS Worker: ビジネスロジック実行 403 拒否 署名不一致 → 即拒否 200 OK(処理スキップ) DB event_id 記録(冪等性)

署名検証の実装

Webhook の受信で最も重要なセキュリティ対策です。署名を検証しないと、攻撃者が偽の Webhook を送信してサービスを操作できます。

Stripe の署名検証

Stripe は Stripe-Signature ヘッダに HMAC-SHA256 署名とタイムスタンプを含めます。

// Laravel での Stripe Webhook 署名検証
use Stripe\Webhook;

Route::post('/webhooks/stripe', function (Request $request) {
    $payload = $request->getContent();
    $sigHeader = $request->header('Stripe-Signature');
    $endpointSecret = config('services.stripe.webhook_secret');

    try {
        $event = Webhook::constructEvent($payload, $sigHeader, $endpointSecret);
    } catch (\UnexpectedValueException $e) {
        return response('Invalid payload', 400);
    } catch (\Stripe\Exception\SignatureVerificationException $e) {
        return response('Invalid signature', 403);
    }

    // event を処理...
    return response('OK', 200);
});

GitHub の署名検証

GitHub は X-Hub-Signature-256 ヘッダに HMAC-SHA256 署名を含めます。

import hmac
import hashlib

def verify_github_signature(payload_body, signature_header, secret):
    expected = "sha256=" + hmac.new(
        secret.encode(), payload_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
🚨
署名検証をスキップしてはいけない

「開発中だから後で実装する」は事故の原因です。Webhook エンドポイントはインターネットに公開されるため、署名なしでは誰でも偽のイベントを送信できます。決済完了の偽通知で商品を無料で取得される、といった攻撃が実際に報告されています。

Slack の署名検証

Slack は X-Slack-SignatureX-Slack-Request-Timestamp を使います。タイムスタンプが 5 分以上古い場合はリプレイ攻撃として拒否します。

import time

def verify_slack_signature(body, timestamp, signature, signing_secret):
    # タイムスタンプの鮮度チェック(5分以内)
    if abs(time.time() - int(timestamp)) > 300:
        return False

    sig_basestring = f"v0:{timestamp}:{body}"
    expected = "v0=" + hmac.new(
        signing_secret.encode(), sig_basestring.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
💡
タイムスタンプ検証はリプレイ攻撃を防ぐ

署名だけでは、過去の正当な Webhook リクエストを記録して再送する「リプレイ攻撃」を防げません。Stripe と Slack はタイムスタンプ検証でこの攻撃に対応しています。タイムスタンプが古すぎるリクエストは拒否してください。

冪等性の確保

Webhook は配送保証のために同じイベントを複数回送信することがあります。二重処理を防ぐ仕組みが必要です。

実装パターン: イベント ID の記録

// Laravel での冪等性チェック
public function handleWebhook(Request $request)
{
    $eventId = $request->input('id'); // Stripe: evt_xxx, GitHub: delivery ID

    // 処理済みなら 200 を返して終了(再処理しない)
    if (WebhookEvent::where('event_id', $eventId)->exists()) {
        return response('Already processed', 200);
    }

    // イベントを記録(先に記録してから処理する)
    $record = WebhookEvent::create([
        'event_id' => $eventId,
        'event_type' => $request->input('type'),
        'payload' => $request->getContent(),
        'status' => 'processing',
    ]);

    // ジョブキューに投入
    ProcessWebhookJob::dispatch($record);

    return response('OK', 200);
}
⚠️
「処理してから記録」ではなく「記録してから処理」

処理中にサーバがクラッシュすると、イベントが記録されないまま次の配送で再処理されます。先に event_id を DB に記録し、処理完了後にステータスを更新する設計にしてください。

非同期処理の設計

Webhook の送信元は、レスポンスが返るまでの時間に制限を設けています。

サービス タイムアウト リトライ回数
Stripe 20 秒 最大 3 日間にわたり段階的にリトライ
GitHub 10 秒 失敗時は Recent Deliveries から手動で再送可能
Slack 3 秒 3 回(30 分・60 分・3 時間後)

Slack の 3 秒制限は特に厳しいため、受信したら即座に 200 を返し、処理はキューに投入するのが定石です。

// 即座に 200 を返し、処理はキューで行う
Route::post('/webhooks/slack', function (Request $request) {
    // 署名検証
    if (!$this->verifySlackSignature($request)) {
        return response('', 403);
    }

    // キューに投入して即 200 返却
    ProcessSlackEvent::dispatch($request->all());

    return response('', 200);
});

セキュリティのベストプラクティス

エンドポイントの保護

  • HTTPS 必須: HTTP のエンドポイントはペイロードが平文で流れる
  • 推測されにくい URL: /webhooks/stripe よりも /webhooks/stripe/{random_token} の方が安全
  • IP ホワイトリスト: Stripe・GitHub は送信元 IP を公開しているため、ファイアウォールで制限可能
  • 不要なイベントの無視: 処理しないイベントタイプは早期に return response('OK', 200) で抜ける

ログと監視

受信した Webhook はすべてログに記録し、以下の異常を監視してください。

  • 署名検証失敗の急増: 攻撃の可能性
  • 特定イベントの大量配送: 外部サービス側の障害か、自分の処理失敗による再送
  • 処理時間の増加: ビジネスロジックのボトルネック
📌
ローカル開発には ngrok や Stripe CLI を使う

Webhook はインターネットからアクセス可能なエンドポイントが必要です。ローカル開発では ngrok でトンネルを作るか、Stripe CLI の stripe listen --forward-to コマンドでローカルにフォワードできます。GitHub も gh webhook forward で同様のことが可能です。

本番チェックリスト

  • 署名検証 をすべての Webhook エンドポイントで実装している
  • タイムスタンプ検証 でリプレイ攻撃を防いでいる(Stripe / Slack)
  • 冪等性: event_id を DB に記録し、重複処理を防いでいる
  • 受信後 即座に 200 を返し、処理はキューで非同期実行している
  • エンドポイントは HTTPS で公開している
  • エラーログ に署名検証失敗・処理失敗を記録している
  • ローカル開発用の Webhook フォワーディング を設定している

受信する側に実際に要るもの

署名検証や冪等性の前に、そもそも「どう受けるか」で止まることが多い。 必要なのは次の 4 つで、これ以上でも以下でもない。

  • 外部から到達できる URL — 提供元のサーバーが直接叩きに来る。 ローカル環境や社内ネットワークの中では受けられない
  • HTTPS — 平文を受け付けない提供元が多い。証明書は正規のものが要る。 自己署名証明書は弾かれると考えておく
  • 速く返すこと — 受信して即座に 2xx を返す。処理は後回しにする
  • POST を受けるだけの単純なエンドポイント — 認証を挟むと届かなくなる。 正当性は署名で判断する

VPS や自前サーバーで受ける場合

追加で要るのは、ファイアウォールで 443 を開けることと、リバースプロキシを 置くならリクエストボディを改変させないことの 2 点だ。 署名は生のボディに対して計算されるため、プロキシやフレームワークが JSON を再整形すると検証が通らなくなる。「ローカルでは通るのに本番で 署名エラーになる」の典型的な原因がこれになる。

🟡
遅い処理をその場で走らせると、再送を呼び込みます

提供元は一定時間内に 2xx が返らないと失敗とみなし、再送します。受信ハンドラの中で DB 更新・外部 API 呼び出し・メール送信まで済ませようとすると、処理が終わる前にタイムアウトして再送が来て、同じ処理が二重に走ります。しかも遅いほど再送が重なるため、負荷が上がるほど悪化します。受信したら記録して即 2xx、実処理はキューへという順序を崩さないでください。

登録時に飛んでくる検証リクエスト

エンドポイントを登録した瞬間、通常のイベントとは別の確認用リクエストが 届く提供元がある。送られてきた値をそのまま返すことを求めるもの、 特定のイベント名で 1 回だけ届くものなど形式はさまざまだが、共通するのは これに正しく応答できないとエンドポイントが有効にならないことだ。

実装時は、想定するイベントに当てはまらないリクエストを黙って捨てないように しておく。未知の種別を握りつぶす作りにしていると、検証リクエストも 捨ててしまい「登録できない」という形で詰まる。ログには残す。

検証ツールに本番のペイロードを流さない

webhook.site のような、一時 URL を発行して届いた内容を見せるサービスは 実装の初期に便利だ。ただし性質を理解して使う必要がある。

🔴
届いた内容は第三者のサーバーに保存され、URL を知る人は誰でも読めます

これらのツールは受信内容を保存して表示する仕組みです。つまり本番の Webhook をそこへ向けると、顧客の氏名・メールアドレス・決済の内訳・署名ヘッダ・場合によってはトークンまでが外部に残ります。URL は推測されにくいだけで、認証で守られているわけではありません。本番環境のエンドポイントを一時的にそこへ差し替える、という使い方は事故です。使うならテスト環境の、作り物のデータだけにしてください。

安全に中身を見る方法

手元で確認したいなら、提供元が用意している CLI でローカルへ転送する方法が 最も安全だ。外部に何も残らず、異常系のイベントを手動で発火できるものも多い。 それが無い場合も、自前のエンドポイントで受けてログに落とすだけの実装を 先に置けば、外部サービスに頼らずに中身を確認できる。 そのログにも機微な情報が入る点は忘れずに、保存期間とアクセス範囲を決めておく。

届かなかったときに何が起きるか

受信側が落ちていた、あるいは 5xx を返した場合、多くの提供元は 時間を空けて再送する。ただし再送は永久には続かず、回数や期間の上限がある。 上限を超えた分は失われ、あとから取りに行く手段が用意されていないこともある。

したがって Webhook だけに依存した設計は、障害が長引いたときに データの欠損として残る。重要な処理ほど、定期的に API 側の状態と 突き合わせる仕組みを併せて持っておきたい。管理画面から 再送を手動で行える提供元もあるので、導入時に確認しておくと復旧が早い。

まとめ

  • Webhook 受信の核心は 署名検証 → 冪等性チェック → 非同期処理 の 3 ステップ
  • 署名検証は セキュリティの最低ライン — スキップすると偽イベントで操作される
  • 冪等性は 「記録してから処理」 の順序が重要(クラッシュ耐性)
  • Slack は 3 秒、GitHub は 10 秒でタイムアウトするため キュー処理が必須
  • ログと監視で 署名失敗の急増・大量再送 を検知する体制を作る

この記事の情報は 2026 年 4 月時点のものです。各サービスの署名方式やリトライポリシーは変更される可能性があります。公式ドキュメントの Webhook セクションを併せて確認してください。