ThunderPhone 2.0 este acum disponibil.Self-service, 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 promptul ș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 dumneavoastră 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ă răspunsul dumneavoastră înainte de a continua apelul, până la 13 secunde pentru un apel telefonic (inclusiv o reîncercare) și 10 secunde pentru o sesiune de widget.
  2. ThunderPhone vă trimite {call_id, from_number, to_number} (sesiunile de widget includ câmpuri specifice widgetului în locul numerelor — consultați schema cererii).
  3. Serverul dumneavoastră răspunde cu o configurație a agentului (prompt, voce, produs, instrumente). ThunderPhone utilizează acea configurație pentru apel.
  4. Nu există agent de rezervă. Dacă returnați {} sau o configurație nevalidă sau dacă cererea eșuează în continuare după reîncercare, apelul telefonic este încheiat imediat și înregistrat cu end_reason inbound_config_webhook_failed; solicitarea unei sesiuni de widget eșuează.

1. Configurați destinația webhookului

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 cu utilizare unică — 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 dumneavoastră 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 cerere POST la acest URL la începutul fiecărei sesiuni.

2. Implementați handlerul

Trei reguli practice:

  • Verificați semnătura la fiecare solicitare (consultați Verificarea semnăturilor webhook). Nu săriți peste acest pas în dezvoltare — faceți-l corect o dată și reutilizați-l.
  • Răspundeți rapid. Prima încercare a unui apel telefonic este întreruptă după 10 secunde, iar întregul schimb după 13, iar fiecare secundă înseamnă liniște pentru apelant. Efectuați căutări în baza de date dacă este necesar, dar nu apelați sincron LLM-uri din etapele ulterioare — dacă doriți generarea dinamică a prompturilor, precalculați și stocați în cache.
  • Reveniți curat la valoarea implicită. Pentru orice stare neașteptată, răspundeți cu un prompt și o voice implicite (sau cu un agent_id salvat). Returnarea lui {} sau a unei stări de eroare încheie 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 de răspuns

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

CâmpTipDescriere
promptșir (obligatoriu)Prompt de sistem pentru agent
voiceșir (obligatoriu)ID-ul vocii din GET /v1/voices
productșirValoarea implicită este spark. Produsele experimentale sunt disponibile numai pentru organizațiile incluse pe lista de autorizare.
background_trackșir | nullID audio ambiental
acknowledgement_prompt_modeșirauto sau manual (numai pentru Storm cu confirmare)
acknowledgement_promptșirObligatoriu când modul este manual
language_switchingșirPentru agenți multilingvi, ask_first sau follow_caller
toolsmatriceScheme inline pentru instrumente de funcții — consultați Instrumente de funcții

Păstrați un agent salvat și furnizați variabile

Returnați {"agent_id": 12, "variables": {"name": "Ada"}} pentru a utiliza agentul salvat al organizației respective cu date specifice fiecărui apel. Promptul său poate conține {{name}} sau {{name|Friend}}. Variabilele webhook se îmbină peste variabilele de la nivelul cererii; valoarea null utilizează valoarea implicită a substituentului sau text gol dacă nu este furnizată nicio valoare. Valorile finale și numele nerezolvate apar în detaliile apelului și în webhookurile de finalizare. Răspunsurile pentru agenții salvați acceptă numai agent_id, variables și timezone. Dacă prompt este prezent, răspunsul utilizează configurația inline și ignoră agent_id (inclusiv metadatele null sau care nu sunt numere întregi); promptul inline trebuie totuși să fie valid. Răspunsurile cu configurație inline pot include și variables. Răspunsurile pentru agenții salvați utilizează împărțirea A/B implementată a agentului atât pentru apelurile telefonice, cât și pentru cele din widget, apoi redau variabilele. Consultați variabilele apelului pentru limite și suport pentru API-ul de sesiune. Configurația de blocare provine din URL-ul moștenit al numărului de telefon/organizației sau dintr-o cheie de widget în modul webhook; evenimentele primite de sistemul endpoint sunt doar notificări.

Setați fusul orar pentru fiecare apel

Un agent poate răspunde contactelor din fusuri orare diferite. Adăugați timezone, un nume IANA, la un răspuns cu agent salvat sau inclus și agentul indică data și ora și oferă intervale orare în acel fus pentru acest apel, înaintea fusului orar propriu al agentului:

{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}

Trimiteți fusul orar, nu un marcaj temporal, în variables: ceasul agentului continuă să ruleze în timpul apelului, iar o oră transmisă devine depășită. O valoare care nu este un fus orar IANA este ignorată, iar apelul se conectează în fusul orar al agentului.

Modele

Contextul utilizatorului autentificat

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

Lansare graduală A/B a prompturilor

Înainte de a implementa acest lucru manual, 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 nevoie totuși de control pe partea webhookului: aplicați hash la call_id → grup; serviți promptul A pentru 0..49 și promptul B pentru 50..99. Înregistrați grupul ales în propria bază de date și corelați-l ulterior cu evaluarea apelului finalizat.

Rutare bazată pe timp

În timpul programului de lucru → agent de „asistență live”; în afara programului → agent de „preluare mesaje”. Comutare simplă pe new Date().getUTCHours() în handlerul dumneavoastră.


Pașii următori