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 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

  1. Sie abonnieren das Ereignis telephony.incoming (Telefon) oder web.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.
  2. ThunderPhone sendet Ihnen {call_id, from_number, to_number} (Widget- Sitzungen enthalten statt Nummern widgetspezifische Felder — siehe das Anfrageschema).
  3. Ihr Server antwortet mit einer Agentenkonfiguration (Prompt, Stimme, Produkt, Tools). ThunderPhone verwendet diese Konfiguration für den Anruf.
  4. 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 mit end_reason inbound_config_webhook_failed aufgezeichnet; 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-prompt und einer Standard-voice (oder einer gespeicherten agent_id). Die Rückgabe von {} oder eines Fehlerstatus beendet den Anruf.
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
promptstring (erforderlich)System-Prompt für den Agenten
voicestring (erforderlich)Sprach-ID von GET /v1/voices
productstringStandardmäßig spark. Experimentelle Produkte sind nur für Organisationen auf der Zulassungsliste verfügbar.
background_trackstring | nullID der Hintergrund-Audiospur
acknowledgement_prompt_modestringauto oder manual (nur Storm mit Bestätigung)
acknowledgement_promptstringErforderlich, wenn der Modus manual ist
language_switchingstringFür mehrsprachige Agenten: ask_first oder follow_caller
toolsarrayInline-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