Open in
telephony.incoming / web.incoming
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ũ
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). 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
để biết quy trình toàn diện.
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,408hay429, 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
4xxnào khác, phản hồi vượt quá giới hạn kích thước, hoặc phản hồi2xxcó 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 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.
Payload yêu cầu
Đối với cuộc gọi điện thoại (telephony.incoming):
{
"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:
{
"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. 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.
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.
{
"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, 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) |
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 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 |
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 |
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, 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
Ví dụ về trình xử lý
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 {}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({});
},
);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:
{
"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"
}
}
}
]
}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ĩ |