Open in
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ă
- Vă abonați la evenimentul
telephony.incoming(telefon) sauweb.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. - ThunderPhone vă trimite
{call_id, from_number, to_number}(sesiunile de widget includ câmpuri specifice widgetului în locul numerelor — consultați schema cererii). - Serverul dumneavoastră răspunde cu o configurație a agentului (prompt, voce, produs, instrumente). ThunderPhone utilizează acea configurație pentru apel.
- 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 cuend_reasoninbound_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 ovoiceimplicite (sau cu unagent_idsalvat). Returnarea lui{}sau a unei stări de eroare încheie apelul.
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 ...
passimport 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âmp | Tip | Descriere |
|---|---|---|
prompt | șir (obligatoriu) | Prompt de sistem pentru agent |
voice | șir (obligatoriu) | ID-ul vocii din GET /v1/voices |
product | șir | Valoarea implicită este spark. Produsele experimentale sunt disponibile numai pentru organizațiile incluse pe lista de autorizare. |
background_track | șir | null | ID audio ambiental |
acknowledgement_prompt_mode | șir | auto sau manual (numai pentru Storm cu confirmare) |
acknowledgement_prompt | șir | Obligatoriu când modul este manual |
language_switching | șir | Pentru agenți multilingvi, ask_first sau follow_caller |
tools | matrice | Scheme 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
Scheme exacte de solicitare + răspuns, inclusiv fiecare cheie de configurare.
Configurați HMAC corect o dată; reutilizați-l peste tot.
Combinați rutarea dinamică cu instrumente specifice fiecărui agent.
Reîncercări, ordonare, expirări.