ThunderPhone 2.0 kini resmi hadir.Layanan mandiri, mulai dari 2¢/menit.Baca pengumumannya

Webhooks

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) atau web.complete (panggilan web dan panggilan uji mikrofon builder) dengan payload stabil yang didokumentasikan di bawah, event_id per 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"
}
KolomTipeDeskripsi
call_idintegerStabil di setiap peristiwa untuk panggilan ini
directionstringinbound, outbound, web, test. Payload historis mungkin berisi nilai lama mic atau widget
from_number, to_numberstringE.164. from_number secara literal adalah "web" untuk panggilan web dan panggilan uji
origin_domainstringHanya web/uji — origin halaman yang menghosting widget (kosong untuk sesi mikrofon)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullDiturunkan dari waktu mulai/akhir
statusstringcompleted atau failed
end_reasonstringLihat tabel di bawah
product, voicestringKonfigurasi agen yang berlaku saat panggilan berlangsung
transfer_numberstring | nullDitentukan saat panggilan dialihkan
recording_urlstring | nullURL bertanda tangan yang kedaluwarsa; segera unduh. null ketika tidak ada artefak rekaman yang tersedia
billable_minutesnumberMenit 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_centsintegerSen USD
transcriptsarrayEntri transkrip per giliran; dapat kosong ketika transkrip tidak tersedia

Alasan berakhir

NilaiMakna
user_hangupPihak jarak jauh menutup panggilan terlebih dahulu
ai_hangupAI mengakhiri panggilan dengan sengaja
ai_transferAI mengalihkan panggilan; transfer_number ditentukan
ai_warm_transferAI menyelesaikan pengalihan hangat (dengan pendampingan)
voicemail_hangupPesan suara terdeteksi dan panggilan berakhir sesuai voicemail_action Anda
max_durationPanggilan mencapai batas durasi maksimum
supersededSesi digantikan oleh sesi yang lebih baru
unknownAlasan 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"] }
    }
  }
]
BidangTipeDeskripsi
rolestringuser, model, tool, atau system
content_typestringtext/plain untuk ucapan; application/json untuk pemanggilan alat, hasil alat, dan peristiwa sistem
contentstring | objectTeks ucapan, atau objek terstruktur yang ditampilkan di atas. Pemanggilan alat: {"tool_call": name, "arguments": {…}}. Hasil alat: {"tool_name": name, "response": {…}}
start_ms, end_msintegerOffset dari awal panggilan, dalam ms. Ada saat 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, 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, bukan transcripts (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") menghilangkan 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 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

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 dan URL rekaman setiap panggilan bersama catatan pelanggan Anda.

Analitik

Streaming 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