ThunderPhone 2.0 ist live.Direkt im Self-Service – ab 2 ¢/Min..Ankündigung lesen

Developer cookbook

Dynamische Konfiguration pro Anruf

Wählen Sie für jeden eingehenden Anruf separat den antwortenden Agenten aus oder überschreiben Sie dessen Prompt und Einstellungen, gesteuert durch benutzerdefinierte Logik in einem von Ihnen kontrollierten Webhook.

Standardmäßig ist jeder Telefonnummer und jedem veröffentlichbaren Schlüssel ein statischer Agent zugewiesen. Wenn Sie eine Anpassung pro Anrufer oder pro Besucher benötigen — VIP-Routing, Kontext angemeldeter Benutzer, A/B-Tests für Prompts — wechseln Sie in den Webhook-Modus und lassen Sie Ihren Server entscheiden.

So funktioniert es

  1. Abonnieren Sie das Ereignis telephony.incoming (Telefon) oder web.incoming (Widget). Beide sind blockierende Webhooks: ThunderPhone wartet bis zu 10 Sekunden auf Ihre Antwort, bevor der Anruf fortgesetzt wird.
  2. ThunderPhone sendet Ihnen {call_id, from_number, to_number} (Widget- Sitzungen enthalten widgetspezifische Felder statt Nummern — siehe das Anfrageschema).
  3. Ihr Server antwortet mit einer Agentenkonfiguration (Prompt, Stimme, Produkt, Tools). ThunderPhone verwendet diese Konfiguration für den Anruf.
  4. Wenn Sie {} zurückgeben, ein Timeout auftritt oder ein Fehler auftritt, wird der statisch zugewiesene Agent als Fallback verwendet. Sichere Standardeinstellung.

1. Webhook-Ziel konfigurieren

Abonnieren Sie für Telefonnummern an Ihrem Endpunkt 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"]
  }'

Die Antwort enthält ein einmaliges secret — speichern Sie es; Sie benötigen es für die Signaturüberprüfung.

Erstellen Sie für Widget-Sitzungen einen veröffentlichbaren Schlüssel im mode="webhook" mit Ihrer fest integrierten Endpunkt-URL:

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"]
  }'

Das Widget sendet bei jedem Sitzungsstart einen POST-Request an diese URL.

2. Implementieren Sie den Handler

Drei Faustregeln:

  • Überprüfen Sie die Signatur bei jeder Anfrage (siehe Webhook-Signaturen überprüfen). Überspringen Sie dies nicht in der Entwicklung — machen Sie es einmal richtig und verwenden Sie es wieder.
  • Antworten Sie schnell. Zehn Sekunden sind die harte Obergrenze, und jede Sekunde ist Stille für den Anrufer. Führen Sie bei Bedarf Datenbankabfragen durch, aber rufen Sie nachgelagerte LLMs nicht synchron auf — wenn Sie eine dynamische Prompt-Generierung möchten, berechnen Sie diese vorab und cachen Sie sie.
  • Nutzen Sie einen sauberen Fallback. Jeder unerwartete Zustand sollte {} zurückgeben, damit der statisch zugewiesene Agent den Anruf verarbeitet.
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. Antwortschema

Der Antworttext entspricht exakt dem Antwortschema für eingehende Anrufe. Die häufig verwendeten Felder:

FeldTypBeschreibung
promptZeichenfolge (erforderlich)System-Prompt für den Agenten
voiceZeichenfolge (erforderlich)Sprach-ID aus GET /v1/voices
productZeichenfolgeStandardwert ist spark
background_trackZeichenfolge | nullID für Hintergrund-Audio
acknowledgement_prompt_modeZeichenfolgeauto oder manual (nur Storm mit Bestätigung)
acknowledgement_promptZeichenfolgeErforderlich, wenn der Modus manual ist
toolsArrayInline-Schemas für Funktionstools — siehe Funktionstools

Muster

Kontext angemeldeter Nutzer

In Widgets im Webhook-Modus weiß die Seite des Besuchers bereits, wer er ist. Rufen Sie Ihren Webhook mit einem Abfragezeichenfolgenparameter auf, den das Widget-SDK weiterleitet (?customer_id=123), und ermitteln Sie den Kunden serverseitig.

A/B-Prompt-Rollout

Bevor Sie dies selbst implementieren, beachten Sie, dass ThunderPhone über eine native Funktion für Experimente verfügt (/dashboard/experiments und der Tab A/B im Agent-Builder), die Varianten definiert, Traffic aufteilt und Ergebnisse pro Variante vergleicht — kein Webhook erforderlich.

Falls Sie dennoch Kontrolle auf Webhook-Seite benötigen: Hashen Sie call_id → Bucket; liefern Sie Prompt A für 0..49 und Prompt B für 50..99 aus. Speichern Sie, welchen Bucket Sie gewählt haben, in Ihrer eigenen DB und korrelieren Sie ihn später mit der Bewertung des abgeschlossenen Anrufs.

Zeitbasiertes Routing

Geschäftszeiten → Agent für „Live-Support“; außerhalb der Geschäftszeiten → Agent zum „Hinterlassen einer Nachricht“. Einfaches Umschalten über new Date().getUTCHours() in Ihrem Handler.


Nächste Schritte