---
title: "每通電話的動態設定"
description: "針對每一通來電，分別選擇接聽的語音智慧體——或重寫其提示詞與設定——並由你控制的 webhook 中的自訂邏輯驅動。"
---

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

## 運作方式

1. 你訂閱 [`telephony.incoming`](/zh-Hant/webhooks/events)
   （電話）或 [`web.incoming`](/zh-Hant/webhooks/events)（小工具）
   事件。兩者皆為**阻塞式** Webhook：ThunderPhone 會在繼續通話前等待你的
   回應；電話通話最長等待 13 秒（包含一次重試），小工具工作階段最長等待
   10 秒。
2. ThunderPhone 會傳送 `{call_id, from_number, to_number}` 給你（小工具
   工作階段會攜帶小工具專屬欄位，而非電話號碼——請參閱
   [請求結構描述](/zh-Hant/webhooks/call-incoming)）。
3. 你的伺服器會回傳智慧體設定（提示詞、語音、
   產品、工具）。ThunderPhone 會在該通話中使用這項設定。
4. 沒有備援智慧體。如果你回傳 `{}` 或無效的
   設定，或請求在其[重試](/zh-Hant/webhooks/call-incoming)
   後仍然失敗，電話通話會立即結束，並以 `end_reason`
   `inbound_config_webhook_failed` 記錄；小工具工作階段請求則會失敗。

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

<Warning>
  **透過內嵌 Webhook 設定的通話不會播放 ThunderPhone 同意公告。**
  此途徑會略過智慧體層級的通話開始公告，並明確排除於 ThunderPhone 的
  同意公告架構之外（服務條款的「錄音與同意」章節）。你的
  組織須全權負責這些通話的所有錄音、監控、AI 參與及
  來電者身分識別告知與同意。啟用內嵌 Webhook 模式前，請在你自己的流程中
  提供這些告知與同意——例如在提示詞的開場腳本中。一個參照已儲存
  `agent_id` 的 Webhook 回應會使用該智慧體的一般錄音與揭露政策。
</Warning>

## 1. 設定 Webhook 目的地

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

```bash
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
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)）。
  不要在開發環境略過這一步——一次做好，後續重複使用。
- **快速回應**。來電的首次嘗試會在 10
  秒時中斷，整個互動則會在 13 秒時中斷，而每一秒對來電者而言都是空白等待。你可以視需要查詢資料庫，但不要同步呼叫下游 LLM——若要動態產生提示詞，請預先運算並快取。
- **妥善降級處理**。對於任何未預期的狀態，請以預設的 `prompt` 和 `voice`（或已儲存的 `agent_id`）回應。回傳 `{}`
  或錯誤狀態都會結束通話。

<CodeGroup>
```python FastAPI
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
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-with-ack） |
| `acknowledgement_prompt` | 字串 | 模式為 `manual` 時必填 |
| `language_switching` | 字串 | 適用於多語言智慧體，可使用 `ask_first` 或 `follow_caller` |
| `tools` | 陣列 | 內嵌函式工具結構描述——請參閱[函式工具](/zh-Hant/tools/overview) |

<Note>
  每通通話的發話順序與 `max_hold_seconds` 無法在
  webhook 回應中設定。請在你參照的
  [智慧體](/api-reference/agents)上設定。
</Note>

### 保留已儲存的智慧體並提供變數

傳回 `{"agent_id": 12, "variables": {"name": "Ada"}}`，即可使用該組織已儲存的
智慧體並提供每通電話的資料。其提示詞可包含 `{{name}}` 或
`{{name|Friend}}`。Webhook 變數會合併並覆寫請求層級的變數；null
會使用預留位置的預設值，若未提供預設值則使用空白文字。最終
值和未解析的名稱會顯示於通話詳細資料及完成 Webhook 中。
已儲存智慧體的回應僅接受 `agent_id`、`variables` 和 `timezone`。若存在 `prompt`，
回應會使用內嵌設定並忽略 `agent_id`（包括
null 或非整數的中繼資料）；內嵌提示詞仍必須有效。內嵌
設定回應也可包含 `variables`。已儲存智慧體的回應會在電話和小工具通話中使用
智慧體已部署的 A/B 分流，然後轉譯變數。請參閱[通話變數](/zh-Hant/guides/call-variables)
以瞭解限制與工作階段 API 支援。封鎖式設定來自舊版
電話號碼／組織 URL 或 Webhook 模式的小工具金鑰；端點系統的
傳入事件僅為通知。

### 為每通通話設定時區

同一個智慧體可為不同時區的聯絡人接聽來電。將 IANA 名稱 `timezone` 加入已儲存智慧體或內嵌回應中，智慧體便會在這通通話中以該時區說明日期與時間，並提供該時區的可選時段，而非使用智慧體自身的時區：

```json
{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}
```

請傳送時區，而非在 `variables` 中傳送時間戳記：智慧體的時鐘會在通話期間持續運作，你傳入的時間會過期。非 IANA 時區的值會被忽略，通話將以智慧體的時區接通。

## 模式

### 已登入使用者情境

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

### A/B 提示詞逐步推出

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

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

### 依時間路由

營業時間 →「真人支援」智慧體；非營業時間 →「留言」
智慧體。在處理常式中單純根據 `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>
