ThunderPhone 2.0 è arrivato.Parti in autonomia, 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 degli utenti autenticati, 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 fino a 10 secondi la tua risposta prima di proseguire la chiamata.
  2. ThunderPhone ti invia {call_id, from_number, to_number} (le sessioni widget includono campi specifici del widget anziché numeri — consulta lo schema della richiesta).
  3. Il tuo server risponde con una configurazione dell'agente (prompt, voce, prodotto, strumenti). ThunderPhone usa quella configurazione per la chiamata.
  4. Se restituisci {}, vai in timeout o si verifica un errore, viene usato come fallback l'agente assegnato staticamente. Un'impostazione predefinita sicura.

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; lo userai 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 invierà una richiesta POST a questo URL all'avvio di ogni sessione.

2. Implementa il gestore

Tre regole pratiche:

  • Verifica la firma su ogni richiesta (vedi Verifica le firme dei webhook). Non saltare questo passaggio in sviluppo: fallo correttamente una volta e riutilizzalo.
  • Rispondi rapidamente. Dieci secondi sono il limite massimo, e ogni secondo è silenzio per il chiamante. Esegui ricerche nel database se necessario, ma non chiamare LLM downstream in modo sincrono: se vuoi la generazione dinamica dei prompt, precalcola e memorizza nella cache.
  • Applica un fallback pulito. Qualsiasi stato imprevisto deve restituire {} affinché l'agente assegnato staticamente gestisca 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 chiamate in arrivo. I campi usati più comunemente:

CampoTipoDescrizione
promptstringa (obbligatorio)Prompt di sistema per l'agente
voicestringa (obbligatorio)ID voce da GET /v1/voices
productstringaIl valore predefinito è spark
background_trackstringa | nullID dell'audio ambientale
acknowledgement_prompt_modestringaauto o manual (solo Storm-with-ack)
acknowledgement_promptstringaObbligatorio quando la modalità è manual
toolsarraySchemi inline per strumenti funzione: vedi Strumenti funzione

Modelli

Contesto dell'utente autenticato

Nei widget in modalità webhook, la pagina del visitatore sa già chi è. Chiama il tuo webhook con un parametro query string 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 offre una funzionalità nativa di Esperimenti (/dashboard/experiments e la scheda A/B del builder dell'agente) che definisce le varianti, suddivide il traffico e confronta i risultati per variante — senza webhook.

Se hai comunque bisogno del controllo lato webhook: applica un hash a call_id → bucket; fornisci il prompt A per 0..49 e il prompt B per 50..99. Registra nel tuo DB il bucket scelto e in seguito correla il dato con la valutazione della chiamata completata.

Instradamento basato sull'orario

Orario di apertura → agente di "assistenza dal vivo"; fuori orario → agente per "prendere un messaggio". Semplice switch su new Date().getUTCHours() nel tuo handler.


Passaggi successivi