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

# Cấu hình động cho từng cuộc gọi

> Chọn một tác nhân AI — hoặc viết lại prompt — cho mỗi cuộc gọi đến dựa trên logic tùy chỉnh trong webhook.

Theo mặc định, mỗi số điện thoại và khóa có thể công khai đều được gán một tác nhân AI tĩnh. Khi bạn cần tùy chỉnh **theo từng người gọi** hoặc **theo từng khách truy cập** — định tuyến VIP, ngữ cảnh người dùng đã đăng nhập, kiểm thử prompt A/B — hãy chuyển sang chế độ webhook và để máy chủ của bạn quyết định.

## Cách hoạt động

1. Bạn đăng ký sự kiện [`telephony.incoming`](/vi/webhooks/events)
   (điện thoại) hoặc [`web.incoming`](/vi/webhooks/events) (widget).
   Cả hai đều là webhook **chặn**: ThunderPhone chờ tối đa
   10 giây để nhận phản hồi của bạn trước khi tiếp tục cuộc gọi.
2. ThunderPhone gửi cho bạn `{call_id, from_number, to_number}` (phiên widget
   mang các trường dành riêng cho widget thay vì số điện thoại — xem
   [schema yêu cầu](/vi/webhooks/call-incoming)).
3. Máy chủ của bạn phản hồi bằng cấu hình tác nhân AI (prompt, giọng nói,
   sản phẩm, công cụ). ThunderPhone sử dụng cấu hình đó cho cuộc gọi.
4. Nếu bạn trả về `{}`, hết thời gian chờ hoặc gặp lỗi, tác nhân AI được gán tĩnh
   sẽ được dùng làm phương án dự phòng. Mặc định an toàn.

<Note>
  Hoạt động giống hệt cho cuộc gọi điện thoại (`telephony.incoming`) và phiên
  widget (`web.incoming`), dù được gửi đến endpoint webhook
  hay webhook URL đơn cũ.
</Note>

## 1. Cấu hình đích webhook

<Tabs>
  <Tab title="Cuộc gọi điện thoại">
    Đối với số điện thoại, đăng ký endpoint của bạn với `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"]
      }'
    ```

    Phản hồi bao gồm một `secret` chỉ hiển thị một lần — hãy lưu lại; bạn sẽ dùng nó
    để xác minh chữ ký.
  </Tab>

  <Tab title="Widget web">
    Đối với phiên widget, tạo một khóa có thể công khai ở `mode="webhook"`
    với URL endpoint của bạn được tích hợp sẵn:

    ```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"]
      }'
    ```

    Widget sẽ POST đến URL này mỗi khi một phiên bắt đầu.
  </Tab>
</Tabs>

## 2. Triển khai handler

Ba nguyên tắc cơ bản:

* **Xác minh chữ ký** trên mọi request (xem
  [Xác minh chữ ký webhook](/vi/guides/verify-webhook-signatures)).
  Đừng bỏ qua việc này trong môi trường dev — làm đúng một lần rồi tái sử dụng.
* **Phản hồi nhanh**. Mười giây là giới hạn cứng, và mỗi giây đều là
  khoảng lặng đối với người gọi. Thực hiện tra cứu cơ sở dữ liệu nếu cần, nhưng
  đừng gọi LLM hạ nguồn đồng bộ — nếu bạn muốn tạo prompt động, hãy tính trước và lưu vào bộ nhớ đệm.
* **Dự phòng gọn gàng**. Mọi trạng thái không mong đợi phải trả về `{}` để
  tác nhân AI được gán tĩnh xử lý cuộc gọi.

<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. Schema phản hồi

Nội dung phản hồi khớp chính xác với
[schema phản hồi cuộc gọi đến](/vi/webhooks/call-incoming).
Các trường thường dùng:

| Trường                        | Kiểu             | Mô tả                                                                |
| ----------------------------- | ---------------- | -------------------------------------------------------------------- |
| `prompt`                      | chuỗi (bắt buộc) | System prompt cho tác nhân AI                                        |
| `voice`                       | chuỗi (bắt buộc) | ID giọng nói từ [`GET /v1/voices`](/api-reference/agents#voices)     |
| `product`                     | chuỗi            | Mặc định là `spark`                                                  |
| `background_track`            | chuỗi \| null    | ID âm thanh nền                                                      |
| `acknowledgement_prompt_mode` | chuỗi            | `auto` hoặc `manual` (chỉ Storm-with-ack)                            |
| `acknowledgement_prompt`      | chuỗi            | Bắt buộc khi chế độ là `manual`                                      |
| `tools`                       | mảng             | Schema công cụ hàm nội tuyến — xem [Công cụ hàm](/vi/tools/overview) |

<Note>
  Thứ tự nói theo từng cuộc gọi và `max_hold_seconds` không có trong
  phản hồi webhook. Hãy thiết lập chúng trên
  [Tác nhân AI](/api-reference/agents) mà bạn tham chiếu.
</Note>

## Mẫu

### Ngữ cảnh người dùng đã đăng nhập

Trong các widget ở chế độ webhook, trang của khách truy cập đã biết họ là ai. Gọi webhook của bạn với tham số chuỗi truy vấn mà SDK widget chuyển tiếp (`?customer_id=123`) và tra cứu khách hàng ở phía máy chủ.

### Triển khai prompt A/B

Trước khi tự triển khai, lưu ý rằng ThunderPhone có tính năng gốc [Thử nghiệm](/vi/guides/concepts) (`/dashboard/experiments` và tab **A/B** của trình xây dựng tác nhân) để xác định các biến thể, phân chia lưu lượng và so sánh kết quả theo từng biến thể — không cần webhook.

Nếu bạn vẫn cần kiểm soát ở phía webhook: băm `call_id` → bucket; cung cấp prompt A cho `0..49` và prompt B cho `50..99`. Ghi lại bucket bạn đã chọn trong DB riêng và sau đó đối chiếu với điểm của cuộc gọi đã hoàn tất.

### Định tuyến theo thời gian

Giờ làm việc → tác nhân "hỗ trợ trực tiếp"; ngoài giờ → tác nhân "ghi lời nhắn". Chỉ cần chuyển đổi dựa trên `new Date().getUTCHours()` trong trình xử lý của bạn.

***

## Bước tiếp theo

<CardGroup cols={2}>
  <Card title="Tham chiếu webhook cuộc gọi đến" icon="phone" href="/vi/webhooks/call-incoming">
    Schema yêu cầu và phản hồi chính xác, bao gồm mọi khóa cấu hình.
  </Card>

  <Card title="Xác minh chữ ký webhook" icon="shield-check" href="/vi/guides/verify-webhook-signatures">
    Thiết lập HMAC chính xác một lần; tái sử dụng ở mọi nơi.
  </Card>

  <Card title="Xây dựng tích hợp công cụ" icon="screwdriver-wrench" href="/vi/guides/build-tool-integration">
    Kết hợp định tuyến động với các công cụ theo từng tác nhân.
  </Card>

  <Card title="Ngữ nghĩa phân phối" icon="bolt" href="/vi/webhooks/overview">
    Thử lại, thứ tự, thời gian chờ.
  </Card>
</CardGroup>
