telephony.incoming / web.incoming
Webhook bloqueante que configura una llamada entrante en tiempo real.
Cuando una llamada telefónica entrante llega a un número sin un
agente asignado, o una sesión del widget web comienza con una clave publicable en
mode="webhook", ThunderPhone envía una solicitud bloqueante
telephony.incoming / web.incoming a tu
URL de webhook heredada
y espera hasta 10 segundos una respuesta de configuración. Usa este
intercambio para elegir dinámicamente un prompt, una voz y herramientas por llamada:
consulta la guía de configuración dinámica de llamadas
para conocer el patrón integral.
El intercambio bloqueante no tiene alternativa: si tu controlador devuelve un
estado distinto de 2xx, agota el tiempo de espera o devuelve una configuración que
no supera la validación, la llamada se rechaza (la llamada telefónica no se
conecta; la solicitud de sesión del widget falla con 502/422). Responde
rápido: quien llama escucha el tono de llamada mientras decides.
Carga útil de la solicitud
Para llamadas telefónicas (telephony.incoming):
{
"type": "telephony.incoming",
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
}
}| Campo | Tipo | Descripción |
|---|---|---|
call_id | integer | ID de llamada: se mantiene estable en todos los eventos de esta llamada |
from_number | string | Número E.164 de quien llama |
to_number | string | Destino E.164 (uno de tus números de ThunderPhone) |
Para sesiones del widget web (web.incoming), data identifica la
página de inserción en lugar de números telefónicos:
{
"type": "web.incoming",
"data": {
"call_id": 987654322,
"origin_domain": "https://example.com",
"publishable_key_prefix": "pk_live_a1b2"
}
}| Campo | Tipo | Descripción |
|---|---|---|
call_id | integer | ID de llamada |
origin_domain | string | El origen de la página que aloja el widget |
publishable_key_prefix | string | Primeros caracteres de la clave publicable que abrió la sesión |
language, primary_language | string | Se incluye cuando la sesión del widget solicitó una anulación de idioma |
voice | string | Se incluye cuando la sesión del widget solicitó una anulación de voz |
website_context | string | Se incluye cuando el widget pasó contexto de página por sesión |
Esquema de respuesta
Devuelve un objeto JSON que describa la configuración del agente para esta llamada.
prompt y voice son obligatorios; todo lo demás es opcional.
{
"prompt": "You are a helpful booking assistant for Acme Restaurant.",
"voice": "john",
"product": "spark",
"background_track": null,
"tools": []
}| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
prompt | cadena | sí | Prompt del sistema que dirige al agente |
voice | cadena | sí | ID de voz de GET /v1/voices, por ejemplo, john. Se acepta voice_name como alias. Las voces desconocidas no superan la validación y rechazan la llamada |
product | cadena | no | El valor predeterminado es spark. Permitidos: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack |
thinking_level | cadena | no | minimal, base (predeterminado) o extra. Se reemplaza para productos Storm: storm-extra* fuerza extra; los demás storm-* fuerzan base |
audio_context_mode | cadena | no | full (predeterminado) o reduced |
watchdog_enabled | booleano | no | Activa la supervisión para esta llamada. El valor predeterminado es false |
additional_audio_context | booleano | nulo | no | Incluye los últimos turnos de audio de quien llama en lugar de solo el turno más reciente, lo que mejora las correcciones y la recopilación de datos con mucha ortografía o números, con un pequeño impacto en la latencia y el costo. Se activa de forma predeterminada para sesiones entrantes y se desactiva para llamadas telefónicas salientes; null conserva el valor predeterminado |
storm_feedback_mode | cadena | no | none, acknowledgement (predeterminado) o tick |
language | cadena | no | Abreviatura de primary_language |
primary_language | cadena | no | Código de idioma, normalizado (predeterminado en). Los códigos que no se pueden resolver rechazan la llamada |
has_additional_languages | booleano | no | El valor predeterminado es false |
additional_languages | arreglo de cadenas | no | Idiomas adicionales a los que el agente puede cambiar |
native_voice_switching | booleano | no | El valor predeterminado es false. Cuando la llamada cambia a otro idioma, cambia a una voz nativa de ese idioma (según el género) en lugar de conservar la voz configurada |
background_track | cadena | nulo | no | ID de audio ambiental o null |
acknowledgement_prompt_mode | cadena | no | auto (predeterminado) o manual (productos Storm con confirmación) |
acknowledgement_prompt | cadena | no | Se usa cuando acknowledgement_prompt_mode="manual" |
silence_interval_seconds | entero | nulo | no | 5–120. Segundos de silencio de quien llama antes de una verificación |
silence_max_checkins | entero | nulo | no | 1–10 |
silence_checkins_enabled | booleano | no | El valor predeterminado es true |
connect_tone_enabled | booleano | no | El valor predeterminado es false |
voicemail_action | cadena | no | prompt (predeterminado), hangup o message |
voicemail_message | cadena | no | Se usa cuando voicemail_action="message" |
agent_name | cadena | no | Nombre visible que se muestra en los paneles y el widget |
org_name | cadena | no | Nombre visible de la organización para la personalidad del agente |
tools | arreglo | no | Esquemas de herramientas de función en línea (consulta Herramientas de función) |
call_id | entero | no | Eco opcional del ID de llamada de la solicitud; se ignora |
Como prompt y voice son obligatorios, devolver {} o cualquier
respuesta que no supere la validación rechaza la llamada con 422: no existe
respaldo de agente estático en esta ruta (un número o clave en modo webhook
no tiene un agente asignado).
Límite de tamaño de respuesta
Controlador de ejemplo
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({});
},
);Respuesta con herramientas de función
Adjunta herramientas para que la IA pueda llamar a tus API durante la conversación:
{
"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"
}
}
}
]
}Guía rápida de niveles de producto
| Producto | Latencia | Razonamiento | Confirmación |
|---|---|---|---|
spark | La más baja | Básico | — |
bolt | Baja | Mejorado | — |
storm-base | Media | Sólido | — |
storm-base-with-ack | Media | Sólido | Texto de relleno automático mientras piensa |
storm-extra | Más alta | Profundo | — |
storm-extra-with-ack | Más alta | Profundo | Texto de relleno automático mientras piensa |
Relacionado
El evento no bloqueante de finalización de llamada.
Esquema JSON completo para tools[] y el contrato de endpoint firmado.
Suscribe varias URL a telephony.incoming / web.incoming.
Patrones para prompts, herramientas y pruebas A/B por persona que llama.