ThunderPhone 2.0 este acum disponibil.Îl configurați singur, de la 2 ¢/min.Citiți anunțul

Developer cookbook

Configurare dinamică pentru fiecare apel

Alegeți agentul care răspunde — sau rescrieți-i instrucțiunile și setările — separat pentru fiecare apel primit, pe baza unei logici personalizate dintr-un webhook pe care îl controlați.

În mod implicit, fiecărui număr de telefon și fiecărei chei publicabile îi este atribuit un agent static. Când aveți nevoie de personalizare pentru fiecare apelant sau pentru fiecare vizitator — rutare VIP, context pentru utilizatorii autentificați, teste A/B pentru prompturi — treceți la modul webhook și lăsați serverul să decidă.

Cum funcționează

  1. Vă abonați la evenimentul telephony.incoming (telefon) sau web.incoming (widget). Ambele sunt webhookuri blocante: ThunderPhone așteaptă până la 10 secunde răspunsul dumneavoastră înainte de a continua apelul.
  2. ThunderPhone vă trimite {call_id, from_number, to_number} (sesiunile widget includ câmpuri specifice widgetului în locul numerelor — consultați schema solicitării).
  3. Serverul dumneavoastră răspunde cu o configurație de agent (prompt, voce, produs, instrumente). ThunderPhone utilizează acea configurație pentru apel.
  4. Dacă returnați {}, expiră timpul de răspuns sau apare o eroare, agentul atribuit static este utilizat ca rezervă. O valoare implicită sigură.

1. Configurați destinația webhook

Pentru numere de telefon, abonați endpointul dumneavoastră la 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"]
  }'

Răspunsul include un secret de unică folosință — salvați-l; îl veți utiliza pentru verificarea semnăturii.

Pentru sesiunile widget, creați o cheie publicabilă în mode="webhook" cu URL-ul endpointului inclus:

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

Widgetul va trimite o solicitare POST către acest URL la începutul fiecărei sesiuni.

2. Implementați gestionarul

Trei reguli practice:

  • Verificați semnătura pentru fiecare cerere (consultați Verificarea semnăturilor webhook). Nu omiteți acest pas în dezvoltare — implementați-l corect o dată și reutilizați-l.
  • Răspundeți rapid. Zece secunde reprezintă limita strictă, iar fiecare secundă este tăcere pentru apelant. Efectuați căutări în baza de date dacă este necesar, dar nu apelați LLM-uri din aval sincron — dacă doriți generarea dinamică a prompturilor, precalculați și stocați în cache.
  • Folosiți o revenire curată. Orice stare neașteptată trebuie să returneze {}, astfel încât agentul atribuit static să gestioneze apelul.
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. Schema răspunsului

Corpul răspunsului corespunde exact schema de răspuns pentru apeluri primite. Câmpurile utilizate frecvent:

CâmpTipDescriere
promptșir de caractere (obligatoriu)Prompt de sistem pentru agent
voiceșir de caractere (obligatoriu)ID vocal din GET /v1/voices
productșir de caractereValoarea implicită este spark
background_trackșir de caractere | nullID audio ambiental
acknowledgement_prompt_modeșir de caractereauto sau manual (numai Storm-cu-confirmare)
acknowledgement_promptșir de caractereObligatoriu când modul este manual
toolsmatriceScheme inline pentru instrumente de funcții — consultați Instrumente de funcții

Modele

Contextul utilizatorului autentificat

În widgeturile în modul webhook, pagina vizitatorului știe deja cine este acesta. Apelați webhookul cu un parametru de șir de interogare pe care SDK-ul widgetului îl redirecționează (?customer_id=123) și căutați clientul pe server.

Lansare A/B a prompturilor

Înainte de a implementa manual acest lucru, rețineți că ThunderPhone are o funcție nativă Experimente (/dashboard/experiments și fila A/B din generatorul de agenți) care definește variante, distribuie traficul și compară rezultatele pentru fiecare variantă — nu este necesar niciun webhook.

Dacă aveți totuși nevoie de control din partea webhookului: calculați hash-ul pentru call_id → compartiment; serviți promptul A pentru 0..49 și promptul B pentru 50..99. Înregistrați compartimentul ales în propria bază de date și corelați-l ulterior cu evaluarea apelului finalizat.

Rutare bazată pe timp

Program de lucru → agent pentru „asistență în direct”; în afara programului → agent pentru „preluare mesaj”. Comutare simplă pe new Date().getUTCHours() în handlerul dumneavoastră.


Pașii următori