信頼できるウェブフックの設計方法

Ashley Innocent

Ashley Innocent

13 3月 2026

信頼できるウェブフックの設計方法

要約

信頼性の高いWebhookを設計するには、指数関数的バックオフによるリトライ(5~10回)、べき等性キー、HMAC署名検証、5秒のタイムアウトを実装しましょう。2xx応答はすぐに返し、処理は非同期で行います。Modern PetstoreAPIは、注文更新、ペットの引き取り、支払い通知のためのWebhookを、完全なリトライとセキュリティ機能とともに実装しています。

はじめに

クライアントにペットが引き取られたことを通知するためにWebhookを送信しました。しかし、クライアントのサーバーがダウンしています。Webhookは失敗します。リトライしますか?何回?もしクライアントがWebhookを2回受け取ってしまい、顧客に2回請求してしまったらどうなりますか?

Webhookは、イベントが発生した際にクライアントのURLにイベントをプッシュするHTTPコールバックです。理論的には単純ですが、実際には複雑です。ネットワークの障害、サーバーのクラッシュ、クライアント側のバグなどが起こり得ます。本番環境のWebhookには、リトライロジック、べき等性、セキュリティ、監視が必要です。

Modern PetstoreAPIは、注文更新、ペットの引き取り、支払い通知のための本番環境対応のWebhookを実装しています。すべてのWebhookには、リトライロジック、署名検証、べき等性が含まれています。

💡
Webhookを構築またはテストしている場合、ApidogはWebhookの配信テスト、署名の検証、障害シナリオのシミュレーションを支援します。リトライロジックをテストし、べき等性の処理を確認できます。
ボタン

このガイドでは、Modern PetstoreAPIのパターンを使用して信頼性の高いWebhookを設計する方法を学びます。

Webhookの基本

Webhookは、イベントが発生したときにクライアントが提供するURLに送信されるHTTP POSTリクエストです。

Webhookの動作原理

1. クライアントがWebhook URLを登録します:

POST /webhooks
{
  "url": "https://client.com/webhooks/petstore",
  "events": ["pet.adopted", "order.completed"]
}

2. イベントが発生(ペットが引き取られる)

3. サーバーがWebhookを送信します:

POST https://client.com/webhooks/petstore
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...

{
  "event": "pet.adopted",
  "data": {
    "petId": "019b4132",
    "userId": "user-456",
    "timestamp": "2026-03-13T10:30:00Z"
  }
}

4. クライアントが応答します:

200 OK

信頼性の問題

Webhookは多くの理由で失敗する可能性があります。

リトライロジックがなければイベントは失われ、べき等性がなければ重複したWebhookが重複したアクションを引き起こします。

指数関数的バックオフによるリトライロジック

失敗したWebhookを、遅延を増やしながらリトライします。

指数関数的バックオフ戦略

1回目: 即時
2回目: 1秒後
3回目: 2秒後
4回目: 4秒後
5回目: 8秒後
6回目: 16秒後

なぜ指数関数的なのか? クライアントがダウンしている場合、リトライを連打しても意味がありません。指数関数的バックオフは、復旧のための時間を与えます。

実装

async function sendWebhook(url, payload, attempt = 1, maxAttempts = 6) {
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-Signature': generateSignature(payload)
      },
      body: JSON.stringify(payload),
      timeout: 5000 // 5秒のタイムアウト
    });

    if (response.ok) {
      return { success: true, attempt };
    }

    // 5xxエラーでリトライ
    if (response.status >= 500 && attempt < maxAttempts) {
      const delay = Math.pow(2, attempt - 1) * 1000;
      await sleep(delay);
      return sendWebhook(url, payload, attempt + 1, maxAttempts);
    }

    // 4xxエラーではリトライしない(クライアントエラー)
    return { success: false, status: response.status };

  } catch (error) {
    // ネットワークエラーまたはタイムアウト - リトライ
    if (attempt < maxAttempts) {
      const delay = Math.pow(2, attempt - 1) * 1000;
      await sleep(delay);
      return sendWebhook(url, payload, attempt + 1, maxAttempts);
    }
    return { success: false, error: error.message };
  }
}

いつリトライするか

以下の場合にリトライ:

以下の場合にはリトライしない:

デッドレターキュー

最大リトライ回数を超えた後、失敗したWebhookは手動レビューのためにデッドレターキューに移動します。

if (!result.success) {
  await deadLetterQueue.add({
    url,
    payload,
    attempts: maxAttempts,
    lastError: result.error,
    timestamp: new Date()
  });
}

重複防止のためのべき等性

クライアントは同じWebhookを複数回受け取る可能性があります。べき等性は重複処理を防ぎます。

べき等性キー

各Webhookに一意のIDを含めます。

{
  "id": "webhook_019b4132",
  "event": "pet.adopted",
  "data": {...}
}

クライアントは処理済みIDを保存します:

app.post('/webhooks/petstore', async (req, res) => {
  const webhookId = req.body.id;

  // すでに処理済みか確認
  const processed = await db.webhooks.findOne({ id: webhookId });
  if (processed) {
    return res.status(200).json({ message: 'Already processed' });
  }

  // Webhookを処理
  await processPetAdoption(req.body.data);

  // 処理済みとしてマーク
  await db.webhooks.insert({ id: webhookId, processedAt: new Date() });

  res.status(200).json({ message: 'Processed' });
});

べき等な操作

べき等な操作を設計します。

悪い例(べき等ではない):

// 2回請求すると二重請求になる
await chargeCustomer(userId, amount);

良い例(べき等である):

// べき等性キーを使用して請求すると、二重請求を防ぐことができる
await chargeCustomer(userId, amount, { idempotencyKey: webhookId });

セキュリティのための署名検証

Webhookが攻撃者からではなく、あなたのAPIから送信されたものであることを検証します。

HMAC署名

共有シークレットを使用して署名を生成します。

// サーバーが署名を生成
const crypto = require('crypto');

function generateSignature(payload, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  return hmac.digest('hex');
}

// ヘッダーに含める
headers['X-Webhook-Signature'] = `sha256=${generateSignature(payload, webhookSecret)}`;

クライアントが署名を検証します:

function verifySignature(payload, signature, secret) {
  const expected = generateSignature(payload, secret);
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

app.post('/webhooks/petstore', (req, res) => {
  const signature = req.headers['x-webhook-signature'];

  if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Webhookを処理
  ...
});

タイムスタンプ検証

リプレイ攻撃を防ぐためにタイムスタンプを含めます。

{
  "id": "webhook_019b4132",
  "timestamp": "2026-03-13T10:30:00Z",
  "event": "pet.adopted",
  "data": {...}
}

古いWebhookを拒否します:

const webhookAge = Date.now() - new Date(req.body.timestamp);
if (webhookAge > 5 * 60 * 1000) { // 5分
  return res.status(400).json({ error: 'Webhook too old' });
}

タイムアウト処理

遅いクライアントがシステムをブロックするのを防ぐために、積極的なタイムアウトを設定します。

5秒のタイムアウト

const response = await fetch(url, {
  method: 'POST',
  body: JSON.stringify(payload),
  timeout: 5000 // 5秒
});

なぜ5秒なのか? Webhookはすぐに応答を返す必要があります。クライアントがそれ以上かかる場合、同期処理を行っていることになります(誤ったパターンです)。

非同期処理パターン

悪い例(同期処理):

app.post('/webhooks/petstore', async (req, res) => {
  // これには30秒かかり、Webhookはタイムアウトします
  await processOrder(req.body.data);
  await sendEmail(req.body.data);
  await updateInventory(req.body.data);

  res.status(200).json({ message: 'Processed' });
});

良い例(非同期処理):

app.post('/webhooks/petstore', async (req, res) => {
  // すぐに返す
  res.status(200).json({ message: 'Received' });

  // 非同期で処理
  queue.add('process-webhook', req.body);
});

Modern PetstoreAPIでのWebhookの実装方法

Modern PetstoreAPIは、本番環境対応のWebhookを実装しています。

Webhookイベント

pet.adopted - ペットが引き取られた
pet.status_changed - ペットのステータスが変更された
order.created - 注文が作成された
order.completed - 注文が完了した
payment.succeeded - 支払いが成功した
payment.failed - 支払いが失敗した

Webhookペイロード

{
  "id": "webhook_019b4132-70aa-764f-b315-e2803d882a24",
  "event": "pet.adopted",
  "timestamp": "2026-03-13T10:30:00Z",
  "data": {
    "petId": "019b4132-70aa-764f-b315-e2803d882a24",
    "userId": "user-456",
    "orderId": "order-789",
    "adoptionDate": "2026-03-13"
  },
  "apiVersion": "v1"
}

リトライ設定

セキュリティ

ApidogでWebhookをテストする

ApidogはWebhookテストをサポートしています。

Webhook配信のテスト

  1. ApidogでモックWebhookエンドポイントを作成
  2. PetstoreAPIにエンドポイントを登録
  3. イベントをトリガー(ペットの引き取り)
  4. Webhookが受信されたことを確認
  5. ペイロードの形式を確認

署名検証のテスト

// Apidogテストスクリプト
const signature = pm.request.headers.get('X-Webhook-Signature');
const payload = pm.request.body.raw;
const secret = pm.environment.get('WEBHOOK_SECRET');

const expected = generateSignature(payload, secret);
pm.test('署名が有効', () => {
  pm.expect(signature).to.equal(`sha256=${expected}`);
});

リトライロジックのテスト

  1. モックエンドポイントから500エラーを返す
  2. 指数関数的バックオフによるリトライ試行を確認
  3. 最大リトライ後のデッドレターキューを確認

べき等性のテスト

  1. Webhookを受信
  2. 200を返す
  3. 同じWebhookを再度受信(リトライをシミュレート)
  4. 重複処理がないことを確認

結論

信頼性の高いWebhookには以下が必要です:

Modern PetstoreAPIはこれらのすべてのパターンを実装しています。完全な例についてはWebhookドキュメントを参照してください。

本番環境に移行する前に、ApidogでWebhookをテストし、リトライロジック、署名、べき等性を検証してください。

ボタン

よくある質問

Webhookは何回リトライすべきですか?

指数関数的バックオフで5〜10回試行します。これにより、クライアントに過負荷をかけることなく、一時的な障害(5〜17分)に対応できます。

Webhookは4xxエラーでリトライすべきですか?

いいえ。4xxエラーはクライアント側の問題(URLの間違い、認証失敗など)を示します。リトライしてもこれらは解決しません。5xxエラーとネットワーク障害の場合のみリトライしてください。

Webhookのタイムアウトはどれくらいにすべきですか?

最大5秒です。クライアントはすぐに200を返し、非同期で処理すべきです。タイムアウトが長い場合は、クライアントが同期処理を行っていることを示します。

クライアントがWebhookにまったく応答しない場合はどうすればよいですか?

最大リトライ後、デッドレターキューに移動します。クライアントにメールで通知し、度重なる失敗の後にはそのクライアントへのWebhookを無効にすることを検討してください。

Webhook URLはHTTPSであるべきですか?

はい、常にHTTPSを必須にしてください。HTTPのWebhookは傍受・改ざんされる可能性があります。Modern PetstoreAPIはHTTPのWebhook URLを拒否します。

リプレイ攻撃をどのように防ぎますか?

ペイロードにタイムスタンプを含め、5分以上経過したWebhookは拒否します。署名検証と組み合わせます。

クライアントはWebhookの再配信を要求できますか?

はい。Modern PetstoreAPIは、特定のWebhookを再配信するためのエンドポイントを提供しています: POST /webhooks/{id}/redeliver

ローカルでWebhookをテストするにはどうすればよいですか?

ngrokのようなツールを使用してlocalhostをインターネットに公開するか、開発中にApidogのモックサーバーを使用してWebhookエンドポイントをシミュレートします。

ApidogでAPIデザイン中心のアプローチを取る

APIの開発と利用をよりシンプルなことにする方法を発見できる