ThunderPhone 2.0 yayında.Kendi başınıza kullanmaya başlayın; dakikada 2¢'den başlayan fiyatlarla.Duyuruyu okuyun

Developer cookbook

Çağrı başına dinamik yapılandırma

Kontrol ettiğiniz bir webhook

Varsayılan olarak her telefon numarasına ve yayınlanabilir anahtara statik bir ajan atanır. Arayan başına veya ziyaretçi başına özelleştirme gerektiğinde — VIP yönlendirme, oturum açmış kullanıcı bağlamı, A/B istem testleri — webhook moduna geçin ve sunucunuzun karar vermesine izin verin.

Nasıl çalışır

  1. telephony.incoming (telefon) veya web.incoming (bileşen) etkinliğine abone olursunuz. Her ikisi de engelleyici webhook'lardır: ThunderPhone, aramaya devam etmeden önce yanıtınızı 10 saniyeye kadar bekler.
  2. ThunderPhone size {call_id, from_number, to_number} gönderir (bileşen oturumları numaralar yerine bileşene özgü alanlar içerir — bkz. istek şeması).
  3. Sunucunuz bir ajan yapılandırmasıyla yanıt verir (istem, ses, ürün, araçlar). ThunderPhone bu yapılandırmayı arama için kullanır.
  4. {} döndürürseniz, zaman aşımına uğrarsanız veya hata oluşursa, yedek olarak statik atanmış ajan kullanılır. Güvenli varsayılan.

1. Webhook hedefini yapılandırın

Telefon numaraları için uç noktanızı telephony.incoming etkinliğine abone edin:

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"]
  }'

Yanıt, tek kullanımlık bir secret içerir — kaydedin; bunu imza doğrulaması için kullanacaksınız.

Bileşen oturumları için, uç nokta URL'niz yerleşik olarak bulunan mode="webhook" değerinde bir yayınlanabilir anahtar oluşturun:

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"]
  }'

Bileşen, her oturum başlangıcında bu URL'ye POST isteği gönderir.

2. İşleyiciyi uygulayın

Üç temel kural:

  • Her istekte imzayı doğrulayın (bkz. Webhook imzalarını doğrulama). Geliştirme ortamında bunu atlamayın — bir kez doğru yapın ve yeniden kullanın.
  • Hızlı yanıt verin. On saniye kesin üst sınırdır ve her saniye arayan için sessizlik demektir. Gerekirse veritabanı sorguları yapın, ancak alt seviye LLM'leri eşzamanlı olarak çağırmayın — dinamik istem oluşturmak istiyorsanız önceden hesaplayın ve önbelleğe alın.
  • Temiz şekilde geri dönün. Beklenmeyen herhangi bir durum, statik olarak atanmış ajanın çağrıyı işlemesi için {} döndürmelidir.
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. Yanıt şeması

Yanıt gövdesi, gelen çağrı yanıt şemasıyla tam olarak eşleşir. Yaygın kullanılan alanlar:

AlanTürAçıklama
promptdize (zorunlu)Ajan için sistem istemi
voicedize (zorunlu)GET /v1/voices yanıtındaki ses kimliği
productdizeVarsayılan değer spark
background_trackdize | nullOrtam sesi kimliği
acknowledgement_prompt_modedizeauto veya manual (yalnızca Storm-with-ack)
acknowledgement_promptdizeMod manual olduğunda zorunludur
toolsdiziSatır içi işlev aracı şemaları — bkz. İşlev Araçları

Kalıplar

Oturum açmış kullanıcı bağlamı

Webhook modundaki widget'larda ziyaretçinin sayfası kim olduğunu zaten bilir. Widget SDK'sının ilettiği bir sorgu dizesi parametresiyle (?customer_id=123) webhook'unuzu çağırın ve müşteriyi sunucu tarafında arayın.

A/B istemi yayına alma

Bunu kendiniz geliştirmeden önce, ThunderPhone'un varyantları tanımlayan, trafiği bölen ve varyant başına sonuçları karşılaştıran yerleşik bir Deneyler özelliği (/dashboard/experiments ve ajan oluşturucunun A/B sekmesi) olduğunu unutmayın — webhook gerekmez.

Yine de webhook tarafında denetime ihtiyacınız varsa: call_id değerini karma işleminden geçirin → gruplandırın; 0..49 için istem A'yı, 50..99 için istem B'yi sunun. Seçtiğiniz grubu kendi veritabanınıza kaydedin ve daha sonra tamamlanan çağrının notuyla ilişkilendirin.

Zamana dayalı yönlendirme

Mesai saatleri → "canlı destek" ajanı; mesai dışı → "mesaj alma" ajanı. İşleyicinizde new Date().getUTCHours() üzerinde basit bir koşul kullanın.


Sonraki adımlar