ThunderPhone 2.0 kini resmi hadir.Gunakan langsung secara mandiri, mulai 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 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

  1. Anda berlangganan event telephony.incoming (telepon) atau web.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.
  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. 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 dengan end_reason inbound_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 prompt dan voice default (atau agent_id yang tersimpan). Mengembalikan {} atau status error akan mengakhiri 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 sama persis dengan skema respons panggilan masuk. Kolom yang umum digunakan:

BidangTipeDeskripsi
promptstring (wajib)Prompt sistem untuk agen
voicestring (wajib)ID suara dari GET /v1/voices
productstringDefaultnya adalah spark. Produk eksperimental hanya tersedia untuk organisasi dalam daftar izin.
background_trackstring | nullID audio ambient
acknowledgement_prompt_modestringauto atau manual (khusus Storm dengan ack)
acknowledgement_promptstringWajib saat mode adalah manual
language_switchingstringUntuk agen multibahasa, ask_first atau follow_caller
toolsarraySkema 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