ThunderPhone 2.0 ya está disponible.Empieza por tu cuenta desde 2¢/min.Lee el anuncio

Webhooks

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

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"
  }
}
CampoTipoDescripción
call_idintegerID de llamada: se mantiene estable en todos los eventos de esta llamada
from_numberstringNúmero E.164 de quien llama
to_numberstringDestino 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"
  }
}
CampoTipoDescripción
call_idintegerID de llamada
origin_domainstringEl 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_prefixstringPrimeros caracteres de la clave publicable que abrió la sesión
language, primary_languagestringSe incluye cuando la sesión del widget solicitó una anulación de idioma
voicestringSe incluye cuando la sesión del widget solicitó una anulación de voz
website_contextstringSe 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":   []
}
CampoTipoObligatorioDescripción
promptcadenasíPrompt del sistema que guía al agente
voicecadenasí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
productcadenanoEl valor predeterminado es spark. Permitidos: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack
thinking_levelcadenanominimal, base (predeterminado) o extra. Se anula para los productos Storm: storm-extra* fuerza extra; los demás storm-* fuerzan base
audio_context_modecadenanofull (predeterminado) o reduced
watchdog_enabledbooleanonoActiva la supervisión para esta llamada. El valor predeterminado es false
additional_audio_contextbooleano | nulonoIncluye 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_modecadenanonone, acknowledgement (predeterminado) o tick
languagecadenanoAbreviatura de primary_language
primary_languagecadenanoCódigo de idioma normalizado (predeterminado: en). Los códigos que no se pueden resolver rechazan la llamada
has_additional_languagesbooleanonoEl valor predeterminado es false
additional_languagesarreglo de cadenasnoIdiomas adicionales a los que el agente puede cambiar
native_voice_switchingbooleanonoEl 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_switchingcadena | nulonoask_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_trackcadena | nulonoID de audio ambiental o null
acknowledgement_prompt_modecadenanoauto (predeterminado) o manual (productos Storm con ack)
acknowledgement_promptcadenanoSe usa cuando acknowledgement_prompt_mode="manual"
silence_interval_secondsentero | nulono5–120. Segundos de silencio de quien llama antes de una comprobación
silence_max_checkinsentero | nulono1–10
silence_checkins_enabledbooleanonoEl valor predeterminado es true
connect_tone_enabledbooleanonoEl valor predeterminado es false
pronunciation_dictsbooleano | nulonoEl 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_actioncadenanoprompt (predeterminado), hangup o message
voicemail_messagecadenanoSe usa cuando voicemail_action="message"
agent_namecadenanoNombre para mostrar que se informa a los paneles y al widget
org_namecadenanoNombre para mostrar de la organización para la personalidad del agente
toolsarreglonoEsquemas de herramientas de función en línea (consulta Herramientas de función)
transfer_destinationsarreglo de objetos | nulonoHasta 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
timezonecadenanoZona 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_identeronoEco 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

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 {}
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({});
  },
);

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

ProductoLatenciaRazonamientoConfirmación
sparkLa más bajaBásico—
boltBajaMejorado—
storm-baseMediaSólido—
storm-base-with-ackMediaSólidoTexto de relleno automático mientras piensa
storm-extraMás altaProfundo—
storm-extra-with-ackMás altaProfundoTexto de relleno automático mientras piensa

Relacionado