Open in
Configuración dinámica por llamada
Elige el agente que responde, o reescribe su prompt y configuración, de forma independiente para cada llamada entrante, mediante lógica personalizada en un webhook que controlas.
De forma predeterminada, a cada número de teléfono y clave publicable se le asigna un agente estático. Cuando necesitas personalización por cada persona que llama o por cada visitante — enrutamiento VIP, contexto de usuarios con sesión iniciada, pruebas A/B de prompts — cambia al modo webhook y deja que tu servidor decida.
Cómo funciona
- Te suscribes al evento
telephony.incoming(teléfono) oweb.incoming(widget). Ambos son webhooks bloqueantes: ThunderPhone espera tu respuesta antes de continuar la llamada, hasta 13 segundos para una llamada telefónica (incluido un reintento) y 10 segundos para una sesión de widget. - ThunderPhone te envía
{call_id, from_number, to_number}(las sesiones de widget incluyen campos específicos del widget en lugar de números; consulta el esquema de solicitud). - Tu servidor responde con una configuración del agente (prompt, voz, producto, herramientas). ThunderPhone usa esa configuración para la llamada.
- No hay un agente de respaldo. Si devuelves
{}o una configuración no válida, o si la solicitud sigue fallando después de su reintento, la llamada telefónica finaliza de inmediato y se registra conend_reasoninbound_config_webhook_failed; la solicitud de una sesión de widget falla.
1. Configura el destino del webhook
Para números de teléfono, suscribe tu endpoint a 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"]
}'La respuesta incluye un secret de una sola vez; guárdalo, ya que lo usarás
para verificar la firma.
Para sesiones del widget, crea una clave publicable en mode="webhook"
con la URL de tu endpoint integrada:
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"]
}'El widget hará POST a esta URL al inicio de cada sesión.
2. Implementa el controlador
Tres reglas generales:
- Verifica la firma en cada solicitud (consulta Verificar firmas de webhooks). No omitas esto en desarrollo: hazlo bien una vez y reutilízalo.
- Responde rápido. El primer intento de una llamada telefónica se corta a los 10 segundos y todo el intercambio a los 13; cada segundo es silencio para quien llama. Haz consultas a la base de datos si lo necesitas, pero no llames a LLM posteriores de forma síncrona: si quieres generación dinámica de prompts, precalcula y almacena en caché.
- Usa una alternativa de forma limpia. Para cualquier estado inesperado, responde con un
prompty unavoicepredeterminados (o unagent_idguardado). Devolver{}o un estado de error termina la llamada.
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. Esquema de respuesta
El cuerpo de la respuesta coincide exactamente con el esquema de respuesta de llamadas entrantes. Los campos de uso común:
| Campo | Tipo | Descripción |
|---|---|---|
prompt | cadena (obligatorio) | Prompt del sistema para el agente |
voice | cadena (obligatorio) | ID de voz de GET /v1/voices |
product | cadena | El valor predeterminado es spark. Los productos experimentales solo están disponibles para organizaciones autorizadas. |
background_track | cadena | nulo | ID de audio ambiental |
acknowledgement_prompt_mode | cadena | auto o manual (solo Storm con confirmación) |
acknowledgement_prompt | cadena | Obligatorio cuando el modo es manual |
tools | matriz | Esquemas de herramientas de función en línea; consulta Herramientas de función |
Conserva un agente guardado y proporciona variables
Devuelve {"agent_id": 12, "variables": {"name": "Ada"}} para usar el agente
guardado de esa organización con datos por llamada. Su prompt puede contener {{name}} o
{{name|Friend}}. Las variables del webhook se combinan sobre las variables a nivel de solicitud; null
usa el valor predeterminado del marcador de posición o texto vacío si no se proporciona ninguno. Los valores
finales y los nombres sin resolver aparecen en los detalles de la llamada y en los webhooks de finalización.
Las respuestas de agente guardado solo aceptan agent_id, variables y timezone. Si prompt está
presente, la respuesta usa configuración integrada e ignora agent_id (incluidos
metadatos null o que no sean enteros); el prompt integrado debe seguir siendo válido. Las respuestas de configuración
integrada también pueden incluir variables. Las respuestas de agente guardado usan la división A/B implementada del agente tanto en llamadas
telefónicas como de widget y luego procesan las variables. Consulta variables de llamada
para conocer los límites y la compatibilidad con la API de sesión. La configuración de bloqueo proviene de la URL heredada
del número de teléfono o de la organización, o de una clave de widget en modo webhook; los eventos entrantes del sistema de endpoints
son solo notificaciones.
Configura la zona horaria por llamada
Un agente puede responder a contactos en distintas zonas horarias. Agrega timezone, un
nombre de IANA, a una respuesta de agente guardado o en línea, y el agente indicará la fecha
y hora y ofrecerá horarios en esa zona para esta llamada, antes que en la propia zona horaria
del agente:
{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}Envía la zona en lugar de una marca de tiempo en variables: el reloj del agente sigue
avanzando durante la llamada, y una hora que envíes queda desactualizada. Se ignora un valor
que no sea una zona IANA y la llamada se conecta en la zona del agente.
Patrones
Contexto de usuario con sesión iniciada
En los widgets en modo webhook, la página del visitante ya sabe quién es.
Llama a tu webhook con un parámetro de cadena de consulta que el SDK del widget
reenvía (?customer_id=123) y busca al cliente en el servidor.
Implementación gradual de prompts A/B
Antes de implementar esto manualmente, ten en cuenta que ThunderPhone cuenta con
una función nativa de
Experimentos
(/dashboard/experiments y la pestaña A/B del creador de agentes) que
define variantes, divide el tráfico y compara los resultados por variante —
sin necesidad de webhook.
Si de todos modos necesitas control desde el webhook: aplica hash a call_id →
bucket; sirve el prompt A para 0..49 y el prompt B para 50..99. Registra el
bucket que elegiste en tu propia base de datos y luego correlaciónalo con la
calificación de la llamada completada.
Enrutamiento basado en horario
Horario comercial → agente de "soporte en vivo"; fuera del horario comercial →
agente de "tomar un mensaje". Cambio simple según new Date().getUTCHours() en
tu controlador.
Próximos pasos
Esquemas exactos de solicitud y respuesta, incluida cada clave de configuración.
Configura correctamente el HMAC una vez; reutilízalo en todas partes.
Combina el enrutamiento dinámico con herramientas por agente.
Reintentos, orden, tiempos de espera.