Verificando acceso interno...
ElevenLabs — configuración técnica
Esta es la guía operativa para configurar un agente de ElevenLabs Conversational AI y conectarlo con Cadences. Cubre tres bloques: (1) qué tocar en el panel de ElevenLabs, (2) qué endpoints nuestros usar como webhooks / server-tools, y (3) cómo se enchufa todo con Twilio para llamadas de teléfono reales.
1. Por qué ElevenLabs (y por qué este modelo)
ElevenLabs ofrece tres cosas que, juntas, son hoy la mejor opción para asistentes de voz reales:
- Voz clonable y naturalísima en español (la mejor del mercado para nuestro caso de uso, mejor que Azure/Google/OpenAI para acentos castellanos y gallegos).
- Conversational AI nativo: ellos gestionan turn-taking, interrupciones, latencia, ASR y TTS en una única sesión. No tenemos que orquestar STT + LLM + TTS por separado.
- Server-side tools (function calling vía HTTPS) — el agente puede llamar a endpoints nuestros para leer datos (estado de pedido, agenda, alergias del cliente) y para actuar (crear reserva, mandar WhatsApp, escalar a humano).
Nuestro modelo es: ElevenLabs lleva la conversación, nosotros le damos contexto y herramientas y cobramos por orquestar todo (multi-canal, memoria, branding, panel para el cliente).
2. Cuenta, secretos y bindings
Todos estos secretos viven en el Pages project cadences:
| Secret | Para qué | Dónde se obtiene |
|---|---|---|
ELEVENLABS_API_KEY | Llamadas a la REST API de ElevenLabs (TTS, STT, signed URLs, listar voces). | Panel ElevenLabs → Profile → API Keys. |
ELEVENLABS_AGENT_ID | ID del agente Conversational AI por defecto. Cada cliente puede sobreescribirlo en voice_assistants. | Panel ElevenLabs → Conversational AI → Agents → "..." → Copy ID. |
ELEVENLABS_PHONE_NUMBER_ID | Si vamos a recibir llamadas vía la integración de teléfono propia de ElevenLabs (no Twilio). Hoy usamos Twilio, así que es opcional. | Panel ElevenLabs → Phone Numbers. |
ELEVENLABS_SIGNING_SECRET | Para verificar la firma del webhook de eventos (xi-signature, HMAC-SHA256 con timestamp). | Panel ElevenLabs → Workspace → Webhooks → Generate signing secret. |
Bindings relevantes en wrangler.toml: D1 projectos-db
(donde viven voice_assistants, voice_calls, etc.) y la
Durable Object ELEVENLABS_MEDIA_STREAM (puente WebSocket Twilio ⇄ ElevenLabs).
3. Configurar el agente en el panel
Pasos a seguir en ElevenLabs → Conversational AI → Agents → New agent:
- Voice: elige la voz clonada del cliente (si la tenemos) o una de nuestras voces base. Para Vila Sen Vento usaremos voz clonada de Javier (5-10 min de audio limpio).
- Language: Spanish (Spain). Activar también Galician si aplica.
- System prompt: NO escribir el prompt entero a mano. El prompt vive en
voice_assistants.personaPrompt(D1) y lo enviamos vía conversation initiation override al arrancar la sesión. En el panel dejamos un prompt mínimo de fallback ("Eres un asistente. Espera instrucciones del sistema."). - First message: dejar vacío. La primera frase se inyecta dinámicamente al iniciar la sesión (depende de canal: teléfono = "¡Hola! Soy el asistente de [Negocio]"; WhatsApp = vacío porque ya hay contexto en chat).
- LLM: GPT-4o-mini para el 90 % de los casos (latencia + coste). GPT-4o solo para clientes premium. No Claude (latencia inestable hoy en su Conversational AI).
- Tools: añadir uno por uno los endpoints de la sección 4. Para cada tool: type =
Webhook, method =POST, URL =https://cadences.app/api/voice/tools/<nombre>, auth = Bearer con el token del cliente. - Knowledge base: NO subir documentos al panel de ElevenLabs. Nuestro RAG vive en Cloudflare Vectorize (índice
codex-knowledge, namespace por cliente). El toolsearch_knowledgeconsulta nuestro RAG. - Webhook (events): activar el "Workspace webhook" apuntando a
https://cadences.app/api/webhooks/elevenlabs. Pegar el signing secret enELEVENLABS_SIGNING_SECRET. - Variables de inicialización (override): permitimos inyectar
customer_phone,tenant_id,channelypersona_overridesal arrancar. Esto lo hace nuestro endpoint/api/voice/twilio/incomingantes de abrir el WebSocket.
4. Tools / webhooks que le damos
Estos son los endpoints HTTP que el agente puede llamar como
function calls. Todos viven bajo functions/api/voice/tools/
(proyecto cadences) y todos requieren Bearer con un token
por-tenant que generamos al alta. La lista de tools activos por cliente está en
voice_tools_config.
4.1 Lectura — el agente pregunta
| Tool | Devuelve | Uso típico |
|---|---|---|
get_customer_context | Datos del cliente (nombre, alergias, preferencias, último pedido) desde voice_customer_facts. | El agente saluda con nombre y recuerda alergias. |
search_knowledge | Top-K chunks del RAG del tenant (Vectorize codex-knowledge, namespace = tenantId). | "¿La tortilla lleva cebolla?" → busca en ficha de producto. |
get_order_status | Estado del pedido vía voice_connectors (Shopify / WooCommerce / restaurant23). | "¿Dónde está mi pedido #12345?" |
list_calendar_slots | Huecos disponibles para reserva en los próximos N días. | Reservas restaurante / cita. |
lookup_product | Ficha de producto (precio, stock, alérgenos) desde el conector e-commerce. | Pre-compra, recomendación. |
4.2 Escritura — el agente actúa
| Tool | Acción | Confirmación |
|---|---|---|
create_reservation | Crea reserva en el conector (restaurant23 / calendario externo). | Devuelve ID + hora confirmada para que el agente la lea. |
create_order_draft | Crea draft order en Shopify (no cobra todavía). | Devuelve URL de pago para enviar por WhatsApp/SMS. |
send_whatsapp | Envía mensaje WhatsApp al teléfono del cliente (template o sesión, según ventana 24h). | Útil para mandar link de pago, foto de producto, mapa. |
escalate_to_human | Marca voice_calls.callAnalysis.nextAction='escalate' + manda WhatsApp al staff con resumen + offers transferencia. | Caso queja grave, problema técnico. |
save_fact | Inserta en voice_customer_facts (alergias, preferencias) con source='phone' y verified=1 tras confirmación verbal. | "Apuntado: alérgico al marisco." |
4.3 Convenciones de payload
Todas las tools reciben un body JSON con esta forma:
{
"tenant_id": "vilasenvento",
"call_id": "vc_01HXY...",
"channel": "phone | whatsapp | web",
"customer": {
"phone": "+34666...",
"id": "cust_..." // si lo conocemos
},
"args": { ... } // los parámetros declarados en el tool
} Y devuelven:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": "human-readable message" } Importante: los mensajes de error tienen que ser frases que el agente pueda leer en voz alta sin sonar técnico. Ejemplo: "No encuentro ningún pedido con ese número, ¿lo confirmas?", no "404 NOT_FOUND".
5. Webhook de eventos (call lifecycle)
ElevenLabs nos manda eventos al endpoint POST /api/webhooks/elevenlabs
(functions/api/webhooks/elevenlabs.js)
con firma xi-signature verificada vía
verifyElevenLabsSignature() (HMAC-SHA256, tolerancia 5 min).
Eventos que procesamos:
conversation_initiation_metadata— inicio: creamos fila envoice_calls.agent.responseyuser.transcription— turno: insertamos envoice_call_turns.call.interrupted— el cliente cortó al agente: marcamos en analytics.conversation_end_metadata— fin: lanzamos el Analyzer (segundo LLM) que generasummary,intent,sentiment,leadScorey los escribe envoice_call_analysis.
Si el signing secret está configurado y la firma falla → 401 y log
en Sentry. Sin signing secret se acepta sin verificar (modo desarrollo);
nunca dejar en producción sin firma.
6. Llamadas de teléfono — puente Twilio ⇄ ElevenLabs
ElevenLabs tiene su propia integración de teléfono pero es cara y poco flexible
para multi-tenant. Usamos Twilio como portador de número y nosotros hacemos el
puente con una Durable Object:
ELEVENLABS_MEDIA_STREAM (src/durable-objects/ElevenLabsMediaStream.js).
- Llamada entrante a número Twilio del cliente → Twilio hace POST a
/api/voice/twilio/incoming. - Devolvemos TwiML con
<Connect><Stream>apuntando al WSS/api/elevenlabs/media-stream?tenant=...&callSid=.... - Twilio abre WebSocket bidireccional contra nuestra Durable Object.
- La DO abre otro WebSocket contra ElevenLabs Conversational AI con la signed URL.
- La DO reenvía audio Twilio (μ-law 8 kHz) ⇄ ElevenLabs (PCM 16 kHz) y reescribe los frames in situ.
- Al final de la llamada Twilio cierra el stream → la DO cierra el WS de ElevenLabs y persiste metadata.
Por qué Durable Object: necesitamos un único actor por llamada que mantenga estado del WebSocket y haga conversión de audio en tiempo real con latencia < 100 ms. Workers normales no permiten WebSockets duraderos bidireccionales con estado.
7. Observabilidad y QA
- Tabla maestra:
voice_calls(1 fila por llamada). Joinea convoice_call_turns(transcripción) yvoice_call_analysis(resumen + scores). - Panel:
voice.cadences.app→ Llamadas. Filtros por intent, sentiment, lead score. Reproductor de grabación + transcripción turn-by-turn. - Alertas: dashboard en voice-admin sale en rojo si
callAnalysis.dangerLevel ≥ 3(queja grave) o sinextAction = 'escalate'sin atender. - Costes: ElevenLabs cobra por minuto de conversación. Hoy ≈ $0.08/min con GPT-4o-mini incluido. Sumamos el coste por llamada en
voice_calls.cost_cents.
8. Checklist de alta de un cliente nuevo
- Crear el agente en panel ElevenLabs (sección 3).
- Insertar fila en
voice_assistantsconelevenlabsAgentId,personaPreset,personaPrompt. - Crear conector(es) en
voice_connectors(Shopify, calendario, etc.). - Activar las tools deseadas en
voice_tools_config(típicamente:get_customer_context,search_knowledge,send_whatsapp,escalate_to_human+ las específicas del vertical). - Comprar número Twilio + apuntar Voice URL a
/api/voice/twilio/incoming. - Indexar la knowledge base del cliente en Vectorize (namespace = tenantId).
- Test E2E: llamar al número, hablar 2 min, comprobar en panel que aparece la llamada con summary y turns.
- Pasarle al cliente acceso al panel
voice.cadences.appcon su login.