---
title: "Cấu hình động cho từng cuộc gọi"
description: "Chọn tác nhân AI trả lời — hoặc viết lại prompt và cài đặt của tác nhân — riêng cho từng cuộc gọi đến, dựa trên logic tùy chỉnh trong webhook do bạn kiểm soát."
---

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ờ phản hồi của bạn
   trước khi tiếp tục cuộc gọi, tối đa 13 giây đối với cuộc gọi điện thoại
   (bao gồm một lần thử lại) và 10 giây đối với phiên widget.
2. ThunderPhone gửi cho bạn `{call_id, from_number, to_number}` (phiên widget
   chứa 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. Không có tác nhân AI dự phòng. Nếu bạn trả về `{}` hoặc một cấu hình
   không hợp lệ, hoặc yêu cầu vẫn thất bại sau
   [lần thử lại](/vi/webhooks/call-incoming), cuộc gọi điện thoại
   sẽ bị kết thúc ngay và được ghi lại với `end_reason`
   `inbound_config_webhook_failed`; yêu cầu phiên widget sẽ thất bại.

<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 điểm cuối webhook
  hay webhook URL đơn cũ.
</Note>

<Warning>
  **Không có thông báo đồng ý của ThunderPhone nào được phát trong các cuộc gọi
  được cấu hình bằng webhook nội tuyến.** Luồng này bỏ qua thông báo khi bắt đầu
  cuộc gọi ở cấp tác nhân AI và được loại trừ rõ ràng khỏi khuôn khổ thông báo
  đồng ý của ThunderPhone (Điều khoản dịch vụ, mục "Ghi âm và đồng ý"). Tổ chức
  của bạn chịu trách nhiệm cho mọi thông báo và sự đồng ý về ghi âm, giám sát,
  sự tham gia của AI và nhận diện người gọi trong các cuộc gọi này. Hãy cung cấp
  chúng trong luồng riêng của bạn — ví dụ trong kịch bản mở đầu của prompt —
  trước khi bật chế độ webhook nội tuyến. Phản hồi webhook tham chiếu đến
  `agent_id` đã lưu sẽ sử dụng chính sách ghi âm và công bố thông tin thông thường
  của tác nhân AI đó.
</Warning>

## 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ý điểm cuối của bạn với `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"]
  }'
```

Phản hồi bao gồm `secret` dùng 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 khóa có thể công khai ở `mode="webhook"`
với URL điểm cuối của bạn được nhúng sẵn:

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

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

## 2. Triển khai handler

Ba quy tắc thực hành:

- **Xác minh chữ ký** trên mọi yêu cầu (xem
  [Xác minh chữ ký webhook](/vi/guides/verify-webhook-signatures)).
  Đừng bỏ qua bước này trong môi trường dev — thực hiện đúng một lần rồi tái sử dụng.
- **Phản hồi nhanh**. Lần thử đầu tiên của một cuộc gọi sẽ bị ngắt sau 10
  giây và toàn bộ trao đổi sau 13 giây, và mỗi giây đều là khoảng lặng
  với người gọi. Hãy tra cứu cơ sở dữ liệu nếu cần, nhưng đừng gọi đồng bộ
  các LLM ở hạ nguồn — nếu 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**. Với mọi trạng thái không mong đợi, hãy phản hồi bằng
  `prompt` và `voice` mặc định (hoặc `agent_id` đã lưu). Trả về `{}`
  hoặc trạng thái lỗi sẽ kết thúc cuộc gọi.

<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. Lược đồ phản hồi

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

| Trường | Loại | 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`. Sản phẩm thử nghiệm chỉ dành cho tổ chức trong danh sách cho phép. |
| `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ự phát lời theo từng cuộc gọi và `max_hold_seconds` không khả dụng trong
  phản hồi webhook. Thiết lập chúng trên
  [Tác nhân AI](/api-reference/agents) mà bạn tham chiếu.
</Note>

### Giữ một tác nhân AI đã lưu và cung cấp biến

Trả về `{"agent_id": 12, "variables": {"name": "Ada"}}` để sử dụng tác nhân AI
đã lưu của tổ chức đó với dữ liệu theo từng cuộc gọi. Prompt của tác nhân có thể chứa `{{name}}` hoặc
`{{name|Friend}}`. Biến webhook được hợp nhất đè lên biến ở cấp yêu cầu; giá trị null
sử dụng giá trị mặc định của chỗ giữ chỗ hoặc văn bản trống nếu không có giá trị mặc định. Các giá trị cuối cùng
và tên chưa được phân giải xuất hiện trong chi tiết cuộc gọi và webhook hoàn tất.
Phản hồi tác nhân đã lưu chỉ chấp nhận `agent_id`, `variables` và `timezone`. Nếu có `prompt`,
phản hồi sẽ dùng cấu hình nội tuyến và bỏ qua `agent_id` (bao gồm
siêu dữ liệu null hoặc không phải số nguyên); prompt nội tuyến vẫn phải hợp lệ. Phản hồi cấu hình
nội tuyến cũng có thể bao gồm `variables`. Phản hồi tác nhân đã lưu sử dụng phân chia A/B đã triển khai của
tác nhân trên cả cuộc gọi điện thoại và cuộc gọi widget, sau đó hiển thị biến. Xem [biến cuộc gọi](/vi/guides/call-variables)
để biết giới hạn và khả năng hỗ trợ API phiên. Cấu hình chặn đến từ URL
số điện thoại/tổ chức cũ hoặc khóa widget ở chế độ webhook; các sự kiện đến của hệ thống điểm cuối chỉ là thông báo.

### Đặt múi giờ cho từng cuộc gọi

Một tác nhân AI có thể trả lời cho liên hệ ở các múi giờ khác nhau. Thêm `timezone`,
một tên IANA, vào phản hồi tác nhân đã lưu hoặc phản hồi nội tuyến và tác nhân sẽ nêu ngày
và giờ cũng như đề xuất thời gian theo múi giờ đó cho cuộc gọi này, thay vì múi giờ
của chính tác nhân:

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

Gửi múi giờ thay vì dấu thời gian trong `variables`: đồng hồ của tác nhân vẫn tiếp tục
chạy trong cuộc gọi và thời gian bạn truyền vào sẽ trở nên lỗi thời. Giá trị không phải
là múi giờ IANA sẽ bị bỏ qua và cuộc gọi kết nối theo múi giờ của tác nhân.

## Mẫu

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

Trong widget ở chế độ webhook, trang của khách truy cập đã biết họ
là ai. Gọi webhook của bạn bằng 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ự xây dựng cách này, hãy lưu ý rằng ThunderPhone có tính năng
[Thử nghiệm](/vi/guides/concepts) gốc
(`/dashboard/experiments` và tab **A/B** của trình tạo tác nhân AI) để
xác định 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 quyền kiểm soát ở phía webhook: băm `call_id` → nhóm;
phục vụ prompt A cho `0..49` và prompt B cho `50..99`. Ghi lại nhóm
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

Trong giờ làm việc → tác nhân AI "hỗ trợ trực tiếp"; ngoài giờ → tác nhân AI "nhận lời nhắn".
Chỉ cần chuyển nhánh theo `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">
    Lược đồ yêu cầu + 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 đúng 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ông cụ theo từng tác nhân AI.
  </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>
