> ## 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/webhooks/events)
   （电话）或 [`web.incoming`](/zh/webhooks/events)（小组件）
   事件。两者都是**阻塞式** Webhook：ThunderPhone 在继续通话前最多会
   等待您的响应 10 秒。
2. ThunderPhone 会向您发送 `{call_id, from_number, to_number}`（小组件
   会话会携带小组件专用字段而非号码——请参阅
   [请求架构](/zh/webhooks/call-incoming)）。
3. 您的服务器返回一个智能体配置（提示词、语音、产品、工具）。ThunderPhone
   会在该通话中使用此配置。
4. 如果您返回 `{}`、超时或出错，则会回退使用静态分配的
   智能体。这是一种安全的默认行为。

<Note>
  无论是电话通话（`telephony.incoming`）还是小组件
  会话（`web.incoming`），无论是发送到 Webhook 端点还是旧版单 URL
  Webhook，其工作方式都相同。
</Note>

## 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="Web 小组件">
    对于小组件会话，请创建一个 `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"]
      }'
    ```

    小组件会在每次会话开始时向此 URL 发送 POST 请求。
  </Tab>
</Tabs>

## 2. 实现处理程序

三条经验法则：

* 对每个请求都**验证签名**（请参阅
  [验证 webhook 签名](/zh/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/webhooks/call-incoming)
完全一致。常用字段如下：

| 字段                            | 类型             | 描述                                                         |
| ----------------------------- | -------------- | ---------------------------------------------------------- |
| `prompt`                      | string（必填）     | 智能体的系统提示词                                                  |
| `voice`                       | string（必填）     | 来自 [`GET /v1/voices`](/api-reference/agents#voices) 的语音 ID |
| `product`                     | string         | 默认为 `spark`                                                |
| `background_track`            | string \| null | 环境音频 ID                                                    |
| `acknowledgement_prompt_mode` | string         | `auto` 或 `manual`（仅适用于带确认功能的 Storm）                        |
| `acknowledgement_prompt`      | string         | 当模式为 `manual` 时必填                                          |
| `tools`                       | array          | 内联函数工具架构——请参阅[函数工具](/zh/tools/overview)                    |

<Note>
  webhook 响应中无法设置单次通话的说话顺序和 `max_hold_seconds`。
  请在您引用的
  [智能体](/api-reference/agents)上设置它们。
</Note>

## 模式

### 已登录用户上下文

在 webhook 模式的小部件中，访客所在页面已知其身份。使用小部件 SDK 会转发的查询字符串参数（`?customer_id=123`）调用您的 webhook，并在服务端查找该客户。

### A/B 提示词发布

在自行实现之前，请注意 ThunderPhone 提供原生的 [实验](/zh/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/webhooks/call-incoming">
    精确的请求和响应架构，包括每个配置键。
  </Card>

  <Card title="验证 webhook 签名" icon="shield-check" href="/zh/guides/verify-webhook-signatures">
    一次正确实现 HMAC；随处复用。
  </Card>

  <Card title="构建工具集成" icon="screwdriver-wrench" href="/zh/guides/build-tool-integration">
    将动态路由与每个智能体的工具相结合。
  </Card>

  <Card title="投递语义" icon="bolt" href="/zh/webhooks/overview">
    重试、排序、超时。
  </Card>
</CardGroup>
