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à các 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 — điện thoại đến, điện thoại đi, cuộc gọi web hoặc cuộc gọi kiểm thử (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 kiểm 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 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 của payload cũ.
Payload yêu cầu (gửi đến điểm cuối)
{
"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",
"from_number": "+14155550199",
"product": "spark",
"recording_url": "https://storage.example.com/…",
"start_time": "2026-04-20T18:24:10.113Z",
"status": "completed",
"to_number": "+15551234567",
"transcripts": [ /* see Transcript format */ ],
"transfer_number": null,
"voice": "john"
},
"event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"type": "telephony.complete"
}| Trường | Loại | Mô tả |
|---|---|---|
call_id | integer | Ổn định trong mọi sự kiện của cuộc gọi này |
direction | string | inbound, outbound, web, test. Payload trước đây 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ị "web" theo nghĩa đen đối với 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ữ widget (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 gọi |
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 sớm. null khi không có tệp ghi âm |
billable_minutes | number | Số phút được tính phí, làm tròn đến mỗi một phần tư phút gần nhất (mỗi 15 giây, tối thiểu 0.25). Cuộc gọi đi thẳng vào hộp thư thoại vẫn báo cáo số phút thực tế được đo tại đây, nhưng phí được giới hạn ở một phút theo mức giá gói. |
billing_total_cents | integer | Cent USD |
transcripts | array | Các 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 |
Lý do kết thúc
| Giá trị | Ý nghĩa |
|---|---|
user_hangup | Bên ở đầu kia gác máy 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 tiếp cuộc gọi; transfer_number được đặt |
ai_warm_transfer | Tác nhân AI đã hoàn tất chuyển tiếp có tham gia |
voicemail_hangup | Đã phát hiện hộp 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 |
superseded | Phiên đã được thay thế bằng một phiên mới hơn |
unknown | Không thể xác định lý do kết thúc |
Định dạng bản chép lời
Mỗi mục trong transcripts là một lượt hội thoại. Vai trò gồm
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
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ừ khi cuộc gọi bắt đầu, đơn vị ms. Có mặt khi biết thời gian âm thanh |
ttfa_ms | integer | Thời gian đến âm thanh đầu tiên cho một lượt model, khi được đo |
audio_url, audio_urls | string / array | URL có chữ ký hết hạn cho âm thanh của lượt, khi đượ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,
ack-prompt và vị trí thô), sử dụng
GET /v1/calls/{call_id}/history.
Khác biệt của payload cũ
Envelope webhook cũ dùng một URL là
{"type": "telephony.complete" | "web.complete", "data": {…}} và
không có event_id, còn data của nó khác với payload endpoint:
- Mảng lượt nằm trong
history, không phảitranscripts(cùng schema lượt như bên trên). - Tập trường là báo cáo thô khi kết thúc cuộc gọi và có thể bao gồm các trường nội bộ bổ sung ngoài bảng trên — 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 việc 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 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ẽ để việc 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.
Ví dụ về handler
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 });
},
);Các trường hợp sử dụng phổ biến
Lưu transcript và URL bản ghi của mỗi cuộc gọi cùng với hồ sơ khách hàng của bạn.
Truyền transcript đến một pipeline để mô hình hóa chủ đề, trích xuất tín hiệu CSAT hoặc theo dõi tỷ lệ chuyển cuộc gọi.
Mở cuộc gọi trong công cụ QA để con người đánh giá hoặc chạy qua mô hình đánh giá riêng của bạn.
Kích hoạt thông báo cho một thành viên trong nhóm khi chuyển cuộc gọi / xảy ra lỗi.