Open in
Konfigurasi dinamis per panggilan
Pilih agen yang menjawab — atau tulis ulang Prompt dan pengaturannya — secara terpisah untuk setiap panggilan masuk, berdasarkan logika khusus dalam webhook yang Anda kendalikan.
Secara default, setiap nomor telepon dan kunci yang dapat dipublikasikan memiliki agen statis yang ditetapkan. Saat Anda memerlukan penyesuaian per-penelepon atau per-pengunjung — perutean VIP, konteks pengguna yang masuk, pengujian Prompt A/B — beralihlah ke mode webhook dan biarkan server Anda yang menentukan.
Cara kerjanya
- Anda berlangganan event
telephony.incoming(telepon) atauweb.incoming(widget). Keduanya adalah webhook yang memblokir: ThunderPhone menunggu respons Anda sebelum melanjutkan panggilan, hingga 13 detik untuk panggilan telepon (termasuk satu kali percobaan ulang) dan 10 detik untuk sesi widget. - ThunderPhone mengirimkan
{call_id, from_number, to_number}kepada Anda (sesi widget membawa kolom khusus widget, bukan nomor — lihat skema permintaan). - Server Anda merespons dengan konfigurasi agen (Prompt, suara, produk, alat). ThunderPhone menggunakan konfigurasi tersebut untuk panggilan.
- Tidak ada agen cadangan. Jika Anda mengembalikan
{}atau konfigurasi yang tidak valid, atau permintaan tetap gagal setelah percobaan ulang, panggilan telepon langsung diakhiri dan dicatat denganend_reasoninbound_config_webhook_failed; permintaan sesi widget gagal.
1. Konfigurasikan tujuan webhook
Untuk nomor telepon, berlangganankan endpoint Anda ke telephony.incoming:
curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Prod call-incoming",
"url": "https://example.com/thunderphone/incoming",
"events": ["telephony.incoming"]
}'Responsnya menyertakan secret sekali pakai — simpan; Anda akan menggunakannya
untuk verifikasi tanda tangan.
Untuk sesi widget, buat kunci yang dapat dipublikasikan dalam mode="webhook"
dengan URL endpoint Anda tertanam:
curl -X POST https://api.thunderphone.com/v1/publishable-key \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Dynamic widget",
"mode": "webhook",
"webhook_url": "https://example.com/thunderphone/widget-incoming",
"allowed_domains": ["example.com"]
}'Widget akan mengirim POST ke URL ini pada setiap awal sesi.
2. Implementasikan handler
Tiga aturan praktis:
- Verifikasi tanda tangan pada setiap permintaan (lihat Verifikasi tanda tangan webhook). Jangan lewati ini saat pengembangan — lakukan dengan benar sekali lalu gunakan kembali.
- Respons dengan cepat. Upaya pertama panggilan telepon diputus pada 10 detik dan seluruh pertukaran pada 13 detik, dan setiap detik adalah keheningan bagi penelepon. Lakukan pencarian database jika diperlukan, tetapi jangan panggil LLM hilir secara sinkron — jika Anda ingin menghasilkan prompt secara dinamis, prahitung dan simpan dalam cache.
- Lakukan fallback dengan rapi. Untuk keadaan tak terduga apa pun, berikan
promptdanvoicedefault (atauagent_idyang tersimpan). Mengembalikan{}atau status error akan mengakhiri panggilan.
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, sig: str) -> bool:
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig or "")
@app.post("/thunderphone/incoming")
async def incoming(request: Request):
body = await request.body()
if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
raise HTTPException(401)
event = json.loads(body)
if event["type"] not in ("telephony.incoming", "web.incoming"):
return {} # fall back to default
caller = event["data"]["from_number"]
# Cheap DB lookup: is this a known VIP?
customer = lookup_customer(caller)
if customer and customer.tier == "vip":
return {
"prompt": f"You are a VIP concierge for {customer.name}. Be proactive…",
"voice": "john",
"product": "storm-base",
}
return {} # default agent handles non-VIPs
def lookup_customer(phone: str):
# ... your CRM integration ...
passimport crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;
function verify(body, sig) {
const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
return sig &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
app.post(
"/thunderphone/incoming",
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"));
const IMPORTANT_TYPES = new Set([
"telephony.incoming",
"web.incoming",
]);
if (!IMPORTANT_TYPES.has(event.type)) return res.json({});
const customer = await lookupCustomer(event.data.from_number);
if (customer?.tier === "vip") {
return res.json({
prompt: `You are a VIP concierge for ${customer.name}. Be proactive…`,
voice: "john",
product: "storm-base",
});
}
res.json({}); // fall back to default agent
},
);3. Skema respons
Isi respons sama persis dengan skema respons panggilan masuk. Kolom yang umum digunakan:
| Bidang | Tipe | Deskripsi |
|---|---|---|
prompt | string (wajib) | Prompt sistem untuk agen |
voice | string (wajib) | ID suara dari GET /v1/voices |
product | string | Defaultnya adalah spark. Produk eksperimental hanya tersedia untuk organisasi dalam daftar izin. |
background_track | string | null | ID audio ambient |
acknowledgement_prompt_mode | string | auto atau manual (khusus Storm dengan ack) |
acknowledgement_prompt | string | Wajib saat mode adalah manual |
language_switching | string | Untuk agen multibahasa, ask_first atau follow_caller |
tools | array | Skema alat fungsi inline — lihat Alat Fungsi |
Pertahankan agen tersimpan dan berikan variabel
Kembalikan {"agent_id": 12, "variables": {"name": "Ada"}} untuk menggunakan agen tersimpan organisasi tersebut
dengan data per panggilan. Prompt-nya dapat berisi {{name}} atau
{{name|Friend}}. Variabel webhook digabungkan di atas variabel tingkat permintaan; null
menggunakan nilai default placeholder, atau teks kosong jika tidak ada yang disediakan. Nilai akhir
dan nama yang belum terselesaikan muncul dalam detail panggilan dan webhook penyelesaian.
Respons agen tersimpan hanya menerima agent_id, variables, dan timezone. Jika prompt
ada, respons menggunakan konfigurasi inline dan mengabaikan agent_id (termasuk
metadata null atau bukan bilangan bulat); Prompt inline tetap harus valid. Respons konfigurasi
inline juga dapat menyertakan variables. Respons agen tersimpan menggunakan pembagian A/B agen
yang diterapkan pada panggilan telepon dan widget, lalu merender variabel. Lihat variabel panggilan
untuk batas dan dukungan API sesi. Konfigurasi pemblokiran berasal dari
URL nomor telepon/organisasi lama atau kunci widget mode webhook; event masuk sistem endpoint
hanya berupa notifikasi.
Tetapkan zona waktu per panggilan
Satu agen dapat menjawab kontak di zona waktu yang berbeda. Tambahkan timezone,
nama IANA, ke respons agen tersimpan atau inline dan agen akan menyebutkan tanggal
dan waktu serta menawarkan waktu dalam zona tersebut untuk panggilan ini, mendahului
zona waktu agen sendiri:
{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}Kirimkan zona, bukan stempel waktu, dalam variables: jam agen terus berjalan
selama panggilan, dan waktu yang Anda kirim akan kedaluwarsa. Nilai yang bukan
zona IANA akan diabaikan dan panggilan terhubung dalam zona agen.
Pola
Konteks pengguna yang sudah Masuk
Dalam widget mode webhook, halaman pengunjung sudah mengetahui identitas
mereka. Panggil webhook Anda dengan parameter query string yang diteruskan oleh SDK
widget (?customer_id=123) dan cari data pelanggan di sisi server.
Peluncuran Prompt A/B
Sebelum membuatnya sendiri, ketahui bahwa ThunderPhone memiliki fitur asli
Experiments
(/dashboard/experiments dan tab A/B di pembuat agen) yang
menentukan varian, membagi trafik, dan membandingkan hasil per varian —
tanpa webhook.
Jika Anda tetap memerlukan kontrol di sisi webhook: hash call_id → bucket;
sajikan prompt A untuk 0..49 dan prompt B untuk 50..99. Catat bucket yang
Anda pilih dalam DB Anda sendiri dan kemudian korelasikan dengan nilai panggilan
yang telah selesai.
Perutean berbasis waktu
Jam kerja → agen "dukungan langsung"; di luar jam kerja → agen "mencatat pesan".
Buat percabangan sederhana berdasarkan new Date().getUTCHours() dalam handler Anda.
Langkah berikutnya
Skema permintaan + respons lengkap, termasuk setiap kunci konfigurasi.
Buat HMAC dengan benar sekali; gunakan kembali di mana saja.
Gabungkan perutean dinamis dengan tool per agen.
Percobaan ulang, urutan, waktu habis.