Open in
Configurazione dinamica per chiamata
Scegli l
Per impostazione predefinita, a ogni numero di telefono e chiave pubblicabile è assegnato un agente statico. Quando ti serve una personalizzazione per chiamante o per visitatore — instradamento VIP, contesto dell'utente connesso, test A/B dei prompt — passa alla modalità webhook e lascia decidere al tuo server.
Come funziona
- Ti iscrivi all'evento
telephony.incoming(telefono) oweb.incoming(widget). Entrambi sono webhook bloccanti: ThunderPhone attende la tua risposta prima di proseguire la chiamata, fino a 13 secondi per una chiamata telefonica (incluso un tentativo aggiuntivo) e 10 secondi per una sessione widget. - ThunderPhone ti invia
{call_id, from_number, to_number}(le sessioni widget includono campi specifici del widget invece dei numeri: consulta lo schema della richiesta). - Il tuo server risponde con una configurazione dell'agente (prompt, voce, prodotto, strumenti). ThunderPhone utilizza tale configurazione per la chiamata.
- Non esiste un agente di fallback. Se restituisci
{}o una configurazione non valida, oppure la richiesta continua a non riuscire dopo il relativo ritentativo, la chiamata telefonica viene terminata immediatamente e registrata conend_reasoninbound_config_webhook_failed; una richiesta di sessione widget non riesce.
1. Configura la destinazione webhook
Per i numeri di telefono, iscrivi il tuo endpoint a 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"]
}'La risposta include un secret monouso: salvalo; ti servirà
per la verifica della firma.
Per le sessioni widget, crea una chiave pubblicabile in mode="webhook"
con l'URL del tuo endpoint incorporato:
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"]
}'Il widget eseguirà una richiesta POST a questo URL all'avvio di ogni sessione.
2. Implementa il gestore
Tre regole pratiche:
- Verifica la firma su ogni richiesta (consulta Verifica le firme dei webhook). Non saltare questo passaggio in sviluppo: configurarlo correttamente una volta e riutilizzarlo.
- Rispondi rapidamente. Il primo tentativo di una chiamata telefonica viene interrotto dopo 10 secondi e l'intero scambio dopo 13, e ogni secondo è silenzio per il chiamante. Esegui ricerche nel database se necessario, ma non chiamare LLM a valle in modo sincrono: se vuoi generare prompt dinamici, precalcolali e memorizzali nella cache.
- Usa un fallback pulito. Per qualsiasi stato imprevisto, rispondi con un valore predefinito per
promptevoice(oppure con unagent_idsalvato). Restituire{}o uno stato di errore termina la chiamata.
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 della risposta
Il corpo della risposta corrisponde esattamente allo schema della risposta per le chiamate in entrata. I campi più usati:
| Campo | Tipo | Descrizione |
|---|---|---|
prompt | stringa (obbligatorio) | Prompt di sistema per l'agente |
voice | stringa (obbligatorio) | ID della voce da GET /v1/voices |
product | stringa | Il valore predefinito è spark. I prodotti sperimentali sono disponibili solo per le organizzazioni autorizzate. |
background_track | stringa | null | ID dell'audio ambientale |
acknowledgement_prompt_mode | stringa | auto o manual (solo Storm-with-ack) |
acknowledgement_prompt | stringa | Obbligatorio quando la modalità è manual |
language_switching | stringa | Per gli agenti multilingue, ask_first o follow_caller |
tools | array | Schemi degli strumenti funzione in linea — vedi Strumenti funzione |
Mantieni un agente salvato e fornisci variabili
Restituisci {"agent_id": 12, "variables": {"name": "Ada"}} per usare l'agente
salvato di quell'organizzazione con dati per chiamata. Il relativo prompt può contenere {{name}} o
{{name|Friend}}. Le variabili del webhook vengono unite alle variabili a livello di richiesta; null
usa il valore predefinito del segnaposto oppure testo vuoto se non ne viene fornito alcuno. I valori finali
e i nomi non risolti vengono visualizzati nei dettagli della chiamata e nei webhook di completamento.
Le risposte dell'agente salvato accettano solo agent_id, variables e timezone. Se prompt è
presente, la risposta usa la configurazione inline e ignora agent_id (inclusi
metadati null o non interi); il prompt inline deve comunque essere valido. Le risposte con configurazione inline
possono includere anche variables. Le risposte dell'agente salvato usano la suddivisione A/B distribuita dell'agente
sia nelle chiamate telefoniche sia nelle chiamate dal widget, quindi eseguono il rendering delle variabili. Consulta variabili di chiamata
per i limiti e il supporto dell'API di sessione. La configurazione bloccante proviene dall'URL legacy
del numero di telefono/dell'organizzazione o da una chiave del widget in modalità webhook; gli eventi in entrata del sistema endpoint
sono solo notifiche.
Imposta il fuso orario per chiamata
Un agente può rispondere a contatti in fusi orari diversi. Aggiungi timezone, un
nome IANA, a una risposta di agente salvato o inline e l'agente indica la data
e l'ora e propone orari in quel fuso per questa chiamata, anziché nel fuso
orario dell'agente:
{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}Invia il fuso anziché un timestamp in variables: l'orologio dell'agente continua
a scorrere durante la chiamata e l'orario trasmesso diventa obsoleto. Un valore
che non corrisponde a un fuso IANA viene ignorato e la chiamata si connette nel
fuso dell'agente.
Pattern
Contesto dell'utente autenticato
Nei widget in modalità webhook, la pagina del visitatore sa già chi è.
Chiama il tuo webhook con un parametro della stringa di query che l'SDK
del widget inoltra (?customer_id=123) e cerca il cliente lato server.
Distribuzione A/B dei prompt
Prima di implementarlo manualmente, tieni presente che ThunderPhone include una funzionalità nativa
di Esperimenti
(/dashboard/experiments e la scheda A/B del builder dell'agente) che
definisce varianti, divide il traffico e confronta i risultati per variante —
senza webhook.
Se hai comunque bisogno del controllo lato webhook: calcola l'hash di call_id → bucket;
fornisci il prompt A per 0..49 e il prompt B per 50..99. Registra il
bucket scelto nel tuo database e in seguito correlalo alla valutazione della
chiamata completata.
Routing basato sull'orario
Orario lavorativo → agente di "assistenza dal vivo"; fuori orario → agente per
"prendere un messaggio". Semplice switch su new Date().getUTCHours() nel tuo handler.
Passaggi successivi
Schemi esatti di richiesta e risposta, incluse tutte le chiavi di configurazione.
Configura correttamente l'HMAC una volta; riutilizzalo ovunque.
Combina il routing dinamico con strumenti per agente.
Riprova, ordinamento, timeout.