> ## 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.

# Webhooks 概覽

> 了解 ThunderPhone 如何傳送即時事件、如何驗證簽章，以及舊版與以端點為基礎的傳送模型之比較。

當通話期間發生事件時，ThunderPhone 會向你的伺服器傳送 HTTP `POST` 請求——例如開始接聽來電、通話結束、評分作業完成、觸發警示等。提供 **兩種傳送模式**：

<CardGroup cols={2}>
  <Card title="Webhook 端點（建議使用）" icon="bolt" href="/zh-Hant/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">
    每個組織一個 URL。承載通話生命週期事件，包括 **阻塞式** 組態交換。
    透過 `GET/PUT /v1/webhook` 管理。
  </Card>
</CardGroup>

[event 目錄](/zh-Hant/webhooks/events)中的全部十種事件類型都會透過 webhook 端點傳送。六種通話生命週期事件
（`telephony.incoming`、`telephony.complete`、`telephony.tool`、
`web.incoming`、`web.complete`、`web.tool`）也會傳送至
舊版單一 URL webhook——如果你同時設定舊版 URL 與相符的端點，將在 **兩個** 路徑上都收到該事件。阻塞行為（[`telephony.incoming` / `web.incoming` 組態
交換](/zh-Hant/webhooks/call-incoming)與 webhook 模式的
[工具分派](/zh-Hant/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。本文文件中經過美化排版的範例
僅為了便於閱讀。

請參閱[事件目錄](/zh-Hant/webhooks/events)，查看完整的事件
類型與承載資料欄位清單。

## 簽章驗證

每個請求都會在 `X-ThunderPhone-Signature` 標頭中，帶有針對**原始請求本文**的 HMAC-SHA256 簽章。簽署金鑰為端點的 `secret`（若為舊版傳遞，則使用組織層級 webhook 的 `secret`）。

### 步驟

1. 在進行任何剖析**之前**讀取原始請求本文。
2. 計算 `hmac_sha256(secret, body).hexdigest()`。
3. 以固定時間方式與 `X-ThunderPhone-Signature` 標頭比較。

我們簽署的正是傳送的位元組，而這些位元組是標準 JSON 序列化格式（排序的鍵、精簡分隔符號）。因此，針對原始本文驗證一定可行——若你的框架只提供已剖析的 JSON，使用排序鍵與精簡分隔符號重新序列化，也會產生完全相同的位元組。這兩種做法皆載於[驗證指南](/zh-Hant/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
網路回呼僅會進行一次同步嘗試，不會重試。

<AccordionGroup>
  <Accordion title="重試">
    每個事件會立即嘗試傳遞一次。任何 `2xx` 回應
    都會確認傳遞。在任何其他結果下（非 2xx、
    連線錯誤、逾時），我們會在**第一次嘗試後的 1 分鐘、5 分鐘、30 分鐘、2 小時、6 小時、
    12 小時及 24 小時**重試——共 8 次嘗試，橫跨
    24 小時。若每次嘗試皆失敗，傳遞將停止，端點
    會在[網路回呼端點](/zh-Hant/webhooks/endpoints)中標示為
    `status="failing"`。只要承載資料已被可靠地接受，請立即回傳 `2xx`；
    並以非同步方式處理。
  </Accordion>

  <Accordion title="排序">
    傳遞排序採盡力而為原則。實務上，我們會依照事件
    發出的順序傳遞，但重試可能會在失敗後改變順序。
    請一律依 `call_id`／物件 id 去除重複並進行協調。
  </Accordion>

  <Accordion title="重複">
    傳遞為**至少一次**：在我們未收到回應後進行重試時，
    可能會重複傳遞事件。每次重試都帶有相同的
    `event_id`，因此請儲存已處理的 id 並略過重複項目。`event_id`
    也會在各端點間共用——訂閱相同事件的兩個端點
    會收到相同的 `event_id`。
  </Accordion>

  <Accordion title="逾時">
    每次端點傳遞的逾時時間為 **30 秒**。在
    舊版路徑上，會影響即時通話行為的阻塞式請求——
    [`telephony.incoming` / `web.incoming`](/zh-Hant/webhooks/call-incoming)
    設定交換——會在 **10 秒**後逾時，但緩慢的
    回應會延遲通話接聽，因此請盡量在幾秒內回應。
    網路回呼模式的[工具分派](/zh-Hant/tools/overview)允許 20 秒。
  </Accordion>

  <Accordion title="來源 IP">
    外傳網路回呼來自 ThunderPhone 的雲端 IP 範圍。
    若你的防火牆需要允許清單，請聯絡支援團隊，我們會
    提供目前的範圍。
  </Accordion>
</AccordionGroup>

## 在舊版與端點式網路回呼之間選擇

| 功能      | 舊版（`/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`](/zh-Hant/webhooks/call-incoming)） | 永不——僅通知                               |
| 最適合     | 動態通話設定                                                                      | 正式環境中的事件接收                            |

新的整合應透過端點式
網路回呼接收事件。僅在你需要於接聽時動態設定通話，
或使用網路回呼模式的工具分派時，才保留（或新增）舊版 URL——這些
請求／回應交換僅會在舊版路徑上執行。

***

## 相關內容

<CardGroup cols={2}>
  <Card title="事件目錄" icon="list" href="/zh-Hant/webhooks/events">
    所有事件類型及其承載資料。
  </Card>

  <Card title="網路回呼端點" icon="bolt" href="/zh-Hant/webhooks/endpoints">
    管理多個端點、事件篩選條件及密鑰。
  </Card>

  <Card title="telephony.incoming / web.incoming" icon="phone" href="/zh-Hant/webhooks/call-incoming">
    你的伺服器必須回應以設定通話的阻塞式請求。
  </Card>

  <Card title="telephony.complete / web.complete" icon="phone" href="/zh-Hant/webhooks/call-complete">
    包含文字記錄、錄音及指標的通話後承載資料。
  </Card>
</CardGroup>
