ThunderPhone 2.0 kini resmi hadir.Gunakan langsung secara mandiri, mulai 2¢/menit.Baca pengumumannya

Webhooks

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:

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"
}
FieldTipeDeskripsi
call_idintegerKonsisten di setiap event untuk panggilan ini
agent_idinteger | nullAgen yang menangani panggilan, jika ada yang ditetapkan
agent_namestring | nullAgen yang menangani panggilan, jika ada yang ditetapkan
directionstringinbound, outbound, web, test. Payload historis dapat berisi nilai lama mic atau widget
from_number, to_numberstringE.164. from_number secara literal bernilai "web" untuk panggilan web dan panggilan uji
origin_domainstringHanya web/uji — asal halaman yang menghosting widget (kosong untuk sesi mic)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullDihitung dari waktu mulai/selesai
statusstringcompleted atau failed
end_reasonstringLihat tabel di bawah
product, voicestringKonfigurasi agen yang berlaku pada waktu panggilan
variablesobjectVariabel input yang diambil sebagai snapshot saat panggilan dimulai
unresolved_variablesarrayNama variabel yang dirujuk oleh konfigurasi panggilan tetapi tidak diberikan saat panggilan dimulai
transfer_numberstring | nullDiatur saat panggilan ditransfer
recording_urlstring | nullURL bertanda tangan yang kedaluwarsa; unduh segera. null saat tidak ada artefak rekaman yang tersedia
recording_stopped_attimestamp | nullWaktu 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_reasonstring | nullAlasan 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_minutesnumberMenit 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_centsintegerSen USD
transcriptsarrayEntri transkrip per giliran; dapat kosong saat transkrip tidak tersedia
summarystring | nullRingkasan 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_dataobject | nullHasil 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_tracearrayHanya 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_valuesobjectHanya 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

NilaiArti
user_hangupPihak jarak jauh menutup panggilan terlebih dahulu
ai_hangupAI sengaja mengakhiri panggilan
ai_transferAI mentransfer panggilan; transfer_number ditetapkan
ai_warm_transferAI menyelesaikan transfer hangat (dengan pendampingan)
voicemail_hangupPesan suara terdeteksi dan panggilan diakhiri sesuai voicemail_action Anda
max_durationPanggilan 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
supersededSesi digantikan oleh sesi yang lebih baru
inbound_config_webhook_failedPanggilan 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
unknownAlasan 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"] }
    }
  }
]
KolomTipeDeskripsi
rolestringuser, model, tool, atau system
content_typestringtext/plain untuk ucapan; application/json untuk panggilan alat, hasil alat, dan peristiwa sistem
contentstring | objectTeks ucapan, atau objek terstruktur yang ditampilkan di atas. Panggilan alat: {"tool_call": name, "arguments": {…}}. Hasil alat: {"tool_name": name, "response": {…}}
start_ms, end_msintegerOffset dari awal panggilan, dalam ms. Ada saat pengaturan waktu audio diketahui
ttfa_msintegerWaktu hingga audio pertama untuk giliran model, jika diukur
audio_url, audio_urlsstring / arrayURL 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, bukan transcripts (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 menyertakan from_number / to_number dan menambahkan origin_domain.
  • Panggilan uji mikrofon Builder dilaporkan sebagai telephony.complete pada jalur lama (sistem endpoint memetakannya ke web.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

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

Kasus penggunaan umum

Integrasi CRM

Simpan transkrip + URL rekaman setiap panggilan bersama catatan pelanggan Anda.

Analitik

Alirkan transkrip ke pipeline untuk pemodelan topik, ekstraksi sinyal CSAT, atau pemantauan tingkat transfer.

Tinjauan kualitas

Buka panggilan di alat QA untuk ditinjau manusia, atau jalankan melalui model evaluasi Anda sendiri.

Notifikasi

Picu rekan tim manusia saat transfer / kegagalan.


Terkait