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
- Abonnieren Sie das Ereignis
telephony.incoming(Telefon) oderweb.incoming(Widget). Beide sind blockierende Webhooks: ThunderPhone wartet bis zu 10 Sekunden auf Ihre Antwort, bevor der Anruf fortgesetzt wird. - ThunderPhone sendet Ihnen
{call_id, from_number, to_number}(Widget- Sitzungen enthalten widgetspezifische Felder statt Nummern — siehe das Anfrageschema). - Ihr Server antwortet mit einer Agentenkonfiguration (Prompt, Stimme, Produkt, Tools). ThunderPhone verwendet diese Konfiguration für den Anruf.
- 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.
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. Antwortschema
Der Antworttext entspricht exakt dem Antwortschema für eingehende Anrufe. Die häufig verwendeten Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
prompt | Zeichenfolge (erforderlich) | System-Prompt für den Agenten |
voice | Zeichenfolge (erforderlich) | Sprach-ID aus GET /v1/voices |
product | Zeichenfolge | Standardwert ist spark |
background_track | Zeichenfolge | null | ID für Hintergrund-Audio |
acknowledgement_prompt_mode | Zeichenfolge | auto oder manual (nur Storm mit Bestätigung) |
acknowledgement_prompt | Zeichenfolge | Erforderlich, wenn der Modus manual ist |
tools | Array | Inline-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
Exakte Anfrage- und Antwortschemas, einschließlich aller Konfigurationsschlüssel.
Setzen Sie HMAC einmal korrekt um; verwenden Sie es überall wieder.
Kombinieren Sie dynamisches Routing mit agentenspezifischen Tools.
Wiederholungen, Reihenfolge, Timeouts.