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

Developer cookbook

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

  1. Anda berlangganan ke event telephony.incoming (telepon) atau web.incoming (widget). Keduanya adalah webhook pemblokir: ThunderPhone menunggu hingga 10 detik untuk respons Anda sebelum melanjutkan panggilan.
  2. ThunderPhone mengirimkan {call_id, from_number, to_number} kepada Anda (sesi widget membawa kolom khusus widget, bukan nomor — lihat skema permintaan).
  3. Server Anda merespons dengan konfigurasi agen (Prompt, suara, produk, alat). ThunderPhone menggunakan konfigurasi tersebut untuk panggilan.
  4. 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.
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, 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 ...
    pass
Express
import 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:

KolomTipeDeskripsi
promptstring (wajib)Prompt sistem untuk agen
voicestring (wajib)ID suara dari GET /v1/voices
productstringDefault-nya adalah spark
background_trackstring | nullID audio latar
acknowledgement_prompt_modestringauto atau manual (khusus Storm-with-ack)
acknowledgement_promptstringWajib jika mode adalah manual
toolsarraySkema 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