Salta al contenido

Crea un agente de voz en 20 minutos con ElevenLabs y Twilio

Publicado

EscucharEscucha este artículo

Un agente de voz puede responder llamadas entrantes, transcribir en tiempo real con voz a texto (STT), generar respuestas con un modelo de lenguaje (LLM) y responder con un módulo de texto a voz (TTS). Con ElevenLabs y Twilio, puedes tener un agente funcionando en un número real en unos 20 minutos.

Como desarrollador, el stack completo que vas a usar es ElevenLabs para síntesis de voz (Flash v2.5) y transcripción (Scribe v2 Realtime), Twilio para telefonía y OpenAI o Anthropic como LLM. Todos estos componentes son intercambiables, así que puedes elegir los que mejor conozcas y usarlos en su lugar.

En este artículo verás cómo crear un agente de voz en 20 minutos, usando Node.js y Typescript. Si prefieres una alternativa gestionada que se encargue de los turnos, interrupciones y telefonía sin tener que mantener la cascada tú mismo, entra en ElevenAgents. 

Cómo funciona la arquitectura de un agente de voz

Antes de escribir código, ayuda entender cómo se conectan los tres servicios de tu stack tecnológico.

  • Twilio: gestiona la llamada y el transporte de audio.
  • ElevenLabs: gestiona STT con Scribe v2 Realtime y TTS con Flash v2.5.
  • LLM: se encarga de llamar herramientas y generar la respuesta.

Cada etapa es un adaptador sencillo, por eso puedes cambiar un servicio por otro sin tocar el resto. Por ejemplo, puedes cambiar el LLM de OpenAI por Anthropic sin tener que reescribir los demás componentes.

Una llamada llega a tu servidor a través de Twilio. Twilio responde la llamada PSTN, abre un WebSocket hacia tu servidor y reenvía el audio del llamante como un flujo de frames mu-law codificados en base64. Tu servidor ejecuta la cascada y envía el audio sintetizado de vuelta por ese mismo WebSocket, y Twilio lo reproduce al llamante.

Build a voice agent diagram of a call processing system using Twilio for speech-to-text conversion and LLM for response.

Este es el flujo que vas a usar para crear un agente de voz:

Un llamante marca tu número de Twilio. Twilio obtiene un documento TwiML de tu webhook. El TwiML indica a Twilio que abra un Media Stream hacia tu endpoint WebSocket. Twilio envía el audio entrante como eventos JSON con cargas mu-law base64 (ulaw_8000).

Tu servidor reenvía los fragmentos de audio a Scribe v2 Realtime para la transcripción en streaming. Cuando se finaliza el turno del llamante, envías la transcripción al LLM y luego sintetizas la respuesta con Flash v2.5 en ulaw_8000. Envías los frames mu-law sintetizados de vuelta a Twilio por el WebSocket, codificados en base64, y Twilio los reproduce al llamante.

Scribe v2 Realtime emite transcripciones parciales con unos 150ms de latencia, y Flash v2.5 funciona con unos 75ms de inferencia de modelo, sin contar la latencia de red y aplicación. El LLM es el componente más grande y menos predecible en el tiempo hasta el primer audio, y ahí es donde va la mayor parte del presupuesto de latencia. Para acortar la espera, transmitimos la salida del LLM token a token y empezamos a sintetizar antes de que termine la frase.

Para ver los motivos detrás de estas elecciones, consulta la visión general de modelos y la explicación sobre cómo funciona la latencia.

Qué necesitas antes de crear un agente de voz

Esta guía asume que tienes cuatro cosas preparadas. Cada una se configura rápido, pero si falta alguna, el servidor no funcionará.

Estos son los requisitos previos que debes comprobar:

  1. Un número de Twilio con capacidad de Voz: Apunta el número junto con tu Account SID y Auth Token desde la consola de Twilio.
  2. Clave de API de ElevenLabs: Créala en tu panel de ElevenLabs. La clave viaja en la cabecera xi-api-key y es secreta, así que mantenla solo en el servidor. Consulta la autenticación de la API.
  3. Clave de API del LLM:Este tutorial trata Anthropic Claude y OpenAI como backends intercambiables, así que elige uno.
  4. Ngrok (o cualquier túnel) para desarrollo local: Twilio necesita acceder a tu servidor por una URL pública HTTPS y WSS, y ngrok lo facilita sin desplegar nada.

Configura tus secretos como variables de entorno y nunca los subas.

export ELEVENLABS_API_KEY="..."
export ANTHROPIC_API_KEY="..."          # or OPENAI_API_KEY
export TWILIO_AUTH_TOKEN="..."          # used for webhook signature validation
export PUBLIC_HOST="your-subdomain.ngrok.app"

Luego abre un túnel apuntando al puerto que usará tu servidor:

ngrok http 8080

Cómo funciona el protocolo Twilio Media Streams

Twilio no te da un socket de audio en bruto. En su lugar, envuelve todo en un protocolo JSON estructurado sobre WebSocket. Entender los cuatro tipos de eventos y el formato de envío hará que el handler WebSocket del Paso 2 tenga sentido antes de escribirlo.

Cuando Twilio conecta con tu WebSocket, envía una secuencia de mensajes de texto JSON, que pueden ser de cuatro tipos de evento.

El evento connected llega primero y confirma que el WebSocket está activo. El evento start se envía una vez cuando empieza el media stream; lleva un streamSid que debes guardar, ya que lo necesitas para enviar audio de vuelta, e incluye metadatos de la llamada en start.customParameters y start.callSid.

El evento media es recurrente: media.payload es un fragmento de audio mu-law a 8kHz codificado en base64, 20ms por frame, y media.track es inbound para el audio del llamante. Por último, stop se envía cuando termina el stream, normalmente porque se ha colgado la llamada.

Para reproducir audio, envía un mensaje de tipo media con el mismo streamSid y una carga mu-law en base64. Para interrumpir audio ya encolado, envía un mensaje clear con el streamSid, que vacía el buffer de salida de Twilio.

Las codificaciones de entrada y salida son idénticas (ulaw_8000). Pedimos ulaw_8000 a ElevenLabs Texto a Voz y reenviamos los bytes directamente a Twilio sin hacer resampling.

Paso 1: Sirve el webhook TwiML

Cuando llega una llamada, Twilio hace una petición HTTP a tu webhook y tú respondes con TwiML que conecta la llamada a tu Media Stream. El verbo <Connect><Stream> abre un WebSocket bidireccional. Usa <Connect> en vez de <Start>: así la llamada sigue activa durante todo el stream y puedes enviar audio de vuelta, que es el objetivo de este sistema.

El TwiML que devuelve el webhook es:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Connect>
    <Stream url="wss://your-subdomain.ngrok.app/media" />
  </Connect>
</Response>

En Express, esto es un único handler POST que rellena el host y devuelve el documento:

// ... imports and app setup
app.post("/incoming-call", (_req, res) => {
  const twiml = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Connect>
    <Stream url="wss://${process.env.PUBLIC_HOST}/media" />
  </Connect>
</Response>`;
  res.type("application/xml").send(twiml);
});

En la consola de Twilio, pon el webhook de "A call comes in" del número en https://your-subdomain.ngrok.app/incoming-call usando HTTP POST.

Paso 2: Acepta el WebSocket del Media Stream

El handler WebSocket lee los eventos de Twilio, gestiona la cascada y escribe el audio de vuelta.

Guardamos un pequeño estado por llamada: el streamSid, una conexión STT y una bandera para saber si el agente está hablando. El handler decodifica cada frame de media entrante desde base64 y reenvía los bytes mu-law en bruto a STT:

import { WebSocketServer } from "ws";
// ... http server bound to the same port as Express

const wss = new WebSocketServer({ server, path: "/media" });

wss.on("connection", (ws) => {
  const state = { streamSid: null as string | null, agentSpeaking: false };

  ws.on("message", async (raw) => {
    const event = JSON.parse(raw.toString());
    switch (event.event) {
      case "start":
        state.streamSid = event.start.streamSid;
        await startSttSession(ws, state);
        break;
      case "media":
        await forwardToStt(Buffer.from(event.media.payload, "base64"), state);
        break;
      case "stop":
        await teardown(state);
        ws.close();
        break;
    }
  });
});

Paso 3: Transcribe con Scribe v2 Realtime

Scribe v2 Realtime acepta fragmentos de audio en streaming y devuelve transcripciones parciales y finales, y soporta codificación mu-law directamente, así que le pasamos los frames de Twilio tal cual.

También ofrece Detección de Actividad de Voz para segmentación por silencios y control manual para finalizar un segmento. Para un agente telefónico, la segmentación por VAD suele ser la mejor opción porque una pausa natural es la señal más fiable de que el turno del llamante ha terminado.

Los pasos son: abre un stream STT al empezar la llamada. Envía cada fragmento mu-law entrante. Cuando se finaliza una transcripción, invoca el LLM.

El cliente STT en tiempo real aún está evolucionando, así que la estructura de abajo se mantiene detrás de un pequeño adaptador TypeScript (openRealtimeStt) que implementas contra la API en vivo y no contra un conjunto fijo de campos. Usa onFinal como el hook que entrega un turno finalizado del llamante a la siguiente etapa.

async function startSttSession(ws, state) {
  // openRealtimeStt is a thin adapter over the realtime STT API:
  // model_id="scribe_v2_realtime", mu-law encoding, 8kHz, VAD on
  // so turns finalize on silence.
  const session = await openRealtimeStt({
    modelId: "scribe_v2_realtime",
    encoding: "ulaw",
    sampleRate: 8000,
  });
  state.stt = session;

  session.onFinal(async (text: string) => {
    if (text.trim()) await handleTurn(ws, state, text);
  });
}

async function forwardToStt(audioBytes, state) {
  if (state.stt) await state.stt.sendAudio(audioBytes);
}

La latencia de reconocimiento en tiempo real es de unos 150ms para parciales, lo que mantiene pequeño el hueco entre que el llamante termina y el agente empieza. Para la versión batch y todas las funciones, consulta la documentación de Voz a Texto y la página de producto de Voz a Texto en Tiempo Real.

Paso 4: Genera una respuesta con un LLM

Esta es la etapa que produce la respuesta. El LLM toma el historial de la conversación y devuelve el texto del asistente. Transmite la respuesta para poder empezar la síntesis con la primera frase.

Aquí está respaldado por OpenAI:

// ... client init: new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const SYSTEM_PROMPT =
  "You are a concise phone assistant. Keep replies to one or two sentences.";

async function llmReply(history) {
  const stream = await llm.chat.completions.create({
    model: "gpt-4.1-mini",
    stream: true,
    messages: [{ role: "system", content: SYSTEM_PROMPT }, ...history],
  });
  for await (const part of stream) {
    const token = part.choices[0]?.delta?.content;
    if (token) yield token; // incremental tokens
  }
}

El modelo de arriba, gpt-4.1-mini, es un ejemplo de baja latencia; claude-haiku-4-5 es una opción similar en Anthropic. Cualquiera de los dos puede usarse en el mismo contrato llmReply; cambia el cuerpo de la función y el resto del agente no cambia.

El prompt del sistema limita la longitud de la respuesta, lo cual es importante por teléfono: las respuestas largas se sienten lentas y son difíciles de interrumpir de forma natural.

Paso 5: Sintetiza con Flash TTS en ulaw_8000

Ahora el texto debe convertirse en audio que Twilio pueda reproducir. Solicita Flash v2.5 con outputFormat: "ulaw_8000" para que los bytes coincidan con la codificación que espera Twilio, luego transmite el audio y reenvía cada fragmento por el WebSocket como un evento media.

Agrupa los tokens del LLM en fragmentos del tamaño de una frase y sintetiza cada uno al completarse, en vez de esperar a la respuesta completa. Así reduces el tiempo hasta el primer audio porque el llamante escucha la primera frase mientras el modelo sigue generando la segunda. Para más control sobre la síntesis incremental, la guía de TTS en tiempo real por WebSocket muestra cómo enviar texto a un único socket de síntesis abierto; el enfoque HTTP streaming de abajo es más sencillo y suficiente para turnos cortos.

import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const eleven = new ElevenLabsClient(); // reads ELEVENLABS_API_KEY
const VOICE_ID = "JBFqnCBsd6RMkjVDRZzb"; // George, a default voice

async function speak(ws, state, text: string) {
  state.agentSpeaking = true;
  const stream = await eleven.textToSpeech.stream(VOICE_ID, {
    text,
    modelId: "eleven_flash_v2_5",
    outputFormat: "ulaw_8000",
  });
  for await (const chunk of stream) {
    if (!state.agentSpeaking) break; // interrupted by barge-in
    ws.send(
      JSON.stringify({
        event: "media",
        streamSid: state.streamSid,
        media: { payload: Buffer.from(chunk).toString("base64") },
      })
    );
  }
  state.agentSpeaking = false;
}

Prepara tu agente de voz IA para producción

Tras los cinco pasos anteriores, tienes un agente funcionando. Pero esto no es lo mismo que un despliegue en producción.

Hay varios aspectos que debes tener en cuenta antes de poner el agente en una línea real.

Valida las firmas del webhook de Twilio

Cualquiera que conozca la URL de tu webhook puede hacerle POST, así que lo primero es confirmar que la petición viene realmente de Twilio. Twilio firma cada petición con tu Auth Token en la cabecera X-Twilio-Signature, y debes rechazar cualquier petición que no pase la validación. La firma se calcula sobre la URL completa y los parámetros POST, así que debes calcularla igual que Twilio.

El helper de Twilio lo hace por ti:

import twilio from "twilio";

app.post("/incoming-call", express.urlencoded({ extended: false }), (req, res) => {
  const url = `https://${process.env.PUBLIC_HOST}/incoming-call`;
  const valid = twilio.validateRequest(
    process.env.TWILIO_AUTH_TOKEN!,
    req.header("X-Twilio-Signature") || "",
    url,
    req.body
  );
  if (!valid) return res.sendStatus(403);
  // ... return TwiML as before
});

Gestiona los secretos correctamente

Guarda ELEVENLABS_API_KEY, la clave del LLM y TWILIO_AUTH_TOKEN en un gestor de secretos, no en el código fuente ni en archivos env en texto plano subidos a un repo. Limita la clave de ElevenLabs solo a las rutas que necesita este servicio y ponle un cupo de crédito para que, si se filtra, el impacto sea limitado y no ilimitado.

Los planes Enterprise pueden además restringir una clave a rangos de IP concretos con listas blancas. Este servidor usa la clave de API directamente porque nunca sale de tu backend; si alguna lógica de audio pasara a navegador o app móvil, usarías tokens de un solo uso para que la clave nunca se exponga en el cliente.

Entiende el límite de concurrencia

Cada plan tiene un límite de concurrencia que varía según la familia de modelos, y el límite cuenta cuántas peticiones están generando audio al mismo tiempo.

Para un agente telefónico, esto juega a tu favor. La generación de audio es más rápida que la reproducción, así que cada llamada solo consume concurrencia TTS durante los breves momentos en que se está sintetizando una respuesta, no durante toda la llamada. Como referencia, un límite de concurrencia de cinco puede soportar unas 100 conversaciones simultáneas, porque la generación termina mucho antes que la reproducción.

Aun así, monitoriza el margen en vez de adivinar. Las respuestas de ElevenLabs incluyen las cabeceras current-concurrent-requests y maximum-concurrent-requests; regístralas y lanza alertas si te acercas al máximo. Si superas el límite, las peticiones se ponen en cola por prioridad, lo que suele añadir unos 50ms, y si la sobrecarga se mantiene, devuelve HTTP 429.

Gestiona las respuestas HTTP 429 con un pequeño backoff. Si persisten, sube los límites desde la página de precios o, si eres cliente Enterprise, a través de tu account manager.

Gestiona barge-in e interrupciones

Un llamante que empieza a hablar mientras el agente está hablando espera que el agente se detenga. Esto es barge-in, y gestionarlo bien es clave para que el agente suene natural y no robótico.

Detecta la voz del llamante durante la reproducción del agente usando la señal VAD de STT. Cuando la detectes, haz dos cosas. Primero, deja de enviar fragmentos TTS, que la bandera agentSpeaking en speak ya gestiona cortando el bucle. Segundo, envía a Twilio un mensaje clear para vaciar el audio ya encolado en su lado.

function interrupt(ws, state) {
  state.agentSpeaking = false;
  ws.send(JSON.stringify({ event: "clear", streamSid: state.streamSid }));
}

Si te saltas el clear, Twilio sigue reproduciendo el audio en buffer aunque hayas dejado de enviar, así que el agente parece hablar por encima del llamante.

Registra, monitoriza y gestiona errores de forma elegante

Instrumenta cada etapa para poder atribuir la latencia si una llamada parece lenta. Mide el tiempo desde la transcripción final al primer token del LLM, del primer token del LLM al primer byte TTS, y del primer byte TTS al frame enviado a Twilio. Verás que la mayor parte de la latencia variable está en la etapa LLM; las etapas STT y TTS son más estables.

Y prepárate para fallos parciales. El LLM puede agotar el tiempo, el stream STT puede caerse y el viaje de red a ElevenLabs varía entre 20 y 200ms según la geografía. Coloca tu servidor cerca de tus llamantes, no solo de ElevenLabs, ya que ElevenLabs ya enruta al nodo más cercano de Norteamérica, Europa o Sudeste Asiático.

Si una etapa falla, no dejes al llamante en silencio: sintetiza una frase corta de respaldo ("Perdona, ¿puedes repetir?") y mantén la llamada activa. Envuelve cada etapa en un timeout y un try/catch para que un fallo no cierre todo el WebSocket.

Hay algunos valores por defecto que merece la pena fijar antes de lanzar:

  • Limita la longitud de la respuesta en el prompt del sistema, como se muestra, para que los turnos sean cortos e interrumpibles.
  • Limita el historial de la conversación para que las llamadas largas no hagan crecer el contexto del LLM sin límite.
  • Pon una duración máxima de llamada como protección ante sesiones atascadas que consuman concurrencia.

Para seguir ajustando lo que sí puedes controlar, el documento de latencia explica de dónde viene el tiempo hasta el primer audio, la visión general de modelos cubre los compromisos entre velocidad y calidad, y la guía de TTS en tiempo real por WebSocket muestra cómo reducir la latencia de síntesis con entrada de texto incremental.

Crea agentes de voz listos para producción con ElevenAPI

Ahora que han pasado 20 minutos, tienes todas las capas de un agente de voz listo para producción. Twilio gestiona la telefonía, Scribe v2 Realtime transcribe, un LLM genera respuestas y Flash v2.5 responde por el mismo WebSocket.

Si prefieres no mantener la cascada tú mismo, ElevenAgents ofrece turnos, gestión de interrupciones e integración telefónica como servicio gestionado, usando los mismos modelos que acabas de conectar a mano.

Para seguir ajustando un stack que controlas, explora la página de producto de ElevenAPI para ver planes, límites de concurrencia y la biblioteca de voces. O bien, regístrate y empieza hoy mismo a hacer tu primera llamada.

Preguntas frecuentes sobre crear un agente de voz con Twilio y ElevenLabs

Artículos relacionados

Crea con el audio IA de la más alta calidad