Open in
Dynamische Konfiguration pro Anruf
Wählen Sie für jeden eingehenden Anruf separat den antwortenden Agenten aus – oder schreiben Sie dessen Prompt und Einstellungen neu –, gesteuert durch benutzerdefinierte Logik in einem von Ihnen kontrollierten Webhook.
Standardmäßig ist jeder Telefonnummer und jeder veröffentlichbare Schlüssel ein fester Agent zugewiesen. Wenn Sie Anpassungen pro Anrufer oder pro Besucher benötigen — VIP-Weiterleitung, Kontext für angemeldete Nutzer, A/B-Tests für Prompts — wechseln Sie in den Webhook-Modus und lassen Sie Ihren Server entscheiden.
So funktioniert es
- Sie abonnieren das Ereignis
telephony.incoming(Telefon) oderweb.incoming(Widget). Beide sind blockierende Webhooks: ThunderPhone wartet auf Ihre Antwort, bevor der Anruf fortgesetzt wird — bis zu 13 Sekunden bei einem Telefonanruf (einschließlich eines Wiederholungsversuchs) und 10 Sekunden bei einer Widget-Sitzung. - ThunderPhone sendet Ihnen
{call_id, from_number, to_number}(Widget- Sitzungen enthalten statt Nummern widgetspezifische Felder — siehe das Anfrageschema). - Ihr Server antwortet mit einer Agentenkonfiguration (Prompt, Stimme, Produkt, Tools). ThunderPhone verwendet diese Konfiguration für den Anruf.
- Es gibt keinen Fallback-Agenten. Wenn Sie
{}oder eine ungültige Konfiguration zurückgeben oder die Anfrage nach ihrem Wiederholungsversuch weiterhin fehlschlägt, wird der Telefonanruf sofort beendet und mitend_reasoninbound_config_webhook_failedaufgezeichnet; die Anfrage einer Widget-Sitzung schlägt fehl.
1. Webhook-Ziel konfigurieren
Abonnieren Sie für Telefonnummern Ihren Endpunkt für 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 Signaturprüfung.
Erstellen Sie für Widget-Sitzungen einen veröffentlichbaren Schlüssel mit
mode="webhook", in dem Ihre Endpunkt-URL hinterlegt ist:
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 eine POST-Anfrage 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 — richten Sie es einmal korrekt ein und verwenden Sie es wieder.
- Antworten Sie schnell. Der erste Versuch eines Anrufs wird nach 10 Sekunden abgebrochen und der gesamte Austausch nach 13, und jede Sekunde ist Stille für den Anrufer. Führen Sie bei Bedarf Datenbankabfragen durch, rufen Sie jedoch keine nachgelagerten LLMs synchron auf — wenn Sie dynamische Prompt- Generierung benötigen, berechnen Sie sie vorab und cachen Sie sie.
- Greifen Sie sauber zurück. Antworten Sie bei jedem unerwarteten Zustand mit einem
Standard-
promptund einer Standard-voice(oder einer gespeichertenagent_id). Die Rückgabe von{}oder eines Fehlerstatus beendet den Anruf.
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 | string (erforderlich) | System-Prompt für den Agenten |
voice | string (erforderlich) | Sprach-ID von GET /v1/voices |
product | string | Standardmäßig spark. Experimentelle Produkte sind nur für Organisationen auf der Zulassungsliste verfügbar. |
background_track | string | null | ID der Hintergrund-Audiospur |
acknowledgement_prompt_mode | string | auto oder manual (nur Storm mit Bestätigung) |
acknowledgement_prompt | string | Erforderlich, wenn der Modus manual ist |
language_switching | string | Für mehrsprachige Agenten: ask_first oder follow_caller |
tools | array | Inline-Schemas für Funktions-Tools — siehe Funktions-Tools |
Einen gespeicherten Agenten beibehalten und Variablen übergeben
Geben Sie {"agent_id": 12, "variables": {"name": "Ada"}} zurück, um den
gespeicherten Agenten dieser Organisation mit daten pro Anruf zu verwenden. Sein Prompt kann {{name}} oder
{{name|Friend}} enthalten. Webhook-Variablen werden mit Variablen auf Anforderungsebene zusammengeführt; null
verwendet den Standardwert des Platzhalters oder leeren Text, wenn keiner angegeben ist. Die endgültigen
Werte und nicht aufgelösten Namen erscheinen in den Anrufdetails und Abschluss-Webhooks.
Antworten für gespeicherte Agenten akzeptieren nur agent_id, variables und timezone. Wenn prompt
vorhanden ist, verwendet die Antwort die Inline-Konfiguration und ignoriert agent_id (einschließlich
null- oder nicht ganzzahliger Metadaten); der Inline-Prompt muss weiterhin gültig sein. Antworten mit Inline-
Konfiguration können ebenfalls variables enthalten. Antworten für gespeicherte Agenten verwenden die
bereitgestellte A/B-Aufteilung des Agenten sowohl bei Telefon- als auch bei Widget-Anrufen und rendern anschließend Variablen. Siehe Anrufvariablen
für Limits und Unterstützung der Sitzungs-API. Die blockierende Konfiguration stammt von der älteren
URL für Telefonnummer/Organisation oder einem Widget-Schlüssel im Webhook-Modus; eingehende Ereignisse des Endpunktsystems sind lediglich Benachrichtigungen.
Zeitzone pro Anruf festlegen
Ein Agent kann Anrufe von Kontakten in unterschiedlichen Zeitzonen beantworten. Fügen Sie timezone, einen
IANA-Namen, zu einer gespeicherten Agentenantwort oder einer Inline-Antwort hinzu, und der Agent nennt für diesen Anruf das Datum und die Uhrzeit und bietet Zeiten in dieser Zone an, unabhängig von der eigenen Zeitzone des Agenten:
{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}Senden Sie die Zone statt eines Zeitstempels in variables: Die Uhr des Agenten läuft während des Anrufs weiter, und eine übergebene Zeit wird veraltet. Ein Wert, der keine IANA-Zone ist, wird ignoriert, und der Anruf wird in der Zeitzone des Agenten verbunden.
Muster
Kontext angemeldeter Benutzer
In Widgets im Webhook-Modus weiß die Seite des Besuchers bereits, wer er ist.
Rufen Sie Ihren Webhook mit einem Query-String-Parameter auf, den das Widget-SDK
weiterleitet (?customer_id=123), und suchen Sie den Kunden serverseitig.
A/B-Prompt-Rollout
Bevor Sie dies selbst implementieren, beachten Sie, dass ThunderPhone eine native
Funktion für Experimente bietet
(/dashboard/experiments und den Tab A/B im Agent-Builder), die Varianten
definiert, Traffic aufteilt und Ergebnisse je Variante vergleicht —
kein Webhook erforderlich.
Falls Sie dennoch eine Steuerung 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 den gewählten Bucket in Ihrer eigenen Datenbank und korrelieren Sie
ihn später mit der Bewertung des abgeschlossenen Anrufs.
Zeitbasierte Weiterleitung
Geschäftszeiten → Agent für „Live-Support“; außerhalb der Geschäftszeiten → Agent
für „Nachricht aufnehmen“. Einfacher Wechsel basierend auf
new Date().getUTCHours() in Ihrem Handler.
Nächste Schritte
Exakte Anfrage- und Antwortschemata, einschließlich aller Konfigurationsschlüssel.
Richten Sie HMAC einmal korrekt ein und verwenden Sie es überall wieder.
Kombinieren Sie dynamische Weiterleitung mit Tools pro Agent.
Wiederholungen, Reihenfolge, Timeouts.