> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook の概要

> ThunderPhone がリアルタイムイベントを配信する仕組み、署名を検証する方法、レガシー配信モデルとエンドポイントベースの配信モデルの比較。

ThunderPhoneは、通話中にイベントが発生するとサーバーにHTTP `POST` リクエストを送信します。たとえば、着信の開始、通話の終了、評価実行の完了、アラートの発生などです。配信モデルは**2種類**あります。

<CardGroup cols={2}>
  <Card title="Webhookエンドポイント（推奨）" icon="bolt" href="/ja/webhooks/endpoints">
    複数のURL、エンドポイントごとのシークレット、エンドポイントごとのイベントフィルター、
    および自動再試行。
    `GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints` で管理します。
  </Card>

  <Card title="単一URLのレガシーWebhook" icon="link" href="/api-reference/organizations#legacy-single-url-webhook">
    組織ごとに1つのURL。**ブロッキング**構成のやり取りを含む、
    通話ライフサイクルイベントを送信します。`GET/PUT /v1/webhook` で管理します。
  </Card>
</CardGroup>

[イベントカタログ](/ja/webhooks/events)にある10種類すべてのイベントは、Webhookエンドポイントを通じて
配信されます。6種類の通話ライフサイクルイベント
（`telephony.incoming`、`telephony.complete`、`telephony.tool`、
`web.incoming`、`web.complete`、`web.tool`）は、レガシーの単一URL
Webhookにも**送信されます**。レガシーURLと一致するエンドポイントの両方がある場合、
イベントは**両方**のパスで受信されます。ブロッキング動作
（[`telephony.incoming` / `web.incoming` の構成のやり取り](/ja/webhooks/call-incoming)およびWebhookモードの
[ツールディスパッチ](/ja/tools/overview)）は、レガシーパスでのみ提供されます。
エンドポイントへの配信はすべて、応答を待たない通知です。

## ペイロード形式

エンドポイントへの配信は、`data`、`event_id`、`type` を含むJSONオブジェクトです。

```json theme={null}
{
  "data": {
    "call_id": 987654321,
    "from_number": "+14155550199",
    "to_number": "+15551234567"
  },
  "event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
  "type": "telephony.incoming"
}
```

`event_id` は、送信されたイベントごとに一意です。再試行時も、
**さらに**イベントを受信するすべてのエンドポイント間でも同一です。この値で重複を排除してください。

レガシーの単一URL Webhookは、同じ `type` と `data` を送信しますが、
`event_id` は**含まれません**。

```json theme={null}
{
  "type": "telephony.incoming",
  "data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}
```

通信上では、すべての本文が正規形式でシリアライズされます。キーはアルファベット順に並び、
空白は含まれず、UTF-8を使用します。これらのドキュメントにある整形済みの例は、
読みやすさのためだけに用意されています。

イベントタイプとペイロードフィールドの完全な一覧については、
[イベントカタログ](/ja/webhooks/events)を参照してください。

## 署名の検証

すべてのリクエストには、**生のリクエスト
本文**に対する HMAC-SHA256 署名が `X-ThunderPhone-Signature` ヘッダーに含まれます。署名キーはエンドポイントの
`secret`（またはレガシー配信の場合は組織レベルの webhook `secret`）です。

### 手順

1. 解析を行う**前**に、生のリクエスト本文を読み取ります。
2. `hmac_sha256(secret, body).hexdigest()` を計算します。
3. `X-ThunderPhone-Signature` ヘッダーと定数時間で比較します。

ThunderPhone は送信するバイト列そのものに署名します。これらのバイト列は
正規化された JSON シリアライズ（キーをソートし、区切り文字をコンパクト化）です。そのため、
生の本文に対する検証は常に機能します。フレームワークから解析済み JSON しか取得できない場合も、
キーをソートして区切り文字をコンパクト化して再シリアライズすれば、同一のバイト列が生成されます。両方の方法は[検証ガイド](/ja/guides/verify-webhook-signatures)で説明しています。

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_signature(body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode("utf-8"),
          body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(expected, signature or "")

  # Example Flask handler
  from flask import Flask, request, abort
  app = Flask(__name__)

  @app.post("/thunderphone-webhook")
  def handle():
      body = request.get_data()
      sig = request.headers.get("X-ThunderPhone-Signature", "")
      if not verify_signature(body, sig, WEBHOOK_SECRET):
          abort(401)
      event = request.get_json()
      # dispatch on event["type"] …
      return "", 204
  ```

  ```javascript Node.js (Express) theme={null}
  import crypto from "node:crypto";
  import express from "express";

  function verifySignature(body, signature, secret) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(body)
      .digest("hex");
    if (!signature || expected.length !== signature.length) return false;
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature),
    );
  }

  const app = express();
  app.post(
    "/thunderphone-webhook",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const sig = req.header("X-ThunderPhone-Signature") || "";
      if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
        return res.sendStatus(401);
      }
      const event = JSON.parse(req.body.toString("utf8"));
      // dispatch on event.type …
      res.sendStatus(204);
    },
  );
  ```
</CodeGroup>

## 配信セマンティクス

これらのセマンティクスは **エンドポイント** 配信に適用されます。レガシーの単一 URL
Webhook は、再試行なしの単一同期試行です。

<AccordionGroup>
  <Accordion title="再試行">
    各イベントは直ちに 1 回試行されます。`2xx` 応答はすべて
    配信を確認します。それ以外の結果（非 2xx、
    接続エラー、タイムアウト）の場合、**最初の試行から 1 分、5 分、30 分、2 時間、6 時間、
    12 時間、24 時間後**に再試行します。24 時間にわたる
    合計 8 回の試行です。すべての試行が失敗すると、配信は停止し、エンドポイントは
    [Webhook エンドポイント](/ja/webhooks/endpoints)で
    `status="failing"` としてマークされます。ペイロードが永続的に受理されたら、できるだけ早く
    `2xx` を返してください。処理は非同期で行います。
  </Accordion>

  <Accordion title="順序">
    配信順序はベストエフォートです。実際にはイベントが発行された
    順序で配信しますが、失敗時の再試行によって順序が入れ替わる場合があります。
    常に `call_id` / オブジェクト ID によって重複排除と整合性確認を行ってください。
  </Accordion>

  <Accordion title="重複">
    配信は **少なくとも 1 回**です。確認できなかった応答の後の再試行により、
    イベントが重複する場合があります。すべての再試行には同じ
    `event_id` が含まれるため、処理済み ID を保存し、重複をスキップしてください。`event_id` は
    エンドポイント間でも共有されます。同じイベントを購読する 2 つのエンドポイントは、同じ
    `event_id` を受け取ります。
  </Accordion>

  <Accordion title="タイムアウト">
    エンドポイント配信は、試行ごとに **30 秒**のタイムアウトがあります。
    レガシーパスでは、ライブ通話の動作を制御するブロッキングリクエスト、つまり
    [`telephony.incoming` / `web.incoming`](/ja/webhooks/call-incoming)の
    設定交換は **10 秒**後にタイムアウトします。ただし、応答が遅いと通話の応答が遅延するため、
    数秒以内の応答を目標にしてください。Webhook モードの[ツールディスパッチ](/ja/tools/overview)では 20 秒を使用できます。
  </Accordion>

  <Accordion title="送信元 IP アドレス">
    送信 Webhook は ThunderPhone のクラウド IP アドレス範囲から送信されます。
    ファイアウォールで許可リストが必要な場合は、サポートにお問い合わせください。最新の
    範囲を共有します。
  </Accordion>
</AccordionGroup>

## レガシー Webhook とエンドポイントベース Webhook の選択

| 機能            | レガシー (`/v1/webhook`)                                                    | エンドポイント (`/v1/developer/webhook-endpoints`) |
| ------------- | ----------------------------------------------------------------------- | ------------------------------------------- |
| URL の数        | 組織ごとに 1 つ                                                               | 組織ごとに複数                                     |
| イベント対象範囲      | `telephony.*` / `web.*` のみ                                              | 全 10 イベントタイプ                                |
| イベントフィルター     | —                                                                       | エンドポイントごと                                   |
| 再試行           | なし                                                                      | 24 時間で 8 回試行                                |
| エンベロープ        | `type` + `data`                                                         | `type` + `data` + `event_id`                |
| シークレットローテーション | 単一シークレットを置換                                                             | エンドポイントごとのシークレット                            |
| 削除せずに無効化      | —                                                                       | `status=disabled`                           |
| ステータスの可視性     | —                                                                       | `active` / `disabled` / `failing`           |
| ブロッキング設定交換    | はい（[`telephony.incoming` / `web.incoming`](/ja/webhooks/call-incoming)） | なし — 通知のみ                                   |
| 最適な用途         | 動的な通話設定                                                                 | 本番環境でのイベント処理                                |

新しい統合では、エンドポイントベースの
Webhook を通じてイベントを処理してください。通話応答時に動的に通話を設定する場合、または
Webhook モードのツールディスパッチを使用する場合にのみ、レガシー URL を維持または追加してください。これらの
リクエスト/レスポンス交換はレガシーパスでのみ実行されます。

***

## 関連

<CardGroup cols={2}>
  <Card title="イベントカタログ" icon="list" href="/ja/webhooks/events">
    すべてのイベントタイプとそのペイロード。
  </Card>

  <Card title="Webhook エンドポイント" icon="bolt" href="/ja/webhooks/endpoints">
    複数のエンドポイント、イベントフィルター、シークレットを管理します。
  </Card>

  <Card title="telephony.incoming / web.incoming" icon="phone" href="/ja/webhooks/call-incoming">
    通話を設定するためにサーバーが応答する必要があるブロッキングリクエスト。
  </Card>

  <Card title="telephony.complete / web.complete" icon="phone" href="/ja/webhooks/call-complete">
    文字起こし、録音、メトリクスを含む通話後のペイロード。
  </Card>
</CardGroup>
