ThunderPhone 2.0 đã chính thức ra mắt.Tự thiết lập, từ 2 xu/phút.Xem thông báo ra mắt

Webhooks

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:

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ườngLoạiMô tả
call_idintegerỔn định trong mọi sự kiện của cuộc gọi này
directionstringinbound, outbound, web, test. Payload trước đây có thể chứa các giá trị cũ mic hoặc widget
from_number, to_numberstringE.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_domainstringChỉ web/kiểm thử — nguồn gốc trang đã lưu trữ widget (trống đối với phiên mic)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullĐược suy ra từ thời gian bắt đầu/kết thúc
statusstringcompleted hoặc failed
end_reasonstringXem bảng bên dưới
product, voicestringCấu hình tác nhân có hiệu lực tại thời điểm gọi
transfer_numberstring | nullĐược đặt khi cuộc gọi được chuyển tiếp
recording_urlstring | nullURL đã ký có thời hạn; hãy tải xuống sớm. null khi không có tệp ghi âm
billable_minutesnumberSố 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_centsintegerCent USD
transcriptsarrayCá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_hangupBên ở đầu kia gác máy trước
ai_hangupTác nhân AI chủ động kết thúc cuộc gọi
ai_transferTác nhân AI đã chuyển tiếp cuộc gọi; transfer_number được đặt
ai_warm_transferTá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_durationCuộc gọi đạt giới hạn thời lượng tối đa
supersededPhiên đã được thay thế bằng một phiên mới hơn
unknownKhô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 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ườngLoạiMô tả
rolestringuser, model, tool hoặc system
content_typestringtext/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
contentstring | objectVă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_msintegerĐộ 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_msintegerThời gian đến âm thanh đầu tiên cho một lượt model, khi được đo
audio_url, audio_urlsstring / arrayURL 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": {…}}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ải transcripts (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ỏ qua from_number / to_number và thêm origin_domain.
  • Cuộc gọi kiểm tra mic trong Builder được báo cáo là telephony.complete trên đường dẫn cũ (hệ thống endpoint ánh xạ chúng thành web.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

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, 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}
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" }),
  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

Tích hợp CRM

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.

Phân tích

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.

Đánh giá chất lượng

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.

Thông báo

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.


Liên quan