ThunderPhone 2.0 è arrivato.Attivazione self-service, da 2¢/min.Leggi l’annuncio

Developer cookbook

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

  1. Ti iscrivi all'evento telephony.incoming (telefono) o web.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.
  2. 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).
  3. Il tuo server risponde con una configurazione dell'agente (prompt, voce, prodotto, strumenti). ThunderPhone utilizza tale configurazione per la chiamata.
  4. 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 con end_reason inbound_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 prompt e voice (oppure con un agent_id salvato). Restituire {} o uno stato di errore termina la chiamata.
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 della risposta

Il corpo della risposta corrisponde esattamente allo schema della risposta per le chiamate in entrata. I campi più usati:

CampoTipoDescrizione
promptstringa (obbligatorio)Prompt di sistema per l'agente
voicestringa (obbligatorio)ID della voce da GET /v1/voices
productstringaIl valore predefinito è spark. I prodotti sperimentali sono disponibili solo per le organizzazioni autorizzate.
background_trackstringa | nullID dell'audio ambientale
acknowledgement_prompt_modestringaauto o manual (solo Storm-with-ack)
acknowledgement_promptstringaObbligatorio quando la modalità è manual
language_switchingstringaPer gli agenti multilingue, ask_first o follow_caller
toolsarraySchemi 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