---
title: "Configuración dinámica por llamada"
description: "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

1. Te suscribes al evento [`telephony.incoming`](/es/webhooks/events)
   (teléfono) o [`web.incoming`](/es/webhooks/events) (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.
2. 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](/es/webhooks/call-incoming)).
3. Tu servidor responde con una configuración del agente (prompt, voz,
   producto, herramientas). ThunderPhone usa esa configuración para la llamada.
4. 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](/es/webhooks/call-incoming), la llamada
   telefónica finaliza de inmediato y se registra con `end_reason`
   `inbound_config_webhook_failed`; la solicitud de una sesión de widget falla.

<Note>
  Funciona de forma idéntica para llamadas telefónicas (`telephony.incoming`) y sesiones
  de widget (`web.incoming`), ya sea que se entreguen a un endpoint de webhook
  o al webhook heredado de URL única.
</Note>

<Warning>
  **No se reproduce ningún aviso de consentimiento de ThunderPhone en llamadas
  configuradas en línea mediante webhook.** Esta ruta omite el aviso de inicio de llamada
  del agente y está expresamente excluida del marco de avisos de consentimiento
  de ThunderPhone (Términos de servicio, sección "Grabación y consentimiento"). Tu
  organización es responsable de todos los avisos y consentimientos de grabación,
  monitoreo, participación de IA e identificación de quienes llaman en estas llamadas. Inclúyelos
  en tu propio flujo — por ejemplo, en el guion inicial del prompt —
  antes de habilitar el modo webhook en línea. Una respuesta de webhook que haga referencia a un
  `agent_id` guardado utiliza la política habitual de grabación y divulgación de ese agente.
</Warning>

## 1. Configura el destino del webhook

<Tabs>
<Tab title="Llamadas telefónicas">
Para números de teléfono, suscribe tu endpoint a `telephony.incoming`:

```bash
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.
</Tab>
<Tab title="Widget web">
Para sesiones del widget, crea una clave publicable en `mode="webhook"`
con la URL de tu endpoint integrada:

```bash
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.
</Tab>
</Tabs>

## 2. Implementa el controlador

Tres reglas generales:

- **Verifica la firma** en cada solicitud (consulta
  [Verificar firmas de webhooks](/es/guides/verify-webhook-signatures)).
  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
  `prompt` y una `voice` predeterminados (o un `agent_id` guardado). Devolver `{}`
  o un estado de error termina la llamada.

<CodeGroup>
```python 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
```

```javascript 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
  },
);
```
</CodeGroup>

## 3. Esquema de respuesta

El cuerpo de la respuesta coincide exactamente con el
[esquema de respuesta de llamadas entrantes](/es/webhooks/call-incoming).
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`](/api-reference/agents#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](/es/tools/overview) |

<Note>
  El orden de habla por llamada y `max_hold_seconds` no están disponibles en la
  respuesta del webhook. Configúralos en el
  [agente](/api-reference/agents) al que haces referencia.
</Note>

### 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](/es/guides/call-variables)
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:

```json
{"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](/es/guides/concepts)
(`/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

<CardGroup cols={2}>
  <Card title="Referencia del webhook de llamadas entrantes" icon="phone" href="/es/webhooks/call-incoming">
    Esquemas exactos de solicitud y respuesta, incluida cada clave de configuración.
  </Card>
  <Card title="Verifica las firmas de webhook" icon="shield-check" href="/es/guides/verify-webhook-signatures">
    Configura correctamente el HMAC una vez; reutilízalo en todas partes.
  </Card>
  <Card title="Crea una integración de herramientas" icon="screwdriver-wrench" href="/es/guides/build-tool-integration">
    Combina el enrutamiento dinámico con herramientas por agente.
  </Card>
  <Card title="Semántica de entrega" icon="bolt" href="/es/webhooks/overview">
    Reintentos, orden, tiempos de espera.
  </Card>
</CardGroup>
