Open in
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 de widget web comienza con una clave publicable en
mode="webhook", ThunderPhone envía una solicitud bloqueante de
telephony.incoming / web.incoming a tu
URL de webhook heredada
y espera una respuesta de configuración: hasta 13 segundos para una
llamada telefónica, incluido un reintento, y hasta 10 segundos para una
sesión de widget (consulta Tiempos de espera y reintentos). Usa este
intercambio para elegir dinámicamente un prompt, una voz y herramientas para cada llamada;
consulta la guía de configuración dinámica de llamadas
para conocer el patrón integral.
Tiempos de espera y reintentos
Responde rápido: la persona que llama está esperando en la línea mientras decides.
Para una llamada telefónica (telephony.incoming):
- El primer intento puede tardar hasta 10 segundos.
- Si se agota el tiempo de espera, no se puede conectar o recibe una
respuesta
5xx,408o429, ThunderPhone lo intenta una vez más. El reintento recibe lo que quede de un presupuesto de 13 segundos para toda la interacción: aproximadamente 3 segundos después de un primer intento que agotó el tiempo de espera, y casi los 13 después de una falla rápida. No hay reintento cuando quedarían menos de 2 segundos. - Cualquier otra respuesta
4xx, una respuesta que supere el límite de tamaño, o una respuesta2xxcuyo cuerpo no sea una configuración válida es definitiva y no se reintenta. - Ambos intentos envían el mismo cuerpo, la misma cabecera
X-ThunderPhone-Signature, y la misma cabeceraX-ThunderPhone-Idempotency-Key(telephony-incoming-<call_id>), por lo que un controlador con efectos secundarios puede reconocer el reintento.
No hay un agente de respaldo. Si no llega una configuración utilizable, la
llamada finaliza de inmediato en lugar de dejar a la persona que llama en una
línea silenciosa. Aparece en el historial de llamadas como una llamada fallida sin facturar con
end_reason inbound_config_webhook_failed, y se envía el evento habitual
telephony.complete.
Una sesión de widget (web.incoming) recibe un solo intento de hasta 10
segundos. Si falla, la solicitud de sesión falla con 502 o 422.
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, tal como lo envió el navegador. Un cliente que no sea un navegador puede enviar cualquier valor, así que no lo trates como prueba de dónde provino la solicitud |
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 |
Los campos de anulación (language, voice, website_context y cualquier
variables del navegador) llegan solo cuando la clave publicable los permite en
sus anulaciones del navegador. Una
solicitud de sesión que incluya una anulación que la clave no permita se rechaza antes de que
se llame a este webhook.
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 guía 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 pasan 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 anula para los 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 costo adicional de latencia y costo. Se activa de forma predeterminada para las sesiones entrantes y se desactiva para las 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 (que coincida en género) en lugar de conservar la voz configurada |
language_switching | cadena | nulo | no | ask_first (predeterminado) o follow_caller; null equivale a ask_first. Define cómo un agente multilingüe gestiona que la otra persona hable otro de sus idiomas. ask_first cambia de inmediato cuando solicitan el idioma o dicen una oración completa en él, y pregunta si desea continuar en ese idioma después de algo breve, como un "sí". follow_caller cambia a cualquier idioma configurado que hablen, incluso una sola palabra. Solo se aplica cuando se configura additional_languages |
background_track | cadena | nulo | no | ID de audio ambiental o null |
acknowledgement_prompt_mode | cadena | no | auto (predeterminado) o manual (productos Storm con ack) |
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 comprobació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 |
pronunciation_dicts | booleano | nulo | no | El valor predeterminado es false; null equivale a false. Cuando es true, la síntesis de voz de Cartesia de la llamada usa el diccionario de pronunciación de ThunderPhone para el idioma de la llamada, cuando existe. Funciona para llamadas telefónicas y de widget. Solo se aplica a las voces Cartesia propias de ThunderPhone, no a las voces de tu propia clave de API de Cartesia. No se usa con agent_id, donde se aplican los ajustes del agente guardado |
voicemail_action | cadena | no | prompt (predeterminado), hangup o message |
voicemail_message | cadena | no | Se usa cuando voicemail_action="message" |
agent_name | cadena | no | Nombre para mostrar que se informa a los paneles y al widget |
org_name | cadena | no | Nombre para mostrar 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) |
transfer_destinations | arreglo de objetos | nulo | no | Hasta 50 entradas {name, number} a las que esta llamada puede transferirse, validadas como los números de transferencia de un agente. El agente transfiere por nombre y solo a estas; [] significa que no hay transferencias. Omítelo o envía null para permitir que el agente transfiera a un número que elija |
timezone | cadena | no | Zona horaria IANA para esta llamada, como America/Chicago. El agente indica la fecha y hora en ella, en lugar de usar la zona horaria propia del agente. También se permite junto a agent_id. Se ignoran los valores que no son zonas IANA. Consulta Configura la zona horaria por llamada |
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 pase la validación rechaza la llamada: una llamada telefónica finaliza
como se describe en Tiempos de espera y reintentos, y una
solicitud de sesión del widget falla con 422. No hay alternativa 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.