> ## 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 中的自訂邏輯，為每通來電選擇智慧體，或重寫提示詞。

預設情況下，每個電話號碼和可公開金鑰都會指派一個靜態智慧體。當你需要針對**每位來電者**或**每位訪客**進行自訂——VIP 路由、已登入使用者情境、A/B 提示詞測試——請切換至 Webhook 模式，讓你的伺服器決定。

## 運作方式

1. 訂閱 [`telephony.incoming`](/zh-Hant/webhooks/events)
   （電話）或 [`web.incoming`](/zh-Hant/webhooks/events)（小工具）
   事件。兩者都是**阻塞式** Webhook：ThunderPhone 會在繼續通話前，最多等待
   10 秒以取得你的回應。
2. ThunderPhone 會傳送 `{call_id, from_number, to_number}` 給你（小工具
   工作階段會傳送小工具專用欄位，而非號碼——請參閱
   [請求結構描述](/zh-Hant/webhooks/call-incoming)）。
3. 你的伺服器會回應一個智慧體設定（提示詞、語音、
   產品、工具）。ThunderPhone 會在該通話中使用此設定。
4. 若你回傳 `{}`、逾時或發生錯誤，系統會使用靜態指派的
   智慧體作為備援。安全的預設行為。

<Note>
  無論是電話通話（`telephony.incoming`）還是小工具
  工作階段（`web.incoming`），無論傳送至 Webhook 端點
  或舊版單一 URL Webhook，運作方式皆相同。
</Note>

<Warning>
  **透過 Webhook 設定的通話不會播放 ThunderPhone 同意聲明。**
  此流程會略過智慧體層級的通話開始聲明，且明確排除於 ThunderPhone 的
  同意聲明架構之外（服務條款中的「錄音與同意」章節）。你的組織須自行負責這些
  通話中的所有錄音、監控、人工智慧參與及來電者身分識別通知與同意。請在啟用
  Webhook 模式前，於你自己的流程中提供這些內容——例如在提示詞的開場腳本中。
</Warning>

## 1. 設定 Webhook 目的地

<Tabs>
  <Tab title="電話通話">
    針對電話號碼，將你的端點訂閱至 `telephony.incoming`：

    ```bash theme={null}
    curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "label":  "Prod call-incoming",
        "url":    "https://example.com/thunderphone/incoming",
        "events": ["telephony.incoming"]
      }'
    ```

    回應會包含一次性 `secret`——請儲存它；你將用它
    進行簽章驗證。
  </Tab>

  <Tab title="網頁小工具">
    針對小工具工作階段，建立一個 `mode="webhook"` 的可公開金鑰，
    並內嵌你的端點 URL：

    ```bash theme={null}
    curl -X POST https://api.thunderphone.com/v1/publishable-key \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name":            "Dynamic widget",
        "mode":            "webhook",
        "webhook_url":     "https://example.com/thunderphone/widget-incoming",
        "allowed_domains": ["example.com"]
      }'
    ```

    每次工作階段開始時，小工具都會 POST 至此 URL。
  </Tab>
</Tabs>

## 2. 實作處理常式

三項實用原則：

* 在每個請求中**驗證簽章**（請參閱
  [驗證 Webhook 簽章](/zh-Hant/guides/verify-webhook-signatures)）。
  即使在開發環境也不要略過——一次做好，之後重複使用。
* **快速回應**。十秒是硬性上限，而每一秒對來電者而言都是
  無聲等待。若有需要可以查詢資料庫，但不要同步呼叫下游 LLM——若要動態產生提示詞，請預先運算並快取。
* **妥善回退**。任何非預期狀態都應回傳 `{}`，讓靜態指派的智慧體處理通話。

<CodeGroup>
  ```python FastAPI theme={null}
  import hashlib
  import hmac
  import json
  import os

  from fastapi import FastAPI, HTTPException, Request

  app = FastAPI()
  SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]

  def verify(body: bytes, sig: str) -> bool:
      expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, sig or "")

  @app.post("/thunderphone/incoming")
  async def incoming(request: Request):
      body = await request.body()
      if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
          raise HTTPException(401)

      event = json.loads(body)
      if event["type"] not in ("telephony.incoming", "web.incoming"):
          return {}  # fall back to default

      caller = event["data"]["from_number"]
      # Cheap DB lookup: is this a known VIP?
      customer = lookup_customer(caller)
      if customer and customer.tier == "vip":
          return {
              "prompt":  f"You are a VIP concierge for {customer.name}. Be proactive…",
              "voice":   "john",
              "product": "storm-base",
          }
      return {}  # default agent handles non-VIPs

  def lookup_customer(phone: str):
      # ... your CRM integration ...
      pass
  ```

  ```javascript Express theme={null}
  import crypto from "node:crypto";
  import express from "express";

  const app = express();
  const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;

  function verify(body, sig) {
    const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
    return sig &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  }

  app.post(
    "/thunderphone/incoming",
    express.raw({ type: "application/json" }),
    async (req, res) => {
      if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
        return res.sendStatus(401);
      }
      const event = JSON.parse(req.body.toString("utf8"));

      const IMPORTANT_TYPES = new Set([
        "telephony.incoming",
        "web.incoming",
      ]);
      if (!IMPORTANT_TYPES.has(event.type)) return res.json({});

      const customer = await lookupCustomer(event.data.from_number);
      if (customer?.tier === "vip") {
        return res.json({
          prompt:  `You are a VIP concierge for ${customer.name}. Be proactive…`,
          voice:   "john",
          product: "storm-base",
        });
      }
      res.json({}); // fall back to default agent
    },
  );
  ```
</CodeGroup>

## 3. 回應結構描述

回應本文會完全符合
[來電回應結構描述](/zh-Hant/webhooks/call-incoming)。
常用欄位如下：

| 欄位                            | 類型         | 說明                                                         |
| ----------------------------- | ---------- | ---------------------------------------------------------- |
| `prompt`                      | 字串（必填）     | 智慧體的系統提示詞                                                  |
| `voice`                       | 字串（必填）     | 來自 [`GET /v1/voices`](/api-reference/agents#voices) 的語音 ID |
| `product`                     | 字串         | 預設為 `spark`                                                |
| `background_track`            | 字串 \| null | 環境音訊 ID                                                    |
| `acknowledgement_prompt_mode` | 字串         | `auto` 或 `manual`（僅限含確認提示詞的 Storm）                         |
| `acknowledgement_prompt`      | 字串         | 模式為 `manual` 時必填                                           |
| `tools`                       | 陣列         | 內嵌函式工具結構描述——請參閱[函式工具](/zh-Hant/tools/overview)             |

<Note>
  Webhook 回應不提供單次通話的發話順序和 `max_hold_seconds`。
  請在你所參照的
  [智慧體](/api-reference/agents)上設定這些項目。
</Note>

## 模式

### 已登入使用者情境

在 webhook 模式的小工具中，訪客所在頁面已經知道其身分。使用小工具 SDK 會轉送的查詢字串參數（`?customer_id=123`）呼叫你的 webhook，並在伺服器端查詢客戶資料。

### A/B 提示詞發布

在自行實作前，請注意 ThunderPhone 內建 [實驗](/zh-Hant/guides/concepts)功能
（`/dashboard/experiments` 與智慧體建置工具中的 **A/B** 分頁），可定義變體、分配流量，並比較各變體的結果——無需 webhook。

若你仍需要在 webhook 端控制：將 `call_id` 雜湊至 bucket；對 `0..49` 提供提示詞 A，對 `50..99` 提供提示詞 B。在你自己的資料庫中記錄所選擇的 bucket，之後再與已完成通話的評分建立關聯。

### 依時間路由

營業時間 →「真人支援」智慧體；非營業時間 →「留言」智慧體。在處理常式中單純依 `new Date().getUTCHours()` 切換即可。

***

## 後續步驟

<CardGroup cols={2}>
  <Card title="來電 webhook 參考資料" icon="phone" href="/zh-Hant/webhooks/call-incoming">
    完整的請求與回應結構描述，包括每個設定鍵。
  </Card>

  <Card title="驗證 webhook 簽章" icon="shield-check" href="/zh-Hant/guides/verify-webhook-signatures">
    一次正確設定 HMAC，處處重複使用。
  </Card>

  <Card title="建立工具整合" icon="screwdriver-wrench" href="/zh-Hant/guides/build-tool-integration">
    將動態路由與各智慧體專用工具結合。
  </Card>

  <Card title="傳遞語意" icon="bolt" href="/zh-Hant/webhooks/overview">
    重試、排序、逾時。
  </Card>
</CardGroup>
