telephony.complete / web.complete
Webhook non-pemblokiran dikirimkan saat panggilan berakhir, dengan transkrip, URL rekaman, dan metrik.
Peristiwa completion dipicu setelah setiap panggilan berakhir — telepon masuk, telepon keluar, panggilan web, atau panggilan uji (sesi mikrofon builder). Peristiwa ini tidak memblokir: respons dengan 2xx apa pun.
Peristiwa dikirimkan melalui kedua jalur:
- Endpoint webhook menerima
telephony.complete(panggilan telepon) atauweb.complete(panggilan web dan panggilan uji mikrofon builder) dengan payload stabil yang didokumentasikan di bawah,event_idper pengiriman, batas waktu 30 d, dan percobaan ulang hingga 24 j. - Webhook satu-URL lama menerima satu percobaan sinkron (batas waktu 10 d, tanpa percobaan ulang) dengan payload yang sedikit berbeda — lihat Perbedaan payload lama.
Payload permintaan (pengiriman 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",
"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"
}| Kolom | Tipe | Deskripsi |
|---|---|---|
call_id | integer | Stabil di setiap peristiwa untuk panggilan ini |
direction | string | inbound, outbound, web, test. Payload historis mungkin berisi nilai lama mic atau widget |
from_number, to_number | string | E.164. from_number secara literal adalah "web" untuk panggilan web dan panggilan uji |
origin_domain | string | Hanya web/uji — origin halaman yang menghosting widget (kosong untuk sesi mikrofon) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Diturunkan dari waktu mulai/akhir |
status | string | completed atau failed |
end_reason | string | Lihat tabel di bawah |
product, voice | string | Konfigurasi agen yang berlaku saat panggilan berlangsung |
transfer_number | string | null | Ditentukan saat panggilan dialihkan |
recording_url | string | null | URL bertanda tangan yang kedaluwarsa; segera unduh. null ketika tidak ada artefak rekaman yang tersedia |
billable_minutes | number | Menit yang ditagihkan, dibulatkan ke seperempat menit terdekat (kenaikan 15 detik, minimum 0.25). Panggilan yang langsung ke pesan suara tetap melaporkan menit terukur aktualnya di sini, tetapi biayanya dibatasi hingga satu menit dengan tarif paket. |
billing_total_cents | integer | Sen USD |
transcripts | array | Entri transkrip per giliran; dapat kosong ketika transkrip tidak tersedia |
Alasan berakhir
| Nilai | Makna |
|---|---|
user_hangup | Pihak jarak jauh menutup panggilan terlebih dahulu |
ai_hangup | AI mengakhiri panggilan dengan sengaja |
ai_transfer | AI mengalihkan panggilan; transfer_number ditentukan |
ai_warm_transfer | AI menyelesaikan pengalihan hangat (dengan pendampingan) |
voicemail_hangup | Pesan suara terdeteksi dan panggilan berakhir sesuai voicemail_action Anda |
max_duration | Panggilan mencapai batas durasi maksimum |
superseded | Sesi digantikan oleh sesi yang lebih baru |
unknown | Alasan berakhir tidak dapat ditentukan |
Format transkrip
Setiap entri dalam transcripts adalah satu giliran percakapan. Perannya adalah
user (ucapan penelepon), model (ucapan agen dan pemanggilan alat),
tool (hasil alat), dan system (peristiwa panggilan seperti pergantian
bahasa).
[
{
"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"] }
}
}
]| Bidang | Tipe | Deskripsi |
|---|---|---|
role | string | user, model, tool, atau system |
content_type | string | text/plain untuk ucapan; application/json untuk pemanggilan alat, hasil alat, dan peristiwa sistem |
content | string | object | Teks ucapan, atau objek terstruktur yang ditampilkan di atas. Pemanggilan alat: {"tool_call": name, "arguments": {…}}. Hasil alat: {"tool_name": name, "response": {…}} |
start_ms, end_ms | integer | Offset dari awal panggilan, dalam ms. Ada saat waktu audio diketahui |
ttfa_ms | integer | Waktu hingga audio pertama untuk giliran model, jika diukur |
audio_url, audio_urls | string / array | URL bertanda tangan yang kedaluwarsa untuk audio giliran, jika direkam per giliran |
Untuk riwayat giliran yang terstruktur sepenuhnya (dengan penanda interupsi,
prompt pengakuan, dan posisi mentah), gunakan
GET /v1/calls/{call_id}/history.
Perbedaan payload lama
Envelope webhook lama dengan satu URL adalah
{"type": "telephony.complete" | "web.complete", "data": {…}} tanpa
event_id, dan data-nya berbeda dari payload endpoint:
- Array giliran berada di bawah
history, bukantranscripts(skema giliran sama seperti di atas). - Kumpulan bidang adalah laporan mentah akhir panggilan dan dapat menyertakan bidang internal tambahan di luar tabel di atas — perlakukan bidang yang tidak dikenal sebagai informasi.
- Panggilan web (
direction: "web") menghilangkanfrom_number/to_numberdan menambahkanorigin_domain. - Panggilan uji mikrofon Builder dilaporkan sebagai
telephony.completepada jalur lama (sistem endpoint memetakannya keweb.complete). - Koordinasi transfer: saat panggilan berakhir dalam transfer, webhook lama
dipanggil secara sinkron dan dapat menjawab
{"transfer_ready": false}untuk menandakan target pengalihan belum siap. Respons lain apa pun (atau tidak ada webhook lama) memungkinkan transfer dilanjutkan. Pengiriman endpoint tidak pernah digunakan untuk hal ini.
Contoh 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 });
},
);Kasus penggunaan umum
Simpan transkrip dan URL rekaman setiap panggilan bersama catatan pelanggan Anda.
Streaming transkrip ke pipeline untuk pemodelan topik, ekstraksi sinyal CSAT, atau pemantauan tingkat transfer.
Buka panggilan di alat QA untuk ditinjau manusia, atau jalankan melalui model evaluasi Anda sendiri.
Picu rekan tim manusia saat transfer / kegagalan.