Open in
telephony.incoming / web.incoming
Blockierender Webhook, der die Konfiguration eines eingehenden Anrufs in Echtzeit festlegt.
Wenn ein eingehender Anruf eine Nummer ohne zugewiesenen
Agenten erreicht oder eine Web-Widget-Sitzung mit einem veröffentlichbaren Schlüssel im
mode="webhook" startet, sendet ThunderPhone eine blockierende
telephony.incoming / web.incoming-Anfrage an Ihre
Legacy-Webhook-URL
und wartet auf eine Konfigurationsantwort: bis zu 13 Sekunden für einen
Anruf, einschließlich eines Wiederholungsversuchs, und bis zu 10 Sekunden für eine
Widget-Sitzung (siehe Zeitüberschreitungen und Wiederholungsversuche). Verwenden Sie diesen
Austausch, um Prompt, Stimme und Tools für jeden Anruf dynamisch auszuwählen —
das vollständige Muster finden Sie im Leitfaden zur dynamischen Anrufkonfiguration.
Zeitüberschreitungen und Wiederholungsversuche
Antworten Sie schnell: Der Anrufer wartet in der Leitung, während Sie entscheiden.
Für einen Telefonanruf (telephony.incoming):
- Der erste Versuch kann bis zu 10 Sekunden dauern.
- Wenn er eine Zeitüberschreitung hat, keine Verbindung herstellen kann oder eine
Antwort mit
5xx,408oder429erhält, versucht ThunderPhone es noch einmal. Für den Wiederholungsversuch steht der Rest eines 13-Sekunden-Budgets für den gesamten Austausch zur Verfügung: etwa 3 Sekunden nach einem ersten Versuch mit Zeitüberschreitung, nahezu alle 13 nach einem schnellen Fehler. Es erfolgt kein Wiederholungsversuch, wenn weniger als 2 Sekunden übrig wären. - Jede andere
4xx-Antwort, eine Antwort über dem Größenlimit oder eine2xx-Antwort, deren Body keine gültige Konfiguration enthält, ist endgültig und wird nicht wiederholt. - Beide Versuche senden denselben Body, denselben Header
X-ThunderPhone-Signatureund denselben HeaderX-ThunderPhone-Idempotency-Key(telephony-incoming-<call_id>), sodass ein Handler mit Seiteneffekten den Wiederholungsversuch erkennen kann.
Es gibt keinen Fallback-Agenten. Wenn keine nutzbare Konfiguration eintrifft, wird der
Anruf sofort beendet, statt den Anrufer in einer stillen Leitung warten zu lassen. Er erscheint im Anrufverlauf als fehlgeschlagener, nicht abgerechneter Anruf mit
end_reason inbound_config_webhook_failed, und das übliche
telephony.complete-Ereignis wird gesendet.
Eine Widget-Sitzung (web.incoming) erhält einen einzigen Versuch von bis zu 10
Sekunden. Wenn dieser fehlschlägt, schlägt die Sitzungsanfrage mit 502 oder 422 fehl.
Anfrage-Payload
Für Telefonanrufe (telephony.incoming):
{
"type": "telephony.incoming",
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
}
}| Feld | Typ | Beschreibung |
|---|---|---|
call_id | integer | Anruf-ID — über alle Ereignisse für diesen Anruf hinweg stabil |
from_number | string | E.164-Nummer des Anrufers |
to_number | string | E.164-Zielnummer (eine Ihrer ThunderPhone-Nummern) |
Bei Web-Widget-Sitzungen (web.incoming) identifiziert data statt
Telefonnummern die einbettende Seite:
{
"type": "web.incoming",
"data": {
"call_id": 987654322,
"origin_domain": "https://example.com",
"publishable_key_prefix": "pk_live_a1b2"
}
}| Feld | Typ | Beschreibung |
|---|---|---|
call_id | Ganzzahl | Anruf-ID |
origin_domain | Zeichenfolge | Der Ursprung der Seite, die das Widget hostet, wie vom Browser übermittelt. Ein Nicht-Browser-Client kann einen beliebigen Wert senden. Behandeln Sie ihn daher nicht als Nachweis dafür, woher die Anfrage stammt |
publishable_key_prefix | Zeichenfolge | Erste Zeichen des veröffentlichbaren Schlüssels, der die Sitzung geöffnet hat |
language, primary_language | Zeichenfolge | Vorhanden, wenn die Widget-Sitzung eine Sprachüberschreibung angefordert hat |
voice | Zeichenfolge | Vorhanden, wenn die Widget-Sitzung eine Stimmenüberschreibung angefordert hat |
website_context | Zeichenfolge | Vorhanden, wenn das Widget seitenbezogenen Kontext pro Sitzung übergeben hat |
Die Überschreibungsfelder (language, voice, website_context und alle
Browser-variables) werden nur übermittelt, wenn der veröffentlichbare Schlüssel sie in
seinen Browser-Überschreibungen zulässt. Eine
Sitzungsanfrage, die eine Überschreibung enthält, die der Schlüssel nicht zulässt, wird abgelehnt, bevor
dieser Webhook aufgerufen wird.
Antwortschema
Geben Sie ein JSON-Objekt zurück, das die Agentenkonfiguration für diesen Anruf beschreibt.
prompt und voice sind erforderlich; alles andere ist optional.
{
"prompt": "You are a helpful booking assistant for Acme Restaurant.",
"voice": "john",
"product": "spark",
"background_track": null,
"tools": []
}| Feld | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
prompt | Zeichenfolge | ja | System-Prompt, der den Agenten steuert |
voice | Zeichenfolge | ja | Sprach-ID von GET /v1/voices, z. B. john. voice_name wird als Alias akzeptiert. Unbekannte Stimmen bestehen die Validierung nicht und lehnen den Anruf ab |
product | Zeichenfolge | nein | Standardmäßig spark. Zulässig: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack |
thinking_level | Zeichenfolge | nein | minimal, base (Standard) oder extra. Für Storm-Produkte überschrieben: storm-extra* erzwingt extra, andere storm-* erzwingen base |
audio_context_mode | Zeichenfolge | nein | full (Standard) oder reduced |
watchdog_enabled | boolesch | nein | Überwachung für diesen Anruf aktivieren. Standardmäßig false |
additional_audio_context | boolesch | null | nein | Die letzten paar Gesprächszüge des Anruferaudios statt nur des neuesten Gesprächszugs einbeziehen, um Korrekturen und die Erfassung von Daten mit vielen Buchstaben oder Zahlen bei geringfügig höherer Latenz und Kosten zu verbessern. Für eingehende Sitzungen standardmäßig aktiviert und für ausgehende Telefonanrufe deaktiviert; null behält den Standard bei |
storm_feedback_mode | Zeichenfolge | nein | none, acknowledgement (Standard) oder tick |
language | Zeichenfolge | nein | Kurzform für primary_language |
primary_language | Zeichenfolge | nein | Sprachcode, normalisiert (Standard en). Nicht auflösbare Codes lehnen den Anruf ab |
has_additional_languages | boolesch | nein | Standardmäßig false |
additional_languages | Array von Zeichenfolgen | nein | Zusätzliche Sprachen, zu denen der Agent wechseln kann |
native_voice_switching | boolesch | nein | Standardmäßig false. Wenn der Anruf zu einer anderen Sprache wechselt, wird zu einer in dieser Sprache heimischen Stimme gewechselt, die nach Geschlecht übereinstimmt, statt die konfigurierte Stimme beizubehalten |
language_switching | Zeichenfolge | null | nein | ask_first (Standard) oder follow_caller; null entspricht ask_first. Legt fest, wie ein mehrsprachiger Agent damit umgeht, wenn die andere Person eine andere seiner Sprachen spricht. ask_first wechselt sofort, wenn die Person nach der Sprache fragt oder einen vollständigen Satz darin sagt, und fragt nach etwas Kurzem wie einem „ja“, ob darin fortgefahren werden soll. follow_caller wechselt zu jeder konfigurierten Sprache, die die Person spricht, selbst bei einem einzelnen Wort. Gilt nur, wenn additional_languages festgelegt ist |
background_track | Zeichenfolge | null | nein | ID für Hintergrundaudio oder null |
acknowledgement_prompt_mode | Zeichenfolge | nein | auto (Standard) oder manual (Storm-with-ack-Produkte) |
acknowledgement_prompt | Zeichenfolge | nein | Wird verwendet, wenn acknowledgement_prompt_mode="manual" |
silence_interval_seconds | Ganzzahl | null | nein | 5–120. Sekunden der Stille des Anrufers vor einer Nachfrage |
silence_max_checkins | Ganzzahl | null | nein | 1–10 |
silence_checkins_enabled | boolesch | nein | Standardmäßig true |
connect_tone_enabled | boolesch | nein | Standardmäßig false |
pronunciation_dicts | boolesch | null | nein | Standardmäßig false; null entspricht false. Wenn true, verwendet die Cartesia-Sprachausgabe des Anrufs das Aussprachewörterbuch von ThunderPhone für die Sprache des Anrufs, sofern eines vorhanden ist. Funktioniert für Telefon- und Widget-Anrufe. Gilt nur für die eigenen Cartesia-Stimmen von ThunderPhone, nicht für Stimmen mit Ihrem eigenen Cartesia-API-Schlüssel. Wird nicht mit agent_id verwendet, da dort die Einstellungen des gespeicherten Agenten gelten |
voicemail_action | Zeichenfolge | nein | prompt (Standard), hangup oder message |
voicemail_message | Zeichenfolge | nein | Wird verwendet, wenn voicemail_action="message" |
agent_name | Zeichenfolge | nein | Anzeigename, der an Dashboards und das Widget gemeldet wird |
org_name | Zeichenfolge | nein | Anzeigename der Organisation für die Persona des Agenten |
tools | Array | nein | Schemas für Inline-Funktionstools (siehe Funktionstools) |
transfer_destinations | Array von Objekten | null | nein | Bis zu 50 Einträge {name, number}, an die dieser Anruf weitergeleitet werden kann, validiert wie die Weiterleitungsnummern eines Agenten. Der Agent leitet nach Namen und nur an diese weiter; [] bedeutet keine Weiterleitungen. Lassen Sie das Feld weg oder senden Sie null, damit der Agent an eine selbst gewählte Nummer weiterleiten kann |
timezone | Zeichenfolge | nein | IANA-Zeitzone für diesen Anruf, z. B. America/Chicago. Der Agent nennt Datum und Uhrzeit in dieser Zeitzone, die Vorrang vor der eigenen Zeitzone des Agenten hat. Auch neben agent_id zulässig. Werte, die keine IANA-Zeitzonen sind, werden ignoriert. Siehe Zeitzone pro Anruf festlegen |
call_id | Ganzzahl | nein | Optionales Echo der Call-ID der Anfrage; wird ignoriert |
Da prompt und voice erforderlich sind, lehnt die Rückgabe von {} oder einer
Antwort, die die Validierung nicht besteht, den Anruf ab: Ein Telefonanruf wird
wie in Zeitüberschreitungen und Wiederholungsversuche beschrieben beendet, und eine
Widget-Sitzungsanfrage schlägt mit 422 fehl. Für diesen Pfad gibt es keinen
Fallback auf einen statischen Agenten (einer Nummer oder einem Schlüssel im Webhook-Modus ist kein
Agent zugewiesen).
Begrenzung der Antwortgröße
Beispiel-Handler
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
WEBHOOK_SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]
def verify(body: bytes, signature: str) -> bool:
expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
@app.post("/thunderphone-webhook")
async def webhook(request: Request):
body = await request.body()
if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
raise HTTPException(status_code=401)
event = json.loads(body)
if event["type"] == "telephony.incoming":
caller = event["data"]["from_number"]
prompt = (
"Greet the caller as a San Francisco local…"
if caller.startswith("+1415")
else "You are a friendly customer support agent…"
)
return {
"prompt": prompt,
"voice": "john",
"product": "spark",
}
if event["type"] == "web.incoming":
return {
"prompt": "You are the website's helpful voice assistant…",
"voice": "john",
"product": "spark",
}
return {}import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;
function verify(body, signature) {
const expected = crypto
.createHmac("sha256", SECRET)
.update(body)
.digest("hex");
return signature &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
app.post(
"/thunderphone-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "telephony.incoming" || event.type === "web.incoming") {
const caller = event.data.from_number || "web";
const prompt = caller.startsWith("+1415")
? "Greet the caller as a San Francisco local…"
: "You are a friendly customer support agent…";
return res.json({
prompt,
voice: "john",
product: "spark",
});
}
res.json({});
},
);Antwort mit Funktionswerkzeugen
Fügen Sie Werkzeuge hinzu, damit die KI Ihre APIs während des Gesprächs aufrufen kann:
{
"prompt": "You are a booking assistant. Use the available tools to help customers schedule appointments.",
"voice": "john",
"product": "spark",
"tools": [
{
"type": "function",
"function": {
"name": "search_appointments",
"description": "Find available appointment slots",
"parameters": {
"type": "object",
"properties": {
"date": { "type": "string", "description": "YYYY-MM-DD" },
"service": { "type": "string" }
},
"required": ["date"]
}
},
"endpoint": {
"url": "https://api.example.com/appointments/search",
"method": "POST",
"headers": {
"X-Api-Key": "your-key"
}
}
}
]
}Produktstufen auf einen Blick
| Produkt | Latenz | Schlussfolgern | Bestätigung |
|---|---|---|---|
spark | Niedrigste | Einfach | — |
bolt | Niedrig | Verbessert | — |
storm-base | Mittel | Stark | — |
storm-base-with-ack | Mittel | Stark | Automatischer Fülltext während des Nachdenkens |
storm-extra | Höher | Tiefgehend | — |
storm-extra-with-ack | Höher | Tiefgehend | Automatischer Fülltext während des Nachdenkens |
Verwandt
Das nicht blockierende Ereignis am Ende eines Anrufs.
Vollständiges JSON-Schema für tools[] und den Vertrag für signierte Endpunkte.
Mehrere URLs für telephony.incoming / web.incoming abonnieren.
Muster für anruferspezifische Prompts, Tools und A/B-Tests.