Konfigurasi dinamis per panggilan
Pilih agen yang menjawab — atau tulis ulang Prompt dan pengaturannya — secara terpisah untuk setiap panggilan masuk, berdasarkan logika kustom 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 sudah masuk, pengujian Prompt A/B — beralihlah ke mode webhook dan biarkan server Anda yang menentukan.
Cara kerjanya
- Anda berlangganan ke event
telephony.incoming(telepon) atauweb.incoming(widget). Keduanya adalah webhook pemblokir: ThunderPhone menunggu hingga 10 detik untuk respons Anda sebelum melanjutkan panggilan. - 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.
- Jika Anda mengembalikan
{}, mengalami waktu habis, atau terjadi error, agen yang ditetapkan secara statis digunakan sebagai cadangan. Default yang aman.
1. Konfigurasikan tujuan webhook
Untuk nomor telepon, langganankan 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"]
}'Respons 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 disematkan:
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 melakukan POST ke URL ini pada setiap awal sesi.
2. Implementasikan handler
Tiga aturan praktis:
- Verifikasi signature pada setiap permintaan (lihat Verifikasi signature webhook). Jangan lewati ini saat pengembangan — lakukan dengan benar sekali lalu gunakan kembali.
- Respons dengan cepat. Sepuluh detik adalah batas maksimal, dan setiap detik adalah keheningan bagi penelepon. Lakukan pencarian database jika diperlukan, tetapi jangan panggil LLM downstream secara sinkron — jika Anda menginginkan pembuatan Prompt dinamis, hitung terlebih dahulu dan cache.
- Gunakan fallback dengan rapi. Setiap status tak terduga harus mengembalikan
{}agar agen yang ditetapkan secara statis menangani 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 sesuai persis dengan skema respons panggilan masuk. Kolom yang umum digunakan:
| Kolom | Tipe | Deskripsi |
|---|---|---|
prompt | string (wajib) | Prompt sistem untuk agen |
voice | string (wajib) | ID suara dari GET /v1/voices |
product | string | Default-nya adalah spark |
background_track | string | null | ID audio latar |
acknowledgement_prompt_mode | string | auto atau manual (khusus Storm-with-ack) |
acknowledgement_prompt | string | Wajib jika mode adalah manual |
tools | array | Skema function tool inline — lihat Function Tools |
Pola
Konteks pengguna yang sudah masuk
Dalam widget mode webhook, halaman pengunjung sudah mengetahui siapa
mereka. Panggil webhook Anda dengan parameter string kueri yang
diteruskan oleh SDK widget (?customer_id=123), lalu cari data pelanggan di sisi server.
Peluncuran Prompt A/B
Sebelum Anda membuatnya sendiri, perhatikan bahwa ThunderPhone memiliki fitur
Experiments bawaan
(/dashboard/experiments dan tab A/B di pembuat agen) yang
menentukan varian, membagi traffic, 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 di DB Anda sendiri, lalu korelasikan dengan nilai panggilan
yang telah selesai.
Perutean berbasis waktu
Jam kerja → agen "dukungan langsung"; di luar jam kerja → agen "menerima pesan".
Gunakan switch sederhana pada new Date().getUTCHours() di handler Anda.
Langkah berikutnya
Skema permintaan + respons yang lengkap, termasuk setiap kunci konfigurasi.
Pastikan HMAC benar sekali; gunakan kembali di mana saja.
Gabungkan perutean dinamis dengan alat per agen.
Percobaan ulang, urutan, batas waktu.