> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zelto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# LiveKit

> Envía las sesiones de LiveKit Agents a Zelto desde tu worker cuando finalizan.

LiveKit ejecuta tus agentes de voz como workers alojados por ti. No existe una
API en la nube desde la que Zelto pueda sincronizar las llamadas y los webhooks de
las salas de LiveKit no incluyen la transcripción. Por eso, esta integración
funciona mediante **envío**: el worker del agente envía a Zelto cada sesión
finalizada con una clave de API. Utiliza el mismo endpoint independiente del
proveedor, [`/webhooks/calls`](/es/docs/integrations/api-call-upload), que las
demás fuentes, con la cabecera `X-Zelto-Provider: livekit` para identificar las
llamadas como LiveKit.

## Crear una clave

1. Abre **Configuración → Integraciones → LiveKit** y haz clic en **Crear clave de API**.
2. Asigna un nombre descriptivo, por ejemplo `livekit-prod-worker`.
3. Copia la clave **una sola vez**. Zelto solo guarda su hash. Es la misma clave
   para toda la organización que utilizan la
   [API REST](/es/api-reference/agents/list-agents) y el
   [servidor MCP](/es/docs/mcp).

## Enviar una sesión

Guarda la clave como `ZELTO_API_KEY` en el entorno del worker. Al finalizar
cada sesión, envía su transcripción a `https://ingest.zelto.ai/webhooks/calls`
con el siguiente ejemplo. Instala `aiohttp` en el entorno del worker si aún
no está disponible.

Usa un identificador estable para el agente y un identificador único de sala o
sesión para cada llamada. Reutiliza el identificador de llamada al reintentar
una carga.

```python theme={null}
import os, aiohttp
from livekit.agents import AgentSession

async def forward_to_zelto(
    session: AgentSession, room_name: str, agent_id: str,
    agent_version: str, system_prompt: str,
    version_config: dict | None = None,
):
    turns = [
        {"role": item.role, "content": item.text_content}
        for item in session.history.items
        if item.text_content
    ]
    async with aiohttp.ClientSession() as http:
        await http.post(
            "https://ingest.zelto.ai/webhooks/calls",
            headers={
                "Authorization": f"Bearer {os.environ['ZELTO_API_KEY']}",
                "X-Zelto-Provider": "livekit",
            },
            json={
                "agent": {"externalId": agent_id},
                "version": agent_version,
                **({"versionConfig": version_config} if version_config is not None else {}),
                "systemPrompt": system_prompt,
                "call": {"externalId": room_name},
                "transcript": {"turns": turns},
            },
        )
```

El cuerpo utiliza el formato canónico de llamada de Zelto, documentado en
[Proveedores personalizados y otros](/es/docs/integrations/api-call-upload).
Consulta esa página o la [referencia de la API REST](/es/api-reference/agents/list-agents)
para ver todos los campos. Los roles válidos son `user`, `assistant`, `system` y
`tool`. Todo es opcional salvo `call.externalId` y una referencia al agente.
También puedes enviar `durationSeconds`, `recordingUrl`, `endedReason`, `cost` y
un bloque `customer`.

Una carga correcta devuelve `{ "received": true }`.

## Declarar la versión que atendió la sesión

Llama a `forward_to_zelto` con la etiqueta y el prompt real capturados para esa
sesión. Por ejemplo, conserva `agent_id="support-agent"` para ambos despliegues
y usa `agent_version="support-2026-09-a"` o `"support-2026-09-b"`. El parámetro
de Python se envía como `version` en el nivel superior de la carga.

Cambia la etiqueta al modificar el prompt **o el pipeline**, aunque el prompt
sea idéntico. Conserva la etiqueta original al reintentar o enviar después de
otro despliegue. No uses un ID de sala como versión ni cambies el ID del agente
con cada despliegue.

La primera llamada procesada crea la versión declarada. Cuando las llamadas de
ambas versiones estén visibles, podrás seleccionarlas para un experimento del
agente. La etiqueta no captura el pipeline completo. Consulta
[Versiones de agentes](/es/docs/agent-versions) para conocer las capturas, los
límites de la alternativa automática y los pasos de verificación.

Pasa `version_config` para incluir el contexto completo como `versionConfig`: LLM, STT, modelo/voz TTS, herramientas, flujos o ajustes propios. Debe ser un objeto JSON de hasta 64 KiB. También puedes registrarlo antes en **Agentes → Versiones → Registrar versión** o mediante `POST /v1/agents/{id}/versions`. Consulta el [contrato de configuración](/es/docs/agent-versions#la-identidad-y-la-configuración-son-independientes).

## Enviar invocaciones de herramientas

Si el agente invoca herramientas durante una sesión, envía cada llamada para que
aparezca en la transcripción, se incluya en el análisis de IA y se registre como
una [traza de invocación de herramienta](/es/docs/conversations#invocaciones-de-herramientas)
consultable. LiveKit guarda las herramientas en `session.history.items` como
elementos `function_call` —la invocación con `name`, `arguments` y `call_id`— y
`function_call_output` —el resultado asociado al mismo `call_id`—. Convierte cada
`function_call` en un turno `tool` con un objeto `toolCall` y asócialo con su
resultado mediante `call_id`:

```python theme={null}
def build_turns(session: AgentSession):
    # Index tool outputs by the call id they answer.
    outputs = {
        item.call_id: item.output
        for item in session.history.items
        if item.type == "function_call_output"
    }
    turns = []
    for item in session.history.items:
        if item.type == "message" and item.text_content:
            turns.append({"role": item.role, "content": item.text_content})
        elif item.type == "function_call":
            turns.append({
                "role": "tool",
                "toolCall": {
                    "name": item.name,
                    "toolCallId": item.call_id,
                    "arguments": item.arguments,       # JSON or a JSON string
                    "result": outputs.get(item.call_id),
                    "status": "success" if item.call_id in outputs else "pending",
                },
            })
    return turns
```

Solo `toolCall.name` es obligatorio. `arguments` y `result` aceptan cualquier
valor JSON; si reciben una cadena JSON, Zelto la analiza. `status` puede ser
`success`, `error` o `pending`. El `content` del turno puede quedar vacío cuando
existe `toolCall`; Zelto utiliza el nombre de la herramienta como etiqueta.
Envía `build_turns(session)` como `transcript.turns` en la solicitud anterior.

## Verificar la primera llamada

Envía una sesión y abre [Conversaciones](/es/docs/conversations). Debería aparecer
identificada como LiveKit en pocos segundos. Si no aparece, revisa la respuesta
que recibió el worker: `{ "received": true }` indica que Zelto aceptó la carga y
una respuesta `4xx` incluye el motivo del rechazo. Comprueba también que se envían
las cabeceras `Authorization` y `X-Zelto-Provider: livekit`. Consulta
[Conectar un proveedor de voz](/es/docs/guides/connect-a-voice-provider#diagnosticar-una-llamada-ausente).

## Identificadores estables

* **`agent.externalId`** identifica al agente. Zelto busca o crea un agente de
  LiveKit para cada valor distinto, así que utiliza un identificador estable por
  agente, no por sesión. El `agent.name` de la primera sesión le asigna el nombre.
* **`call.externalId`** —el identificador de la sala o sesión— elimina duplicados.
  Si vuelves a enviar el mismo valor, Zelto **actualiza** la llamada existente con
  la transcripción o duración nuevas, o añade una grabación que faltaba, sin
  cobrar otra vez la llamada. Conserva también la misma referencia del agente.
  Envía la sesión cuando finalice y reenvía el cuerpo original completo si
  añades información.

## Grabaciones y transcripciones

Este flujo envía la transcripción que el agente ya tiene. Adjunta una grabación
agregando `call.recordingUrl` al mismo cuerpo de la solicitud. El audio puede
proceder de [egress](https://docs.livekit.io/home/egress/overview/) o de tu propio
servicio de grabación, incluido un bucket de S3.

### Grabación disponible al finalizar la llamada

Incluye la URL en tu solicitud habitual a
`https://ingest.zelto.ai/webhooks/calls`, con los encabezados existentes
`Authorization: Bearer $ZELTO_API_KEY`, `X-Zelto-Provider: livekit` y
`Content-Type: application/json`. Por ejemplo, el objeto `call` dentro del
cuerpo completo de una sesión podría ser:

```json theme={null}
{
  "externalId": "room-example-001",
  "startedAt": "2026-09-14T16:00:00Z",
  "durationSeconds": 20,
  "endedReason": "customer_hangup",
  "recordingUrl": "https://recordings.example.com/room-example-001.wav"
}
```

Conserva la referencia del agente, la transcripción, los metadatos y el contexto
de la versión en el resto del cuerpo. Utiliza el identificador de llamada que
ya envías a Zelto; el nombre del archivo no es un nuevo identificador de llamada.

### Grabación disponible más tarde

Envía la sesión como siempre al finalizar. Cuando termine la carga de la
grabación, copia el **cuerpo original completo**, agrega la URL y vuelve a
enviarlo al mismo endpoint con los mismos encabezados:

```python theme={null}
from copy import deepcopy

# original_payload es el cuerpo completo enviado previamente para esta sesión.
recorded_payload = deepcopy(original_payload)
recorded_payload["call"]["recordingUrl"] = recording_url
# Envía recorded_payload con la misma clave de API y el encabezado livekit.
```

Conserva la referencia original del agente, `call.externalId`, la transcripción,
los metadatos y el contexto de la versión (`version`, `systemPrompt` y
`versionConfig`, si los enviaste). Mantén también los detalles originales de la
llamada: cliente, costo, duración y motivo de finalización. Un reenvío parcial
con solo la grabación puede borrar campos omitidos, como el costo o el motivo
de finalización. Guarda el cuerpo original si la notificación de grabación se
procesa en otro trabajo o proceso.

El reenvío adjunta la grabación a la misma conversación sin cobrar otra vez la
llamada. Esto añade una grabación **que faltaba** o reemplaza una URL del proveedor
que Zelto aún no ha capturado. Para recuperar una URL vencida o inaccesible,
reenvía el cuerpo original completo con una URL nueva que permita descargar el
audio. Reintentar después de que Zelto haya capturado la grabación conserva la
copia almacenada.

Una URL sin firma de un objeto privado de S3 no es suficiente: envía una URL GET
prefirmada. Las respuestas de acceso denegado o recurso no encontrado reciben
reintentos limitados; una URL cuya caducidad está confirmada requiere una URL
nueva. Zelto no puede renovar las URL firmadas por tu servicio de almacenamiento.

### Reproducción y captura

Los usuarios pueden reproducir, pausar y desplazarse por la grabación en el
reproductor de audio de la conversación. Envía `startSeconds` / `endSeconds` de
la transcripción relativos al inicio de la grabación para sincronizar la
reproducción; adjuntar audio no genera ni reemplaza las marcas de tiempo.

Zelto debe poder descargar la URL sin autenticación interactiva. También puedes
usar una URL prefirmada: envíala después de completar la carga y mantenla válida
hasta que termine la captura. Para archivos WAV, utiliza preferentemente un
tipo de contenido de audio como `audio/wav`. Las URL WAV servidas como
`application/octet-stream` también se capturan sin modificar los bytes del audio.

Zelto pone la captura en cola de forma asíncrona y vuelve a alojar la grabación
cuando está habilitado el almacenamiento de grabaciones de tu organización. Si
tu organización lo ha desactivado, la reproducción usa tu URL, que debe seguir
accesible y admitir rangos de bytes para desplazarse por el audio. Esta
integración no cambia esa configuración.

Una respuesta `200` con `{ "received": true }` confirma la aceptación, **no que
la captura haya terminado**. Antes de enviar grabaciones de todas las llamadas,
verifica una en [Conversaciones](/es/docs/conversations): debe seguir siendo una
sola conversación, conservar su transcripción y sus detalles, y permitir
reproducir y desplazarse por el audio. Si el almacenamiento está habilitado,
confirma con Zelto que la captura terminó antes de dejar que venza la URL de
origen.

Si solo tienes audio y no una transcripción, utiliza el flujo de carga de
archivos de la [API REST](/es/api-reference/agents/list-agents); adjuntar
`call.recordingUrl` aquí no solicita una transcripción.

## Estado de la conexión

La tarjeta de LiveKit muestra **Activo** cuando Zelto ha ingerido al menos una
llamada de LiveKit. La página de detalles de cada agente indica cuándo llegó la
última sesión, para que puedas comprobar si el worker sigue enviando datos.

## Transmitir trazas (OpenTelemetry)

El flujo anterior alimenta [Conversaciones](/es/docs/conversations), la vista de
transcripción y análisis de una llamada. Además, un agente de LiveKit puede
transmitir **trazas de OpenTelemetry** a [Trazas](/es/docs/traces): el árbol de
spans de cada llamada —LLM, TTS, STT, herramientas y spans propios— con latencia,
tokens y costo. LiveKit instrumenta cada sesión automáticamente; solo debes
registrar un exportador OTLP que apunte al endpoint de ingestión de Zelto.

Crea una **clave de ingestión de observabilidad** de solo escritura en
**Configuración → Integraciones → Observabilidad** o en **Trazas → Conectar
agente**. Necesitas el [rol](/es/docs/settings#miembros-e-invitaciones) `owner` o `admin`.
Registra el exportador **al principio del entrypoint, antes de `session.start()`**.
El código debe ejecutarse, no basta con definirlo:

```python theme={null}
import asyncio
import os

from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from livekit.agents import JobContext
from livekit.agents.telemetry import set_tracer_provider

async def entrypoint(ctx: JobContext):
    provider = TracerProvider(resource=Resource.create({
        "service.name": "livekit-agent",
        "zelto.agent_external_id": os.environ["ZELTO_AGENT_EXTERNAL_ID"],
        "zelto.agent_provider": "livekit",
    }))
    provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
    set_tracer_provider(provider, metadata={"session.id": ctx.room.name})

    async def flush_traces():
        await asyncio.to_thread(provider.force_flush, timeout_millis=10_000)

    ctx.add_shutdown_callback(flush_traces)

    # ... build your AgentSession and await session.start(...) here ...
```

`OTLPSpanExporter()` utiliza las variables de entorno estándar de OpenTelemetry y
el SDK añade `/v1/traces` al endpoint:

```bash theme={null}
ZELTO_AGENT_EXTERNAL_ID="support-agent"
OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.zelto.ai"
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20<your-ingest-key>"
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
```

Instala el exportador HTTP con
`pip install opentelemetry-exporter-otlp-proto-http`. Este exportador HTTP de Python envía
**`http/protobuf`**: mantén ese protocolo. Zelto también acepta JSON OTLP de
exportadores compatibles. No se admite gRPC en
`:4317`. Este flujo es independiente del envío de transcripciones: las trazas
utilizan la clave de ingestión de solo escritura, **no** `ZELTO_API_KEY`, y no
necesitan la cabecera `X-Zelto-Provider`. Consulta [Trazas](/es/docs/traces) para
entender qué contiene una traza y cómo leerla.

Usa `ZELTO_AGENT_EXTERNAL_ID` con el mismo valor estable que `agent.externalId`
en `forward_to_zelto`, y `ctx.room.name` como `call.externalId`. El ID externo
crea o reutiliza el agente de LiveKit aunque la traza llegue antes que la llamada.
La metadata añade la referencia de sesión a los spans de LiveKit. En tus propios
spans, envía también `session.id`. Si una sesión contiene varios agentes, pon
el ID externo en cada span en lugar de fijar uno compartido en el recurso.
Consulta [Enviar trazas](/es/docs/guides/send-traces) para probar la entrega,
configurar logs y diagnosticar errores, y la [documentación de LiveKit](https://docs.livekit.io/deploy/observability/tracing/)
para su integración con OpenTelemetry.

## Contenido relacionado

* [Trazas](/es/docs/traces) — el árbol de spans de OpenTelemetry de una llamada; configúralo en [Transmitir trazas](#transmitir-trazas-opentelemetry).
* [Conectar un proveedor de voz](/es/docs/guides/connect-a-voice-provider) — elige un método y verifica la ingestión.
* [Proveedores personalizados y otros](/es/docs/integrations/api-call-upload) — el formato canónico de llamada que acepta el endpoint.
* [Conversaciones](/es/docs/conversations) — dónde aparecen las llamadas de LiveKit.
* [Referencia de API](/es/api-reference/agents/list-agents) · [MCP](/es/docs/mcp) — consulta tus datos mediante código.
