Open in
telephony.complete / web.complete
Webhook non-pemblokiran yang 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 non-blocking: 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 dtk, dan percobaan ulang hingga 24 jam. - Webhook URL tunggal lama menerima satu percobaan sinkron (batas waktu 10 dtk, 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",
"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"
}| Field | Tipe | Deskripsi |
|---|---|---|
call_id | integer | Konsisten di setiap event untuk panggilan ini |
agent_id | integer | null | Agen yang menangani panggilan, jika ada yang ditetapkan |
agent_name | string | null | Agen yang menangani panggilan, jika ada yang ditetapkan |
direction | string | inbound, outbound, web, test. Payload historis dapat berisi nilai lama mic atau widget |
from_number, to_number | string | E.164. from_number secara literal bernilai "web" untuk panggilan web dan panggilan uji |
origin_domain | string | Hanya web/uji — asal halaman yang menghosting widget (kosong untuk sesi mic) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Dihitung dari waktu mulai/selesai |
status | string | completed atau failed |
end_reason | string | Lihat tabel di bawah |
product, voice | string | Konfigurasi agen yang berlaku pada waktu panggilan |
variables | object | Variabel input yang diambil sebagai snapshot saat panggilan dimulai |
unresolved_variables | array | Nama variabel yang dirujuk oleh konfigurasi panggilan tetapi tidak diberikan saat panggilan dimulai |
transfer_number | string | null | Diatur saat panggilan ditransfer |
recording_url | string | null | URL bertanda tangan yang kedaluwarsa; unduh segera. null saat tidak ada artefak rekaman yang tersedia |
recording_stopped_at | timestamp | null | Waktu rekaman dihentikan sebelum panggilan berakhir; tidak ada bagian setelah waktu ini dalam rekaman. null saat rekaman berjalan hingga akhir panggilan atau panggilan tidak direkam |
recording_stopped_reason | string | null | Alasan rekaman berhenti lebih awal. warm_transfer: rekaman berhenti tepat sebelum transfer hangat menghubungi tujuan, dan tetap nonaktif meskipun transfer gagal dan penelepon kembali ke agen. Perlakukan nilai lain sebagai informasi; nilai tambahan dapat ditambahkan. null saat recording_stopped_at bernilai null |
billable_minutes | number | Menit yang ditagihkan, dibulatkan ke seperempat menit terdekat (penambahan 15 detik, minimum 0.25). Panggilan langsung ke pesan suara tetap melaporkan menit terukur sebenarnya di sini, tetapi biayanya dibatasi hingga satu menit pada tarif paket. |
billing_total_cents | integer | Sen USD |
transcripts | array | Entri transkrip per giliran; dapat kosong saat transkrip tidak tersedia |
summary | string | null | Ringkasan pascapanggilan dalam bahasa sederhana (dua hingga empat kalimat prosa, maksimal 2.000 karakter, ditulis dalam bahasa panggilan: bahasa utama agen kecuali panggilan menggantinya) saat agen mengaktifkan Buat ringkasan panggilan di bawah Data yang dikumpulkan dalam pembuat agen (call_summary_enabled pada Agen) dan pembuatan berhasil; selain itu null, termasuk untuk panggilan yang terlalu singkat untuk diringkas, panggilan tanpa transkrip, dan agen dengan rekaman dinonaktifkan. Ringkasan dibuat pada langkah pascapanggilan yang sama dengan ekstraksi, sehingga pengaturan Tunggu data yang diekstrak sebelum mengirim webhook panggilan selesai (extraction_blocks_completion) menentukan apakah event ini menunggunya: dalam mode menunggu, ringkasan siap sebelum event ini dikirim (ringkasan itu sendiri mendapat waktu maksimal 30 detik dalam anggaran 75 detik langkah tersebut); saat event tidak menunggu (never, atau transfer di bawah except_transfers) field ini bernilai null dan ringkasan tidak dikirim ulang dalam call.data_extracted, jadi baca nanti dari summary pada objek Panggilan. Juga tersedia pada halaman panggilan di dashboard |
extracted_data | object | null | Hasil ekstraksi terstruktur dengan status, fields, evidence, verification, field_reasons, dan schema_version. Setiap field non-null memiliki kutipan persis yang diperiksa secara struktural (maksimal 1.000 karakter), beserta peran pembicara dan indeks gilirannya; kutipan yang dikembalikan Model dan lebih panjang ditolak alih-alih dipotong. Evidence bernilai null saat field bernilai null. verified berarti setiap kandidat menerima tepat satu putusan independen yang valid. unavailable juga mencakup output pemverifikasi yang tidak valid atau parsial; putusan parsial yang valid tetap diterapkan, sedangkan kandidat tanpa satu putusan valid diatur menjadi null. status bernilai completed, failed, exhausted, skipped, atau skipped_recording_disabled; null saat agen tidak memiliki field ekstraksi |
steps_trace | array | Hanya pada panggilan yang agennya memiliki langkah atau handoff. Urutan panggilan melewatinya: setiap entri memiliki t_ms (milidetik sejak panggilan dimulai), agent (kunci agen, a ditambah ID-nya), step (ID langkah, atau null pada handoff ke agen tanpa langkah), dan via (start, model, condition, on_enter, atau handoff). Hanya menyimpan ID dan waktu, sehingga tetap disimpan meskipun rekaman dinonaktifkan |
collected_values | object | Hanya pada panggilan yang agennya memiliki langkah atau handoff. Nilai yang dikumpulkan agen dalam langkahnya, dengan kunci nama field collect, ditambah <tool>_result untuk hasil on_enter_tool setiap langkah. Dihilangkan saat konten panggilan tidak disimpan (rekaman dinonaktifkan) |
Alasan berakhir
| Nilai | Arti |
|---|---|
user_hangup | Pihak jarak jauh menutup panggilan terlebih dahulu |
ai_hangup | AI sengaja mengakhiri panggilan |
ai_transfer | AI mentransfer panggilan; transfer_number ditetapkan |
ai_warm_transfer | AI menyelesaikan transfer hangat (dengan pendampingan) |
voicemail_hangup | Pesan suara terdeteksi dan panggilan diakhiri sesuai voicemail_action Anda |
max_duration | Panggilan mencapai batas durasi maksimum: batas agen atau batas panggilan itu sendiri, atau batas Platform 6 jam. Panggilan yang diakhiri oleh AI atau penelepon setelah peringatan batas waktu tetap menggunakan ai_hangup atau user_hangup |
superseded | Sesi digantikan oleh sesi yang lebih baru |
inbound_config_webhook_failed | Panggilan masuk ke nomor dalam mode webhook: webhook konfigurasi tidak mengembalikan konfigurasi yang dapat digunakan, sehingga panggilan diakhiri sebelum agen menjawab. status adalah failed dan panggilan tidak ditagihkan |
unknown | Alasan pengakhiran tidak dapat ditentukan |
Format transkrip
Setiap entri dalam transcripts adalah satu giliran percakapan. Perannya adalah
user (ucapan penelepon), model (ucapan agen dan panggilan alat),
tool (hasil alat), dan system (peristiwa panggilan seperti perpindahan
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"] }
}
}
]| Kolom | Tipe | Deskripsi |
|---|---|---|
role | string | user, model, tool, atau system |
content_type | string | text/plain untuk ucapan; application/json untuk panggilan alat, hasil alat, dan peristiwa sistem |
content | string | object | Teks ucapan, atau objek terstruktur yang ditampilkan di atas. Panggilan alat: {"tool_call": name, "arguments": {…}}. Hasil alat: {"tool_name": name, "response": {…}} |
start_ms, end_ms | integer | Offset dari awal panggilan, dalam ms. Ada saat pengaturan 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 tersebut, jika direkam per giliran |
Untuk riwayat giliran yang sepenuhnya terstruktur (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:
Payload penyelesaian lama juga memuat agent_id dan agent_name.
- Array giliran berada di bawah
history, bukantranscripts(dengan skema giliran yang sama seperti di atas). - Kumpulan kolomnya adalah laporan mentah akhir panggilan dan dapat mencakup kolom internal tambahan di luar tabel di atas — perlakukan kolom yang tidak dikenal sebagai informasi.
- Panggilan web (
direction: "web") tidak menyertakanfrom_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 serah terima belum siap. Respons lainnya (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 + URL rekaman setiap panggilan bersama catatan pelanggan Anda.
Alirkan 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.