---
title: "telephony.incoming / web.incoming"
description: "Webhook chặn định hình cấu hình cuộc gọi đến theo thời gian thực."
---

Khi một cuộc gọi điện thoại đến một số **chưa được gán tác nhân
AI**, hoặc một phiên widget web bắt đầu trên khóa có thể công khai ở
`mode="webhook"`, ThunderPhone gửi một yêu cầu **chặn**
`telephony.incoming` / `web.incoming` đến
[URL webhook cũ](/api-reference/organizations#legacy-single-url-webhook)
của bạn và chờ phản hồi cấu hình: tối đa **13 giây** cho một cuộc gọi
điện thoại, bao gồm một lần thử lại, và tối đa **10 giây** cho một phiên
widget (xem [Thời gian chờ và thử lại](#timeouts-and-retries)). Dùng
trao đổi này để chọn động prompt, giọng nói và công cụ cho từng cuộc gọi —
xem [hướng dẫn cấu hình cuộc gọi động](/vi/guides/dynamic-call-config)
để biết quy trình toàn diện.

<Note>
  Các [điểm cuối webhook](/vi/webhooks/endpoints) đã đăng ký cũng nhận
  `telephony.incoming` / `web.incoming` — cho **mọi** cuộc gọi đến
  và phiên web, dù đã cấu hình tác nhân AI hay chưa — nhưng các lần gửi đó là
  thông báo gửi đi không cần chờ phản hồi với `event_id`, không bao giờ chặn.
  Chỉ webhook URL đơn cũ mới thực hiện quy trình trao đổi cấu hình
  trên trang này. Hình dạng thông báo điểm cuối có trong
  [danh mục sự kiện](/vi/webhooks/events).
</Note>

## Hết thời gian chờ và thử lại

Phản hồi nhanh: người gọi đang chờ trên đường dây trong khi bạn quyết định.

Đối với cuộc gọi điện thoại (`telephony.incoming`):

- Lần thử đầu tiên có thể mất tối đa **10 giây**.
- Nếu hết thời gian chờ, không thể kết nối hoặc nhận được phản hồi `5xx`, `408` hay `429`, ThunderPhone sẽ thử lại một lần nữa. Lần thử lại nhận phần còn lại của ngân sách **13 giây** cho toàn bộ quá trình trao đổi: khoảng 3 giây sau lần thử đầu tiên hết thời gian chờ, hoặc gần như toàn bộ 13 giây sau một lỗi xảy ra nhanh.
  Sẽ không thử lại nếu còn chưa đến 2 giây.
- Bất kỳ phản hồi `4xx` nào khác, phản hồi vượt quá
  [giới hạn kích thước](#response-size-limit), hoặc phản hồi `2xx` có phần thân không phải là cấu hình hợp lệ đều là cuối cùng và không được thử lại.
- Cả hai lần thử đều gửi cùng phần thân, cùng tiêu đề `X-ThunderPhone-Signature`,
  và cùng tiêu đề `X-ThunderPhone-Idempotency-Key`
  (`telephony-incoming-<call_id>`), vì vậy trình xử lý có tác dụng phụ
  có thể nhận biết lần thử lại.

Không có tác nhân dự phòng. Nếu không nhận được cấu hình có thể sử dụng, cuộc gọi sẽ được kết thúc ngay thay vì để người gọi chờ trên đường dây im lặng. Cuộc gọi xuất hiện trong lịch sử cuộc gọi là cuộc gọi thất bại, không tính phí với `end_reason` là `inbound_config_webhook_failed`, và sự kiện [`telephony.complete`](/vi/webhooks/call-complete) thông thường sẽ được gửi.

Một phiên widget (`web.incoming`) chỉ có một lần thử tối đa 10 giây. Nếu thất bại, yêu cầu phiên sẽ thất bại với `502` hoặc `422`.

<Warning>
  **Các cuộc gọi được cấu hình qua webhook không có thông báo chấp thuận của ThunderPhone.** Các cuộc gọi được cấu hình thông qua quy trình này bỏ qua thông báo đầ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 chấp thuận của ThunderPhone (Điều khoản dịch vụ, mục "Ghi âm và chấp thuận"). Tổ chức của bạn hoàn toàn chịu trách nhiệm cho mọi thông báo và sự chấp thuận về ghi âm, giám sát, sự tham gia của AI và nhận diện người gọi được yêu cầu cho các cuộc gọi này — các cuộc gọi vẫn có thể được ghi âm, chép lời, phân tích và xử lý bởi tác nhân AI. Hãy xây dựng các nội dung thông báo bắt buộc vào luồng cuộc gọi của riêng bạn trước khi bật phương thức này.
</Warning>

## Payload yêu cầu

Đối với cuộc gọi điện thoại (`telephony.incoming`):

```json
{
  "type": "telephony.incoming",
  "data": {
    "call_id":     987654321,
    "from_number": "+14155550199",
    "to_number":   "+15551234567"
  }
}
```

| Trường | Kiểu | Mô tả |
|-------|------|-------------|
| `call_id` | integer | ID cuộc gọi — không đổi trên mọi sự kiện của cuộc gọi này |
| `from_number` | string | Số điện thoại người gọi theo E.164 |
| `to_number` | string | Đích đến theo E.164 (một trong các số ThunderPhone của bạn) |

Đối với phiên widget web (`web.incoming`), `data` xác định trang nhúng thay vì số điện thoại:

```json
{
  "type": "web.incoming",
  "data": {
    "call_id": 987654322,
    "origin_domain": "https://example.com",
    "publishable_key_prefix": "pk_live_a1b2"
  }
}
```

| Trường | Kiểu | Mô tả |
|-------|------|-------------|
| `call_id` | integer | ID cuộc gọi |
| `origin_domain` | string | Nguồn gốc của trang lưu trữ widget, theo thông tin trình duyệt gửi. Ứng dụng không phải trình duyệt có thể gửi bất kỳ giá trị nào, vì vậy không xem đây là bằng chứng về nơi yêu cầu xuất phát |
| `publishable_key_prefix` | string | Các ký tự đầu của publishable key đã mở phiên |
| `language`, `primary_language` | string | Có mặt khi phiên widget yêu cầu ghi đè ngôn ngữ |
| `voice` | string | Có mặt khi phiên widget yêu cầu ghi đè giọng nói |
| `website_context` | string | Có mặt khi widget truyền ngữ cảnh trang theo từng phiên |

Các trường ghi đè (`language`, `voice`, `website_context` và mọi
`variables` của trình duyệt) chỉ được gửi khi publishable key cho phép chúng trong
[các ghi đè của trình duyệt](/vi/guides/embed-web-widget). Yêu cầu phiên chứa một trường mà key không cho phép sẽ bị từ chối trước khi webhook này được gọi.

<Note>
  Các widget ở chế độ webhook gửi yêu cầu này đến `webhook_url` riêng
  của khóa publishable khi được thiết lập, và dùng URL webhook cấp tổ chức
  nếu không có. Dù theo cách nào, yêu cầu cũng được ký bằng `secret`
  webhook của tổ chức.
</Note>

---

## Lược đồ phản hồi

Trả về một đối tượng JSON mô tả cấu hình tác nhân AI cho cuộc gọi này.
`prompt` và `voice` là bắt buộc; mọi trường khác là tùy chọn.

```json
{
  "prompt":  "You are a helpful booking assistant for Acme Restaurant.",
  "voice":   "john",
  "product": "spark",
  "background_track": null,
  "tools":   []
}
```

| Trường | Loại | Bắt buộc | Mô tả |
|-------|------|----------|-------------|
| `prompt` | string | có | System prompt điều khiển tác nhân AI |
| `voice` | string | có | ID giọng nói từ [`GET /v1/voices`](/api-reference/agents#voices), ví dụ: `john`. `voice_name` được chấp nhận làm bí danh. Giọng nói không xác định sẽ không vượt qua bước xác thực và bị từ chối cuộc gọi |
| `product` | string | không | Mặc định là `spark`. Giá trị được phép: `spark`, `bolt`, `storm-base`, `storm-base-with-ack`, `storm-extra`, `storm-extra-with-ack` |
| `thinking_level` | string | không | `minimal`, `base` (mặc định) hoặc `extra`. Bị ghi đè cho sản phẩm Storm: `storm-extra*` buộc dùng `extra`, các `storm-*` khác buộc dùng `base` |
| `audio_context_mode` | string | không | `full` (mặc định) hoặc `reduced` |
| `watchdog_enabled` | boolean | không | Bật giám sát cho cuộc gọi này. Mặc định là `false` |
| `additional_audio_context` | boolean \| null | không | Bao gồm vài lượt âm thanh gần nhất của người gọi thay vì chỉ lượt gần nhất, cải thiện việc sửa lỗi và thu thập dữ liệu có nhiều chữ cái/số, với mức tăng nhỏ về độ trễ và chi phí. Mặc định bật cho phiên gọi đến và tắt cho cuộc gọi điện thoại gọi đi; `null` giữ nguyên giá trị mặc định |
| `storm_feedback_mode` | string | không | `none`, `acknowledgement` (mặc định) hoặc `tick` |
| `language` | string | không | Viết tắt của `primary_language` |
| `primary_language` | string | không | Mã ngôn ngữ, được chuẩn hóa (mặc định là `en`). Mã không thể phân giải sẽ bị từ chối cuộc gọi |
| `has_additional_languages` | boolean | không | Mặc định là `false` |
| `additional_languages` | array of string | không | Các ngôn ngữ bổ sung mà tác nhân AI có thể chuyển sang |
| `native_voice_switching` | boolean | không | Mặc định là `false`. Khi cuộc gọi chuyển sang ngôn ngữ khác, đổi sang giọng nói bản ngữ của ngôn ngữ đó (khớp theo giới tính) thay vì giữ giọng nói đã cấu hình |
| `language_switching` | string \| null | không | `ask_first` (mặc định) hoặc `follow_caller`; `null` giống với `ask_first`. Cách tác nhân AI đa ngôn ngữ xử lý khi người kia nói một ngôn ngữ khác mà tác nhân AI hỗ trợ. `ask_first` chuyển ngay khi họ yêu cầu ngôn ngữ đó hoặc nói một câu hoàn chỉnh bằng ngôn ngữ đó, và hỏi có muốn tiếp tục bằng ngôn ngữ đó không sau một câu ngắn, như “có”. `follow_caller` chuyển sang bất kỳ ngôn ngữ nào đã cấu hình mà họ nói, kể cả chỉ một từ. Chỉ áp dụng khi đặt `additional_languages` |
| `background_track` | string \| null | không | ID âm thanh nền hoặc `null` |
| `acknowledgement_prompt_mode` | string | không | `auto` (mặc định) hoặc `manual` (sản phẩm Storm-with-ack) |
| `acknowledgement_prompt` | string | không | Được dùng khi `acknowledgement_prompt_mode="manual"` |
| `silence_interval_seconds` | integer \| null | không | 5–120. Số giây người gọi im lặng trước khi kiểm tra lại |
| `silence_max_checkins` | integer \| null | không | 1–10 |
| `silence_checkins_enabled` | boolean | không | Mặc định là `true` |
| `connect_tone_enabled` | boolean | không | Mặc định là `false` |
| `pronunciation_dicts` | boolean \| null | không | Mặc định là `false`; `null` giống với `false`. Khi là `true`, giọng nói Cartesia của cuộc gọi sử dụng từ điển phát âm của ThunderPhone cho ngôn ngữ của cuộc gọi, nếu có. Hoạt động cho cuộc gọi điện thoại và widget. Chỉ áp dụng cho giọng nói Cartesia riêng của ThunderPhone, không áp dụng cho giọng nói trên khóa API Cartesia của bạn. Không dùng với `agent_id`, nơi áp dụng các cài đặt của tác nhân AI đã lưu |
| `voicemail_action` | string | không | `prompt` (mặc định), `hangup` hoặc `message` |
| `voicemail_message` | string | không | Được dùng khi `voicemail_action="message"` |
| `agent_name` | string | không | Tên hiển thị được báo cáo trên dashboard và widget |
| `org_name` | string | không | Tên hiển thị của tổ chức cho hình tượng của tác nhân AI |
| `tools` | array | không | Schema công cụ hàm nội tuyến (xem [Công cụ hàm](/vi/tools/overview)) |
| `transfer_destinations` | array of object \| null | không | Tối đa 50 mục nhập `{name, number}` mà cuộc gọi này có thể chuyển đến, được xác thực như [số chuyển cuộc gọi](/api-reference/agents#transfer-numbers) của tác nhân AI. Tác nhân AI chuyển theo tên và chỉ đến các mục này; `[]` nghĩa là không chuyển cuộc gọi. Bỏ qua hoặc gửi `null` để cho phép tác nhân AI chuyển đến số mà nó chọn |
| `timezone` | string | không | Múi giờ IANA cho cuộc gọi này, chẳng hạn `America/Chicago`. Tác nhân AI nêu ngày và giờ theo múi giờ này, ưu tiên hơn múi giờ riêng của tác nhân AI. Cũng được phép đặt cùng `agent_id`. Các giá trị không phải múi giờ IANA sẽ bị bỏ qua. Xem [Đặt múi giờ cho từng cuộc gọi](/vi/guides/dynamic-call-config) |
| `call_id` | integer | không | Phản hồi tùy chọn của ID cuộc gọi trong yêu cầu; bị bỏ qua |

<Note>
  Các khóa cấp cao nhất không xác định sẽ bị **bỏ qua** âm thầm — tên
  trường gõ sai không khiến cấu hình bị từ chối, nhưng trường đó sẽ không được áp dụng. Thứ tự nói
  và `max_hold_seconds` không được chấp nhận ở đây; chúng chỉ
  có thể được cấu hình trên chính [Tác nhân AI](/api-reference/agents).
</Note>

Vì `prompt` và `voice` là bắt buộc, việc trả về `{}` hoặc bất kỳ phản hồi nào không vượt qua bước xác thực sẽ từ chối cuộc gọi: cuộc gọi điện thoại sẽ kết thúc như mô tả trong [Hết thời gian chờ và thử lại](#timeouts-and-retries), và yêu cầu phiên widget sẽ thất bại với mã `422`. Không có phương án dự phòng bằng tác nhân tĩnh trên luồng này (một số điện thoại hoặc khóa ở chế độ webhook không có tác nhân được gán).

---

## Giới hạn kích thước phản hồi

<Warning>
  Phản hồi cấu hình được giới hạn ở **5 MiB**. Nếu một trình xử lý
  trả về phản hồi lớn hơn, kể cả với trạng thái `2xx`,
  ThunderPhone sẽ báo rằng phản hồi vượt quá giới hạn và
  từ chối cuộc gọi hoặc phiên widget. Chỉ giữ lại trong phản hồi các trường
  cần thiết để thiết lập cuộc gọi; hãy lưu trữ dữ liệu lớn thông qua công cụ hàm hoặc
  một dịch vụ khác thay vì nhúng dữ liệu đó vào cấu hình.
</Warning>

---

## Ví dụ về trình xử lý

<CodeGroup>
```python Python (FastAPI)
import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

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

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

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

    event = json.loads(body)
    if event["type"] == "telephony.incoming":
        caller = event["data"]["from_number"]
        prompt = (
            "Greet the caller as a San Francisco local…"
            if caller.startswith("+1415")
            else "You are a friendly customer support agent…"
        )
        return {
            "prompt": prompt,
            "voice": "john",
            "product": "spark",
        }
    if event["type"] == "web.incoming":
        return {
            "prompt": "You are the website's helpful voice assistant…",
            "voice": "john",
            "product": "spark",
        }
    return {}
```

```javascript Node.js (Express)
import crypto from "node:crypto";
import express from "express";

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

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

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

    if (event.type === "telephony.incoming" || event.type === "web.incoming") {
      const caller = event.data.from_number || "web";
      const prompt = caller.startsWith("+1415")
        ? "Greet the caller as a San Francisco local…"
        : "You are a friendly customer support agent…";
      return res.json({
        prompt,
        voice: "john",
        product: "spark",
      });
    }
    res.json({});
  },
);
```
</CodeGroup>

---

## Phản hồi kèm công cụ hàm

Đính kèm công cụ để tác nhân AI có thể gọi API của bạn trong khi trò chuyện:

```json
{
  "prompt":  "You are a booking assistant. Use the available tools to help customers schedule appointments.",
  "voice":   "john",
  "product": "spark",
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "search_appointments",
        "description": "Find available appointment slots",
        "parameters": {
          "type": "object",
          "properties": {
            "date": { "type": "string", "description": "YYYY-MM-DD" },
            "service": { "type": "string" }
          },
          "required": ["date"]
        }
      },
      "endpoint": {
        "url": "https://api.example.com/appointments/search",
        "method": "POST",
        "headers": {
          "X-Api-Key": "your-key"
        }
      }
    }
  ]
}
```

<Tip>
  Yêu cầu đến điểm cuối công cụ được ký bằng **cùng secret webhook
  của tổ chức** đã ký lần trao đổi này. Xem
  [Công cụ hàm](/vi/tools/overview) để biết cấu trúc chính xác và định dạng
  yêu cầu đã ký.
</Tip>

---

## Bảng tham chiếu nhanh về các gói sản phẩm

| Sản phẩm | Độ trễ | Lập luận | Xác nhận |
|---------|---------|-----------|-----------------|
| `spark` | Thấp nhất | Cơ bản | — |
| `bolt` | Thấp | Cải thiện | — |
| `storm-base` | Trung bình | Mạnh | — |
| `storm-base-with-ack` | Trung bình | Mạnh | Tự động chèn lời đệm khi đang suy nghĩ |
| `storm-extra` | Cao hơn | Chuyên sâu | — |
| `storm-extra-with-ack` | Cao hơn | Chuyên sâu | Tự động chèn lời đệm khi đang suy nghĩ |

---

## Liên quan

<CardGroup cols={2}>
  <Card title="telephony.complete / web.complete" icon="phone-slash" href="/vi/webhooks/call-complete">
    Sự kiện kết thúc cuộc gọi không chặn.
  </Card>
  <Card title="Công cụ hàm" icon="screwdriver-wrench" href="/vi/tools/overview">
    Lược đồ JSON đầy đủ cho `tools[]` và hợp đồng endpoint đã ký.
  </Card>
  <Card title="Endpoint webhook" icon="bolt" href="/vi/webhooks/endpoints">
    Đăng ký nhiều URL cho `telephony.incoming` / `web.incoming`.
  </Card>
  <Card title="Cấu hình cuộc gọi động" icon="wand-magic-sparkles" href="/vi/guides/dynamic-call-config">
    Mẫu cho prompt, công cụ và kiểm thử A/B theo từng người gọi.
  </Card>
</CardGroup>
