---
title: "Webhooks 概覽"
description: "了解 ThunderPhone 如何傳送即時事件、如何驗證簽名，以及舊版與端點式傳送模式的比較。"
---

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

<CardGroup cols={2}>
  <Card title="Webhook 端點（建議使用）" icon="bolt" href="/yue/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>

[事件目錄](/yue/webhooks/events)中的全部十種事件類型，都會透過 webhook
端點傳送。六種通話生命週期事件
（`telephony.incoming`、`telephony.complete`、`telephony.tool`、
`web.incoming`、`web.complete`、`web.tool`）亦會傳送至舊版單一 URL
webhook——如果你同時設有舊版 URL 和相符的端點，便會在**兩個**路徑
收到該事件。阻塞式行為（[`telephony.incoming` / `web.incoming` 設定
交換](/yue/webhooks/call-incoming)及 webhook 模式的
[工具分派](/yue/tools/overview)）只會在舊版路徑上運作；
每次端點傳送均為即發即棄通知。

## Payload 格式

端點傳送的內容為包含 `data`、`event_id` 及 `type` 的 JSON 物件：

```json
{
  "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
{
  "type": "telephony.incoming",
  "data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}
```

在網絡傳輸中，每個主體均會以標準格式序列化——鍵名按字母順序排列、
不含空白字元、採用 UTF-8。本文檔中的美化格式範例僅為方便閱讀。

請參閱[事件目錄](/yue/webhooks/events)，了解完整的事件類型及 payload 欄位清單。

## 簽章驗證

每個請求均會在 `X-ThunderPhone-Signature` 標頭中附帶涵蓋**原始請求內容**
的 HMAC-SHA256 簽章。簽署金鑰為端點的 `secret`（或舊版傳送所使用的組織層級 webhook `secret`）。

### 步驟

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

我們會為實際傳送的位元組進行簽署，而這些位元組採用標準 JSON 序列化格式（已排序的鍵、精簡分隔符）。因此，根據原始內容驗證一定有效——如你的框架只提供已解析的 JSON，使用已排序的鍵及精簡分隔符重新序列化，便會產生完全相同的位元組。兩種做法均於[驗證指南](/yue/guides/verify-webhook-signatures)中說明。

<CodeGroup>
```python Python
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)
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="重試">
    每個事件會立即嘗試傳送一次。任何 `2xx` 回應
    均確認傳送成功。如出現任何其他結果（非 2xx、
    連線錯誤、逾時），我們會在**首次嘗試後 1 分鐘、5 分鐘、30 分鐘、2 小時、6 小時、
    12 小時及 24 小時**重試——合共 8 次嘗試，
    橫跨 24 小時。如每次嘗試均失敗，傳送將會停止，端點
    會在[webhook 端點](/yue/webhooks/endpoints)中標示為
    `status="failing"`。payload 一經可靠接收，請立即傳回 `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`](/yue/webhooks/call-incoming)
    設定交換——會在 **10 秒**後逾時；不過，緩慢的回應會延遲接聽來電，
    因此建議在數秒內作出回應。Webhook 模式的[工具調度](/yue/tools/overview)
    預設允許 20 秒，而工具宣告可設定頂層 `timeout`。
  </Accordion>

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

## 選擇舊版或以端點為本的 webhook

| 功能 | 舊版（`/v1/webhook`） | 端點（`/v1/developer/webhook-endpoints`） |
|---------|------------------------|----------------------------------------------|
| URL 數量 | 每個組織 1 個 | 每個組織多個 |
| 事件涵蓋範圍 | 僅限 `telephony.*`／`web.*` | 全部 10 種事件類型 |
| 事件篩選 | — | 按端點設定 |
| 重試 | 沒有 | 24 小時內 8 次嘗試 |
| 封裝格式 | `type` + `data` | `type` + `data` + `event_id` |
| 密鑰輪換 | 取代單一密鑰 | 每個端點獨立密鑰 |
| 不刪除即可停用 | 使用 `{"url": ""}` 執行 `PUT /v1/webhook` | `status=disabled` |
| 狀態可見度 | — | `active`／`disabled`／`failing` |
| 阻塞式設定交換 | 是（[`telephony.incoming` / `web.incoming`](/yue/webhooks/call-incoming)） | 永不——僅限通知 |
| 最適合 | 動態通話設定 | 正式環境中的事件處理 |

新的整合應透過以端點為本的 webhook 處理事件。
只有在你需要於接聽時動態設定通話，或使用 webhook 模式工具調度時，
才應保留（或新增）舊版 URL——這些請求／回應交換只會在舊版路徑上執行。

---

## 相關內容

<CardGroup cols={2}>
  <Card title="事件目錄" icon="list" href="/yue/webhooks/events">
    所有事件類型及其 payload。
  </Card>
  <Card title="Webhook 端點" icon="bolt" href="/yue/webhooks/endpoints">
    管理多個端點、事件篩選條件及密鑰。
  </Card>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/yue/webhooks/call-incoming">
    你的伺服器必須回應以設定通話的阻塞式請求。
  </Card>
  <Card title="telephony.complete / web.complete" icon="phone" href="/yue/webhooks/call-complete">
    通話後 payload，包括文字記錄、錄音及指標。
  </Card>
</CardGroup>
