---
title: "telephony.incoming / web.incoming"
description: "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](/api-reference/organizations#legacy-single-url-webhook)
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](#timeouts-and-retries)). 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](/es/guides/dynamic-call-config)
para conocer el patrón integral.

<Note>
  Los [endpoints de webhook](/es/webhooks/endpoints) suscritos también reciben
  `telephony.incoming` / `web.incoming` — para **cada** llamada entrante
  y sesión web, con agente configurado o no — pero esas entregas son
  notificaciones de envío sin espera con un `event_id`, nunca bloqueantes.
  Solo el webhook heredado de URL única contiene el intercambio de
  configuración de esta página. Las estructuras de las notificaciones de endpoints
  están en el [catálogo de eventos](/es/webhooks/events).
</Note>

## 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`, `408` o `429`, 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](#response-size-limit), o una respuesta `2xx` cuyo 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 cabecera `X-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`](/es/webhooks/call-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`.

<Warning>
  **Las llamadas configuradas mediante webhook no incluyen un anuncio de
  consentimiento de ThunderPhone.** Las llamadas configuradas mediante este
  intercambio omiten el anuncio de inicio de llamada a nivel de agente y quedan
  expresamente excluidas del marco de anuncios de consentimiento de ThunderPhone
  (Términos de servicio, sección "Grabación y consentimiento"). Tu organización
  es la única responsable de cada aviso y consentimiento de grabación,
  monitoreo, participación de IA e identificación de quienes llaman que se
  requiera en estas llamadas — las llamadas aún pueden grabarse, transcribirse,
  analizarse y ser atendidas por IA. Incorpora las divulgaciones requeridas en
  tu propio flujo de llamadas antes de habilitar esta ruta.
</Warning>

## Carga útil de la solicitud

Para llamadas telefónicas (`telephony.incoming`):

```json
{
  "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:

```json
{
  "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](/es/guides/embed-web-widget). 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.

<Note>
  Los widgets en modo webhook entregan esta solicitud al `webhook_url` propio
  de la clave publicable cuando está configurado, y recurren a la URL de webhook
  de nivel de organización como alternativa. En ambos casos, se firma con el
  `secret` del webhook de la organización.
</Note>

---

## 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.

```json
{
  "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`](/api-reference/agents#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](/es/tools/overview)) |
| `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](/api-reference/agents#transfer-numbers) 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](/es/guides/dynamic-call-config) |
| `call_id` | entero | no | Eco opcional del ID de llamada de la solicitud; se ignora |

<Note>
  Las claves de nivel superior desconocidas se **ignoran** silenciosamente: un nombre
  de campo con un error tipográfico no rechaza la configuración, simplemente no se aplica. El orden de habla
  y `max_hold_seconds` no se aceptan aquí; solo se pueden
  configurar en el propio [agente](/api-reference/agents).
</Note>

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](#timeouts-and-retries), 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

<Warning>
  Las respuestas de configuración están limitadas a **5 MiB**. Si un controlador
  devuelve una respuesta más grande, incluso con un estado `2xx`,
  ThunderPhone informa que la respuesta excedió el límite y
  rechaza la llamada o la sesión del widget. Limita la respuesta a los campos
  necesarios para configurar la llamada; aloja los datos grandes detrás de herramientas de función u
  otro servicio en lugar de incorporarlos en la configuración.
</Warning>

---

## Controlador de ejemplo

<CodeGroup>
```python Python (FastAPI)
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 {}
```

```javascript Node.js (Express)
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({});
  },
);
```
</CodeGroup>

---

## Respuesta con herramientas de función

Adjunta herramientas para que la IA pueda llamar a tus API durante la conversación:

```json
{
  "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"
        }
      }
    }
  ]
}
```

<Tip>
  Las solicitudes al endpoint de herramientas se firman con el **mismo secreto
  de webhook de la organización** que firmó este intercambio. Consulta
  [Herramientas de función](/es/tools/overview) para conocer la estructura exacta y el formato de
  solicitud firmada.
</Tip>

---

## 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

<CardGroup cols={2}>
  <Card title="telephony.complete / web.complete" icon="phone-slash" href="/es/webhooks/call-complete">
    El evento no bloqueante de finalización de llamada.
  </Card>
  <Card title="Herramientas de función" icon="screwdriver-wrench" href="/es/tools/overview">
    Esquema JSON completo para `tools[]` y el contrato de endpoint firmado.
  </Card>
  <Card title="Endpoints de webhook" icon="bolt" href="/es/webhooks/endpoints">
    Suscribe varias URL a `telephony.incoming` / `web.incoming`.
  </Card>
  <Card title="Configuración dinámica de llamadas" icon="wand-magic-sparkles" href="/es/guides/dynamic-call-config">
    Patrones para prompts, herramientas y pruebas A/B por persona que llama.
  </Card>
</CardGroup>
