Open in
telephony.complete / web.complete
Webhook không chặn được gửi khi cuộc gọi kết thúc, kèm theo bản chép lời, URL bản ghi và chỉ số.
Sự kiện hoàn tất được kích hoạt sau khi mỗi cuộc gọi kết thúc — cuộc gọi điện thoại đến, cuộc gọi điện thoại đi, cuộc gọi web hoặc cuộc gọi thử nghiệm (phiên mic trong trình tạo). Sự kiện này không chặn: phản hồi bằng bất kỳ mã 2xx nào.
Sự kiện được gửi qua cả hai đường dẫn:
- Điểm cuối webhook nhận
telephony.complete(cuộc gọi điện thoại) hoặcweb.complete(cuộc gọi web và cuộc gọi thử mic trong trình tạo) với payload ổn định được ghi lại bên dưới,event_idcho mỗi lần gửi, thời gian chờ 30 giây và thử lại trong tối đa 24 giờ. - Webhook URL đơn lẻ cũ nhận một lần thử đồng bộ (thời gian chờ 10 giây, không thử lại) với payload hơi khác — xem Khác biệt về payload cũ.
Nội dung yêu cầu (lần gửi endpoint)
{
"data": {
"billable_minutes": 1.25,
"billing_total_cents": 8,
"call_id": 987654321,
"direction": "inbound",
"duration_seconds": 54,
"end_reason": "user_hangup",
"end_time": "2026-04-20T18:25:04.822Z",
"extracted_data": {
"status": "completed",
"fields": {
"customer_name": "Alex Morgan",
"appointment_date": "2026-04-23"
},
"evidence": {
"customer_name": {
"quote": "My name is Alex Morgan",
"speaker_role": "caller",
"turn_index": 4
},
"appointment_date": {
"quote": "April 23 works for me",
"speaker_role": "caller",
"turn_index": 7
}
},
"verification": "verified",
"field_reasons": {},
"schema_version": "92850758e231a3c95a..."
},
"from_number": "+14155550199",
"product": "spark",
"recording_stopped_at": null,
"recording_stopped_reason": null,
"recording_url": "https://storage.example.com/…",
"start_time": "2026-04-20T18:24:10.113Z",
"status": "completed",
"summary": "Alex Morgan called to book a follow-up visit and confirmed Thursday, April 23. The agent read the appointment back and Alex asked for a reminder text the day before.",
"to_number": "+15551234567",
"transcripts": [ /* see Transcript format */ ],
"transfer_number": null,
"unresolved_variables": ["campaign_owner"],
"variables": {"campaign_name": "Spring renewals"},
"voice": "john"
},
"event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"type": "telephony.complete"
}| Trường | Kiểu | Mô tả |
|---|---|---|
call_id | integer | Ổn định trên mọi sự kiện của cuộc gọi này |
agent_id | integer | null | Tác nhân đã xử lý cuộc gọi, khi có tác nhân được chỉ định |
agent_name | string | null | Tác nhân đã xử lý cuộc gọi, khi có tác nhân được chỉ định |
direction | string | inbound, outbound, web, test. Payload lịch sử có thể chứa các giá trị cũ mic hoặc widget |
from_number, to_number | string | E.164. from_number là giá trị nguyên văn "web" cho cuộc gọi web và cuộc gọi kiểm thử |
origin_domain | string | Chỉ web/kiểm thử — nguồn gốc trang lưu trữ tiện ích (trống đối với phiên mic) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Được suy ra từ thời gian bắt đầu/kết thúc |
status | string | completed hoặc failed |
end_reason | string | Xem bảng bên dưới |
product, voice | string | Cấu hình tác nhân có hiệu lực tại thời điểm cuộc gọi |
variables | object | Biến đầu vào được chụp lại tại thời điểm cuộc gọi bắt đầu |
unresolved_variables | array | Tên biến được tham chiếu bởi cấu hình cuộc gọi nhưng không được cung cấp khi cuộc gọi bắt đầu |
transfer_number | string | null | Được đặt khi cuộc gọi được chuyển tiếp |
recording_url | string | null | URL đã ký có thời hạn; hãy tải xuống ngay. null khi không có tệp ghi âm |
recording_stopped_at | timestamp | null | Thời điểm ghi âm bị dừng trước khi cuộc gọi kết thúc; nội dung sau thời điểm này không có trong bản ghi. null khi quá trình ghi âm kéo dài đến hết cuộc gọi hoặc cuộc gọi không được ghi âm |
recording_stopped_reason | string | null | Lý do ghi âm dừng sớm. warm_transfer: ghi âm dừng ngay trước khi một lần chuyển cuộc gọi có giám sát gọi đến đích và vẫn tắt ngay cả khi việc chuyển tiếp thất bại và người gọi quay lại tác nhân. Coi các giá trị khác là thông tin tham khảo; có thể bổ sung thêm. null khi recording_stopped_at là null |
billable_minutes | number | Số phút được tính phí, làm tròn đến một phần tư phút gần nhất (mỗi khoảng 15 giây, tối thiểu 0,25). Cuộc gọi chuyển thẳng đến hộp thư thoại vẫn báo cáo số phút đo thực tế tại đây, nhưng phí được giới hạn ở một phút theo mức phí của gói. |
billing_total_cents | integer | Cent USD |
transcripts | array | Mục bản chép lời theo từng lượt; có thể trống khi không có bản chép lời |
summary | string | null | Tóm tắt sau cuộc gọi bằng ngôn ngữ thông thường (hai đến bốn câu văn xuôi, tối đa 2.000 ký tự, được viết bằng ngôn ngữ của cuộc gọi: ngôn ngữ chính của tác nhân trừ khi cuộc gọi ghi đè ngôn ngữ đó) khi tác nhân đã bật Tạo tóm tắt cuộc gọi trong Dữ liệu cần thu thập ở trình tạo tác nhân (call_summary_enabled trên Tác nhân) và quá trình tạo thành công; nếu không thì là null, bao gồm các cuộc gọi quá ngắn để tóm tắt, cuộc gọi không có bản chép lời và tác nhân đã tắt ghi âm. Tóm tắt được tạo trong cùng bước sau cuộc gọi với quá trình trích xuất, vì vậy cài đặt Chờ dữ liệu trích xuất trước khi gửi webhook hoàn tất cuộc gọi (extraction_blocks_completion) quyết định liệu sự kiện này có chờ hay không: ở chế độ chờ, tóm tắt sẵn sàng trước khi sự kiện này được gửi (bản thân tóm tắt có tối đa 30 giây trong ngân sách 75 giây của bước); khi sự kiện không chờ (never hoặc chuyển tiếp theo except_transfers), trường này là null và tóm tắt không được gửi lại trong call.data_extracted, vì vậy hãy đọc sau từ summary trên đối tượng Cuộc gọi. Cũng có trên trang cuộc gọi trong bảng điều khiển |
extracted_data | object | null | Kết quả trích xuất có cấu trúc gồm status, fields, evidence, verification, field_reasons và schema_version. Mỗi trường không phải null có trích dẫn chính xác đã được kiểm tra cấu trúc (tối đa 1.000 ký tự), cùng vai trò người nói và chỉ mục lượt; các trích dẫn dài hơn do mô hình trả về sẽ bị từ chối thay vì cắt ngắn. Bằng chứng là null khi trường là null. verified nghĩa là mọi ứng viên đã nhận đúng một kết luận độc lập hợp lệ. unavailable cũng bao gồm đầu ra của trình xác minh bị sai định dạng hoặc không đầy đủ; các kết luận một phần hợp lệ vẫn được áp dụng, trong khi ứng viên không có một kết luận hợp lệ sẽ được đặt thành null. status là completed, failed, exhausted, skipped hoặc skipped_recording_disabled; null khi tác nhân không có trường trích xuất |
steps_trace | array | Chỉ có trên cuộc gọi mà tác nhân có bước hoặc chuyển giao. Thứ tự cuộc gọi đi qua các bước: mỗi mục có t_ms (mili giây kể từ khi cuộc gọi bắt đầu), agent (khóa của tác nhân, a cộng với ID của tác nhân), step (ID bước hoặc null khi chuyển giao đến tác nhân không có bước) và via (start, model, condition, on_enter hoặc handoff). Chỉ chứa ID và thời gian nên vẫn được lưu giữ ngay cả khi ghi âm bị tắt |
collected_values | object | Chỉ có trên cuộc gọi mà tác nhân có bước hoặc chuyển giao. Các giá trị tác nhân thu thập trong các bước, được khóa theo tên trường thu thập, cùng với <tool>_result cho kết quả on_enter_tool của mỗi bước. Bị bỏ qua khi nội dung cuộc gọi không được lưu giữ (ghi âm bị tắt) |
Lý do kết thúc
| Giá trị | Ý nghĩa |
|---|---|
user_hangup | Bên từ xa ngắt cuộc gọi trước |
ai_hangup | Tác nhân AI chủ động kết thúc cuộc gọi |
ai_transfer | Tác nhân AI đã chuyển cuộc gọi; transfer_number được thiết lập |
ai_warm_transfer | Tác nhân AI đã hoàn tất chuyển cuộc gọi có giám sát |
voicemail_hangup | Đã phát hiện thư thoại và cuộc gọi kết thúc theo voicemail_action của bạn |
max_duration | Cuộc gọi đạt giới hạn thời lượng tối đa: giới hạn của tác nhân, của chính cuộc gọi hoặc giới hạn 6 giờ của nền tảng. Cuộc gọi do tác nhân AI hoặc người gọi kết thúc sau cảnh báo giới hạn thời gian vẫn giữ ai_hangup hoặc user_hangup |
superseded | Phiên đã được thay thế bằng một phiên mới hơn |
inbound_config_webhook_failed | Cuộc gọi đến một số ở chế độ webhook: webhook cấu hình không trả về cấu hình có thể sử dụng, nên cuộc gọi đã kết thúc trước khi tác nhân trả lời. status là failed và cuộc gọi không bị tính phí |
unknown | Không thể xác định lý do kết thúc |
Định dạng transcript
Mỗi mục trong transcripts là một lượt hội thoại. Các vai trò là
user (lời nói của người gọi), model (lời nói của tác nhân AI và lệnh gọi công cụ),
tool (kết quả công cụ) và system (sự kiện cuộc gọi như chuyển đổi
ngôn ngữ).
[
{
"role": "user",
"content_type": "text/plain",
"content": "Hi, I'm calling about my appointment.",
"start_ms": 1200,
"end_ms": 4100,
"audio_url": "https://storage.example.com/…"
},
{
"role": "model",
"content_type": "text/plain",
"content": "Sure, what date works best?",
"start_ms": 4200,
"end_ms": 6100
},
{
"role": "model",
"content_type": "application/json",
"content": {
"tool_call": "search_appointments",
"arguments": { "date": "2026-04-21" }
}
},
{
"role": "tool",
"content_type": "application/json",
"content": {
"tool_name": "search_appointments",
"response": { "available_slots": ["9:00 AM", "2:00 PM"] }
}
}
]| Trường | Loại | Mô tả |
|---|---|---|
role | string | user, model, tool hoặc system |
content_type | string | text/plain cho lời nói; application/json cho lệnh gọi công cụ, kết quả công cụ và sự kiện hệ thống |
content | string | object | Văn bản lời nói hoặc đối tượng có cấu trúc hiển thị ở trên. Lệnh gọi công cụ: {"tool_call": name, "arguments": {…}}. Kết quả công cụ: {"tool_name": name, "response": {…}} |
start_ms, end_ms | integer | Độ lệch tính từ lúc cuộc gọi bắt đầu, tính bằng mili giây. Có mặt khi biết thời điểm âm thanh |
ttfa_ms | integer | Thời gian đến âm thanh đầu tiên cho lượt model, khi được đo |
audio_url, audio_urls | string / array | URL đã ký có thời hạn cho âm thanh của lượt, khi âm thanh được ghi theo từng lượt |
Để xem lịch sử lượt đầy đủ có cấu trúc (bao gồm dấu ngắt,
prompt xác nhận và vị trí thô), hãy dùng
GET /v1/calls/{call_id}/history.
Khác biệt của payload cũ
Envelope webhook cũ dùng một URL duy nhất là
{"type": "telephony.complete" | "web.complete", "data": {…}} và
không có event_id, đồng thời data của nó khác với payload endpoint:
Payload hoàn tất cũ cũng chứa agent_id và agent_name.
- Mảng lượt nằm trong
history, không phảitranscripts(cùng schema lượt như ở trên). - Tập trường là báo cáo thô khi cuộc gọi kết thúc và có thể bao gồm các trường nội bộ bổ sung ngoài bảng trên — hãy coi các trường không xác định là thông tin tham khảo.
- Cuộc gọi web (
direction: "web") bỏ quafrom_number/to_numbervà thêmorigin_domain. - Cuộc gọi kiểm tra mic trong Builder được báo cáo là
telephony.completetrên đường dẫn cũ (hệ thống endpoint ánh xạ chúng thànhweb.complete). - Điều phối chuyển cuộc gọi: khi một cuộc gọi kết thúc bằng chuyển cuộc gọi,
webhook cũ được gọi đồng bộ và có thể trả về
{"transfer_ready": false}để báo hiệu mục tiêu nhận chuyển cuộc gọi chưa sẵn sàng. Mọi phản hồi khác (hoặc không có webhook cũ) sẽ cho phép chuyển cuộc gọi tiếp tục. Các lần gửi endpoint không bao giờ được tham chiếu cho việc này.
Trình xử lý mẫu
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, signature: str) -> bool:
expected = hmac.new(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"] in ("telephony.complete", "web.complete"):
data = event["data"]
# Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
turns = data.get("transcripts") or data.get("history") or []
await persist_call_record(
call_id=data["call_id"],
turns=turns,
recording_url=data.get("recording_url"),
)
if data["end_reason"] in ("ai_transfer", "ai_warm_transfer"):
await notify_team(data.get("transfer_number"), data["call_id"])
return {"ok": True}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" }),
async (req, res) => {
if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (["telephony.complete", "web.complete"].includes(event.type)) {
const data = event.data;
// Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
const turns = data.transcripts ?? data.history ?? [];
await persistCallRecord({ ...data, turns });
if (["ai_transfer", "ai_warm_transfer"].includes(data.end_reason)) {
await notifyTeam(data.transfer_number, data.call_id);
}
}
res.json({ ok: true });
},
);Trường hợp sử dụng phổ biến
Lưu transcript và URL bản ghi của từng cuộc gọi cùng với hồ sơ khách hàng của bạn.
Truyền transcript đến pipeline để lập mô hình chủ đề, trích xuất tín hiệu CSAT hoặc theo dõi tỷ lệ chuyển máy.
Mở cuộc gọi trong công cụ QA để con người rà soát hoặc chạy qua mô hình đánh giá của riêng bạn.
Kích hoạt thông báo cho thành viên trong nhóm khi chuyển máy / thất bại.