# Transcripción en vivo

> Envía audio del micrófono o de una pestaña a transcript1 por WebSocket y recibe líneas de subtítulos, con traducción opcional en tiempo real, mientras la gente habla.

Canonical: https://transcript1.com/es/docs/live
Language: es
Last updated: 2026-11-03

La transcripción en vivo convierte un flujo de audio en líneas de subtítulos mientras la gente todavía habla. Tu cliente envía audio PCM sin comprimir por un WebSocket, y transcript1 devuelve líneas que empiezan como una primera aproximación y luego pasan a ser finales. Agrega `translate_to` y cada línea incluye también su traducción. Cuando termina la sesión, el resultado se guarda como una transcripción normal.

No hay planes aparte para el modo en vivo: usa el mismo saldo de minutos que todo lo demás. En [Facturación](https://transcript1.com/es/docs/live#billing) verás cómo se cuenta el tiempo conectado.

## Cuándo usar la transcripción en vivo

| Usa el modo en vivo cuando | Usa [una transcripción normal](https://transcript1.com/es/docs/getting-started) cuando |
|---|---|
| El audio todavía se está produciendo: una llamada, una charla, una transmisión, un video que estás viendo. | Tienes un enlace o un archivo terminado. |
| Necesitas subtítulos en uno o dos segundos tras la voz. | Quieres la mayor precisión y puedes esperar el resultado. |
| Quieres traducción en tiempo real junto a cada línea. | Quieres traducir una transcripción ya terminada, o a varios idiomas. |

El modo en vivo solo acepta audio, como PCM sin comprimir. No admite enlaces de video, archivos subidos, WebM, Opus ni MP3. Captura el audio en tu cliente y conviértelo antes a PCM. En [Captura de audio en el navegador](https://transcript1.com/es/docs/live#capture-audio-in-a-browser) se muestra cómo.

## Inicio rápido con el SDK

Instala el SDK 1.5.0 o posterior: `npm i @transcript1/sdk@latest`. `t1.live.start` crea la sesión, se conecta y se resuelve cuando la sesión ya empezó, así que puedes enviar audio de inmediato.

```javascript
import { createReadStream } from 'node:fs';
import { setTimeout as sleep } from 'node:timers/promises';
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1(); // reads TRANSCRIPT1_API_KEY

const live = await t1.live.start({ language: 'en', translateTo: 'es', sampleRate: 16000 });

live.on('line', line => {
  const mark = line.final ? 'final  ' : 'partial';
  console.log(mark, line.text, '→', line.translation?.text ?? '');
});
live.on('warning', warning => console.warn(warning.code));

// speech.pcm: 16 kHz, mono, signed 16-bit little-endian, no header
for await (const chunk of createReadStream('./speech.pcm', { highWaterMark: 3200 })) {
  live.send(chunk);
  await sleep(100); // 3,200 bytes are 100 ms of audio: send at real-time speed
}

const result = await live.end();
console.log(result.reason, result.transcriptionId, result.minutesCharged);
```

Las líneas se actualizan en su lugar. Reemplaza la línea que ya tienes cuando llegue otra con el mismo `id`, o lee todas desde `live.lines`. `end()` termina la sesión, espera las últimas líneas y guarda la transcripción. Consulta todos los métodos y eventos en [SDK para Node.js](https://transcript1.com/es/docs/sdk#live-transcription).

## Inicio rápido con un WebSocket directo

Sin el SDK hay dos pasos: crear una sesión por HTTPS y abrir el WebSocket con el token de un solo uso que recibes.

### Crea una sesión

```bash
curl -X POST https://api.transcript1.com/v1/live/sessions \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"language": "en", "translate_to": "es", "sample_rate": 16000}'
```

La API responde `201`:

```json
{
  "id": "lv_8fK2Lm0PqRsTuVwXyZaBcD",
  "status": "waiting",
  "transcription_id": "tr_3NcQvXy7BtHjKd5ErFgLmA",
  "websocket_url": "wss://api.transcript1.com/v1/live/sessions/lv_8fK2Lm0PqRsTuVwXyZaBcD/stream",
  "token": "Zk3x...",
  "token_expires_at": "2026-11-02T12:01:00Z",
  "rate": 1
}
```

`transcription_id` es `null` cuando `save` es `false`. El token es de un solo uso y vence a los 60 segundos, así que conéctate de inmediato. `rate` es la cantidad de minutos de saldo que se cobra por cada minuto iniciado de esta sesión; consulta [Facturación](https://transcript1.com/es/docs/live#billing).

El cuerpo de la solicitud acepta estos campos. Todos son opcionales, y el permiso necesario es `transcriptions:write`:

| Campo | Valor por defecto | Regla |
|---|---|---|
| `language` | `auto` | `auto`, o un código de dos letras que admita la transcripción en vivo. |
| `translate_to` | ninguno | Un código de dos letras. Un solo idioma de destino por sesión. Consulta [Traducir en vivo](https://transcript1.com/es/docs/live#translate-live). |
| `diarize` | `false` | Identifica a los hablantes. Algunas configuraciones no pueden hacerlo, por ejemplo junto con `language: "auto"`, y responden `422`. Consulta las [capacidades](https://transcript1.com/es/docs/live#capabilities). |
| `audio_format` | `pcm_s16le` | Es el único valor. El campo existe para poder añadir formatos más adelante. |
| `sample_rate` | `16000` | Un entero de 8000 a 48000. Solo mono. |
| `save` | `true` | Con `false` no se conserva nada cuando termina la sesión. Consulta [Transcripciones guardadas](https://transcript1.com/es/docs/live#saved-transcripts). |

### Conéctate y transmite

Abre el WebSocket con el token como segundo subprotocolo. Así funciona en los navegadores y en Node 22+ sin encabezados personalizados. Espera `session.started` antes de enviar audio: los fragmentos binarios enviados antes se descartan y recibes una advertencia `audio_dropped`.

```javascript
import { createReadStream } from 'node:fs';
import { setTimeout as sleep } from 'node:timers/promises';

const session = await fetch('https://api.transcript1.com/v1/live/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TRANSCRIPT1_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ language: 'en', translate_to: 'es', sample_rate: 16000 }),
}).then(response => response.json());

const ws = new WebSocket(session.websocket_url, ['t1-live', session.token]);
const lines = new Map();

ws.addEventListener('message', async message => {
  const event = JSON.parse(message.data);

  if (event.type === 'session.started') {
    for await (const chunk of createReadStream('./speech.pcm', { highWaterMark: 3200 })) {
      ws.send(chunk);
      await sleep(100);
    }
    ws.send(JSON.stringify({ type: 'end' }));
  } else if (event.type === 'line') {
    lines.set(event.line.id, event.line); // the full current state: replace by id
    console.log(event.line.final, event.line.text, event.line.translation?.text);
  } else if (event.type === 'session.ended') {
    console.log('ended:', event.reason, event.minutes_charged);
  }
});

ws.addEventListener('close', event => console.log('closed', event.code));
```

En Node 20 no hay `WebSocket` integrado: usa el paquete `ws`, que tiene la misma forma `new WebSocket(url, protocols)`.

El servidor responde a un token válido con el subprotocolo `t1-live` y nada más. Un token incorrecto nunca hace fallar el handshake: el socket se abre, recibes un evento `error` y el socket se cierra con un código 4xxx, de modo que un navegador muestra el motivo real en lugar de un 1006 genérico. Consulta [Códigos de cierre](https://transcript1.com/es/docs/live#close-codes).

## Captura de audio en el navegador

El audio en vivo debe ser PCM mono de 16 bits con signo, little-endian, a la `sample_rate` de la sesión. Los navegadores entregan muestras en coma flotante, así que conviértelas en un `AudioWorklet`. Este fragmento captura el micrófono como PCM de 16 kHz en fragmentos de 100 ms y lo envía con el punto de entrada del SDK para navegador. `getAccessToken` devuelve un token de acceso OAuth; nunca pongas una clave de API en un navegador. Consulta [Seguridad](https://transcript1.com/es/docs/live#security).

```javascript
import { createLiveClient } from '@transcript1/sdk/live';

const workletSource = `
class Pcm16Processor extends AudioWorkletProcessor {
  constructor() {
    super();
    this.buffer = new Int16Array(1600); // 100 ms at 16 kHz
    this.filled = 0;
  }

  process(inputs) {
    const channel = inputs[0][0];
    if (!channel) return true;
    for (let i = 0; i < channel.length; i++) {
      const sample = Math.max(-1, Math.min(1, channel[i]));
      this.buffer[this.filled++] = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
      if (this.filled === this.buffer.length) {
        this.port.postMessage(this.buffer.slice());
        this.filled = 0;
      }
    }
    return true;
  }
}
registerProcessor('pcm16', Pcm16Processor);
`;

const client = createLiveClient({ getAccessToken: () => myApp.getOAuthAccessToken() });

const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1 } });
const context = new AudioContext({ sampleRate: 16000 });
const moduleUrl = URL.createObjectURL(new Blob([workletSource], { type: 'text/javascript' }));
await context.audioWorklet.addModule(moduleUrl);

// the context's real rate: a browser that cannot do 16 kHz picks its own
const live = await client.start({ language: 'auto', sampleRate: context.sampleRate });
live.on('line', renderLine);

const microphone = context.createMediaStreamSource(stream);
const capture = new AudioWorkletNode(context, 'pcm16');
capture.port.onmessage = event => live.send(event.data); // an Int16Array
const mute = context.createGain();
mute.gain.value = 0;
microphone.connect(capture).connect(mute).connect(context.destination);

async function stop() {
  stream.getTracks().forEach(track => track.stop());
  await context.close();
  return live.end();
}
```

El worklet está escrito para 16 kHz. Si el contexto funciona a otra frecuencia, el fragmento ya no dura 100 ms, pero el PCM sigue siendo correcto porque `sampleRate` coincide con el del contexto. Para capturar el sonido de una pestaña o de un video en lugar del micrófono, crea el `MediaStream` con `getDisplayMedia` o con el `captureStream()` de un elemento multimedia y conéctalo igual.

## Traducir en vivo

Agrega `translate_to` a la sesión y transcript1 traduce cada línea a medida que se dice.

- Un solo idioma de destino por sesión. Después puedes agregar más idiomas a la transcripción guardada con el [endpoint de traducciones](https://transcript1.com/es/docs/live#saved-transcripts).
- Cada `line` tiene un original (`text`, `final`) y un objeto `translation` (`language`, `text`, `final`). `translation` es `null` si no definiste `translate_to`.
- El original y la traducción pasan a ser finales por separado: una línea puede tener el original final mientras su traducción todavía cambia. Muestra una línea como final cuando lo sean ambos.
- Una línea que ya está en el idioma de destino usa su propio texto como traducción, y es final cuando la línea se cierra.
- Una línea se cierra cuando el hablante hace una pausa, cambia el hablante, una pausa entre palabras dura más de 0.9 segundos, la línea llega a dos filas de subtítulo (84 caracteres) o a 15 segundos, o envías `finalize`. Con la traducción activada, una línea puede tardar unos 3 segundos más en cerrarse mientras llega su traducción. En ese caso la traducción puede seguir abierta (`final: false`) y seguir creciendo después de que el original ya es final.
- La traducción es gratis. No usa minutos.

Los idiomas de destino y los pares de idiomas disponibles dependen del proveedor de voz en vivo. Consulta las [capacidades](https://transcript1.com/es/docs/live#capabilities) en lugar de fijar reglas en tu código. Un destino no admitido devuelve `422 unsupported_translation`. Por ejemplo, un proveedor puede exigir un `language` explícito en lugar de `auto`, o permitir solo pares que incluyan inglés.

## Protocolo WebSocket

### Conexión

```javascript
new WebSocket(websocket_url, ['t1-live', token]);
```

El token viaja como subprotocolo de WebSocket. Nunca aparece en una URL ni en un registro de acceso. No envíes un encabezado `Authorization`: el token es la credencial. El servidor envía un ping cada 30 segundos; los navegadores y el paquete `ws` responden solos. Un socket que no envía pong durante 60 segundos se desconecta.

Una conexión válida a una sesión que ya tiene un socket reemplaza al anterior. Consulta [Reanudación y reemplazo](https://transcript1.com/es/docs/live#resume-and-takeover).

### Mensajes del cliente

| Mensaje | Significado |
|---|---|
| Fragmento binario | Audio: PCM s16le mono a la `sample_rate` de la sesión. Cada fragmento tiene una longitud par y como máximo 256 KB. Unos 100 ms por fragmento es un buen tamaño. |
| `{"type":"finalize"}` | Cierra ya la línea actual, por ejemplo cuando pausas el video. |
| `{"type":"end"}` | Termina la sesión y la guarda. El servidor responde `session.ended` y cierra el socket. |
| Cualquier otra cosa | Un evento `error` no fatal con el código `invalid_message`. |

Un fragmento de longitud impar o demasiado grande es una violación del protocolo y cierra el socket con `4008`.

Empieza a enviar audio después de `session.started`. Un cliente que envía audio más rápido que el tiempo real paga el tiempo del audio en lugar del tiempo conectado (consulta [Facturación](https://transcript1.com/es/docs/live#billing)), así que envíalo a la velocidad a la que se produce.

### Eventos del servidor

Cada evento es un mensaje de texto JSON con un `type`.

| Evento | Cuándo se envía |
|---|---|
| `session.started` | Se aceptó la conexión. Envía audio a partir de ahora. |
| `line` | Se creó o cambió una línea de subtítulos. |
| `usage` | Después de cada cobro. |
| `warning` | Algo requiere atención, pero la sesión continúa. |
| `error` | Un problema no fatal, o el motivo de un cierre inmediato. |
| `reconnect` | El servidor está por reiniciarse. Reanuda la sesión. |
| `session.ended` | La sesión terminó y se guardó. Es el último evento antes de un cierre normal. |

`session.started` indica si se trata de una reanudación y cuánto audio ha procesado ya la sesión:

```json
{
  "type": "session.started",
  "session_id": "lv_8fK2Lm0PqRsTuVwXyZaBcD",
  "transcription_id": "tr_3NcQvXy7BtHjKd5ErFgLmA",
  "resumed": false,
  "offset_ms": 0
}
```

`line` trae el estado completo y actual de una línea, así que reemplaza la que tienes con el mismo `id`. Consulta [El objeto de línea](https://transcript1.com/es/docs/live#the-line-object).

```json
{
  "type": "line",
  "line": {
    "id": 12,
    "start_ms": 53120,
    "end_ms": 56040,
    "speaker": null,
    "language": "en",
    "text": "Welcome back to the stream",
    "final": true,
    "translation": { "language": "es", "text": "Bienvenidos de nuevo", "final": false }
  }
}
```

`usage` se envía después de cada cobro, con el tiempo conectado, el tiempo del audio, los minutos de saldo cobrados hasta ahora y los minutos que te quedan:

```json
{
  "type": "usage",
  "connected_ms": 61240,
  "audio_ms": 60100,
  "minutes_charged": 2,
  "minutes_remaining": 898
}
```

`warning` incluye un `code`:

| `code` | Significado |
|---|---|
| `balance_low` | El próximo cobro fallaría. La sesión termina con `balance_exhausted` cuando llegue ese momento. No se envía mientras la [recarga automática](https://transcript1.com/es/docs/billing-and-limits#auto-top-up) está activada. |
| `audio_dropped` | Llegó audio antes de que la sesión estuviera lista, o demasiado audio mientras se conectaba el proveedor de voz. Se descartó el audio más antiguo. |
| `idle_timeout_soon` | Llevas 4 minutos sin audio. La sesión termina tras 5 minutos sin audio. Envía audio para conservarla. |

```json
{ "type": "warning", "code": "idle_timeout_soon" }
```

`error` incluye un `code` y un `message`. Estos códigos pueden llegar por un socket en vivo:

| `code` | Fatal | Significado |
|---|---|---|
| `invalid_message` | No | Un mensaje de texto que no es `finalize` ni `end`. |
| `invalid_token` | Sí | El token no es válido, venció o ya se usó. El socket se cierra con `4001`. |
| `not_found` | Sí | La sesión no existe. El socket se cierra con `4004`. |
| `session_ended` | Sí | La sesión ya terminó. El socket se cierra con `4004`. |

```json
{ "type": "error", "code": "invalid_message", "message": "Expected {\"type\":\"finalize\"} or {\"type\":\"end\"}" }
```

`reconnect` llega antes de que el servidor cierre el socket con `1012`, por ejemplo durante un despliegue. Obtén un token nuevo y reanuda.

```json
{ "type": "reconnect", "reason": "server_restart" }
```

`session.ended` es el último evento de toda sesión. Las cifras son los totales finales:

```json
{
  "type": "session.ended",
  "reason": "client_ended",
  "transcription_id": "tr_3NcQvXy7BtHjKd5ErFgLmA",
  "minutes_charged": 3,
  "connected_ms": 142500,
  "audio_ms": 141900
}
```

### El objeto de línea

| Campo | Significado |
|---|---|
| `id` | Un número que empieza en 0 y crece de uno en uno con cada línea. Es el índice del segmento en la transcripción guardada. |
| `start_ms`, `end_ms` | Inicio y fin en tiempo de audio de la sesión. Este tiempo continúa tras las reconexiones. |
| `speaker` | La etiqueta del hablante cuando `diarize` está activado; si no, `null`. Las etiquetas son las mismas que en las transcripciones normales. |
| `language` | El idioma detectado o indicado de la línea, o `null` si aún no se conoce. |
| `text` | El texto original hasta ahora. |
| `final` | `true` cuando el texto original ya no cambiará. |
| `translation` | `null` sin `translate_to`; en caso contrario, `language`, `text` y `final`. |

Muestra las líneas con `final: false` con un estilo más tenue: su texto todavía puede cambiar. El texto de una línea con `final: true` no vuelve a cambiar, y una línea final nunca se elimina.

El SDK usa `camelCase` para los campos de línea: `startMs`, `endMs`.

### Motivos de finalización

`session.ended` y `GET /v1/live/sessions/:id` devuelven un `end_reason`:

| Motivo | Significado |
|---|---|
| `client_ended` | Enviaste `end`. |
| `balance_exhausted` | Falló el siguiente cobro. La sesión termina en un límite de minuto y se guarda. |
| `idle_timeout` | Pasaron 5 minutos sin audio. |
| `max_duration` | La sesión estuvo conectada 300 minutos. Inicia una nueva. |
| `disconnected` | El cliente estuvo ausente más de 2 minutos sin reanudar. |
| `upstream_error` | Falló el proveedor de voz. |
| `internal_error` | Falló algo en transcript1. |
| `not_connected` | La sesión nunca se conectó en 5 minutos. Nunca se cobra y solo se ve mediante `GET`. |

### Códigos de cierre

| Código | Significado |
|---|---|
| `1000` | Cierre normal. Siempre va después de `session.ended`. |
| `1011` | Error inesperado del servidor. El servidor recupera la sesión: reanuda con un token nuevo. |
| `1012` | Reinicio del servidor. Va después de `reconnect`: reanuda con un token nuevo. |
| `4001` | El token no es válido, venció o ya se usó. |
| `4004` | No se encontró la sesión o ya terminó. |
| `4008` | Violación del protocolo: un fragmento binario de longitud impar o demasiado grande. |
| `4010` | Reemplazada por una conexión más nueva a la misma sesión. |

Mientras la API se reinicia, una nueva solicitud de WebSocket puede recibir HTTP `503`. Vuelve a intentarlo en un momento.

## Reanudación y reemplazo

Una sesión sobrevive a una caída de la red, a un despliegue de la API y a un fallo. Durante 2 minutos después de una desconexión te espera; pasado ese tiempo termina como `disconnected` y se guarda.

1. Llama a `POST /v1/live/sessions/:id/token`. La respuesta trae `token`, `token_expires_at` y `websocket_url`, los mismos campos que al crear la sesión. Falla con `409 session_ended` si la sesión ya terminó.
2. Abre un WebSocket nuevo con ese token.
3. Llega `session.started` con `resumed: true`. `offset_ms` es la cantidad de audio que la sesión ya procesó: continúa con el audio desde ese punto. Los ids de línea continúan.

Las líneas que ya eran finales se conservan. Se pierde el tramo en curso en el momento de la caída, así que reenvía unos segundos de audio desde `offset_ms` si lo tienes en un búfer. Los cortes de reconexión no se cobran.

El reemplazo se hace con la misma llamada. Si el servidor aún cree que hay un socket abierto, una conexión válida nueva lo reemplaza y el socket anterior se cierra con `4010` después de guardar sus líneas. El límite de sesiones simultáneas nunca bloquea una reanudación.

El SDK hace todo esto por ti: ante una caída o un evento `reconnect`, obtiene un token y se reconecta con una espera de 0.5 a 8 segundos durante un máximo de 2 minutos, y reenvía los últimos 10 segundos de audio enviados durante el corte.

## Facturación

La transcripción en vivo usa el mismo saldo de minutos que las transcripciones por lotes.

- **Se factura el tiempo conectado**, desde que se acepta el socket hasta que se cierra. El silencio y un video en pausa cuentan. El tiempo entre una caída y la reanudación no cuenta.
- **Por minuto iniciado, con un mínimo de 1.** El primer minuto se cobra cuando se acepta la primera conexión, y cada minuto siguiente cuando empieza.
- **Más rápido que el tiempo real se factura como tiempo de audio.** Si el audio que envías dura más que el tiempo conectado, se factura el tiempo del audio.
- **La tarifa se consulta en la API.** Un minuto iniciado cuesta `rate` minutos de saldo, redondeados hacia arriba. La tarifa suele ser 1 y puede tener decimales. Consulta la tarifa en vivo en `rates.live` de `GET /v1/account`, en `rate` de `GET /v1/live/capabilities` y en `rate` de la sesión que creas. La tarifa de una sesión queda fija una vez creada.
- **La traducción es gratis.**
- **Si nunca se conecta, no se cobra.** Una sesión que no se conecta en 5 minutos termina como `not_connected`.
- **Los fallos se reembolsan.** Si una sesión termina con `upstream_error` o `internal_error` antes de producir una sola línea, se devuelve todo lo cobrado. Una sesión vacía por falta de voz se cobra y se guarda vacía.
- **Cuando se acaba el saldo** y la [recarga automática](https://transcript1.com/es/docs/billing-and-limits#auto-top-up) está activada, transcript1 cobra a tu tarjeta guardada mientras la sesión sigue transmitiendo, y la sesión continúa. Si la recarga automática está desactivada o en pausa, o el cobro falla, la sesión se finaliza, se guarda y termina con `balance_exhausted` en un límite de minuto. Antes llega la advertencia `balance_low`, salvo que la recarga automática esté activada.

Crear una sesión devuelve `402 insufficient_minutes` cuando tu saldo utilizable es menor que un minuto iniciado a la tarifa actual. Si la recarga automática está activada, transcript1 intenta recargar primero. Consulta [Facturación y límites](https://transcript1.com/es/docs/billing-and-limits#live-transcription).

## Límites

| | Free | Monthly | Annual |
|---|---|---|---|
| Sesiones en vivo simultáneas | 1 | 2 | 3 |

- Una sesión cuenta mientras está en `waiting`, `connected` o `disconnected` (dentro de su ventana de 2 minutos). Si superas el límite, `POST /v1/live/sessions` devuelve `429 too_many_live_sessions` con `Retry-After: 30`.
- Las sesiones en vivo no cuentan para el límite de transcripciones simultáneas.
- Una sesión dura como máximo 300 minutos de tiempo conectado. Después termina con `max_duration`.
- Una sesión sin audio durante 5 minutos termina con `idle_timeout`; recibes `idle_timeout_soon` a los 4 minutos. Una transmisión en pausa sigue facturando tiempo conectado y cuenta como inactiva, así que termina la sesión si la pausa es larga.
- transcript1 limita todas las sesiones en vivo en conjunto. Cuando están todas en uso, `POST /v1/live/sessions` devuelve `503 live_capacity`, que se puede reintentar. Espera `Retry-After` (30 segundos) y vuelve a intentarlo. Esto solo se comprueba al crear una sesión, nunca en mitad de una.
- Crear sesiones y obtener tokens cuentan para tus requests por minuto.
- El audio es solo PCM sin comprimir: `pcm_s16le`, mono, de 8000 a 48000 Hz.

## Capacidades

`GET /v1/live/capabilities` describe qué admite ahora la transcripción en vivo, para que los clientes nunca fijen reglas del proveedor en su código. El permiso necesario es `transcriptions:read`.

```json
{
  "available": true,
  "auto_language": true,
  "languages": ["en", "es"],
  "translation": {
    "targets": ["es", "de"],
    "pairs": "any",
    "requires_language": false
  },
  "diarize": true,
  "audio_formats": ["pcm_s16le"],
  "sample_rate": { "min": 8000, "max": 48000 },
  "max_session_minutes": 300,
  "rate": 1
}
```

Aquí las listas están abreviadas. Léelas desde la API.

- `translation.pairs` es `"any"` (cualquier idioma detectado hacia cualquier destino), o una lista de objetos con `from` y `to`.
- `translation.requires_language` es `true` cuando la traducción necesita un `language` explícito en lugar de `auto`.
- Cuando la transcripción en vivo no está configurada, la respuesta es `available: false` con solo `rate`, y crear una sesión devuelve `503 live_unavailable`.

Con el SDK: `await t1.live.capabilities()`.

## Consultar una sesión

`GET /v1/live/sessions/:id` devuelve el estado de una sesión. El permiso necesario es `transcriptions:read`.

```bash
curl https://api.transcript1.com/v1/live/sessions/lv_8fK2Lm0PqRsTuVwXyZaBcD \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```json
{
  "id": "lv_8fK2Lm0PqRsTuVwXyZaBcD",
  "status": "ended",
  "end_reason": "client_ended",
  "transcription_id": "tr_3NcQvXy7BtHjKd5ErFgLmA",
  "options": {
    "language": "en",
    "translate_to": "es",
    "diarize": false,
    "audio_format": "pcm_s16le",
    "sample_rate": 16000,
    "save": true
  },
  "connected_ms": 142500,
  "audio_ms": 141900,
  "minutes_charged": 3,
  "rate": 1,
  "created_at": "2026-11-02T11:58:12Z",
  "ended_at": "2026-11-02T12:00:35Z"
}
```

`status` es `waiting`, `connected`, `disconnected` o `ended`. `end_reason` es `null` hasta que la sesión termina. Con el SDK: `await t1.live.get(id)`.

## Errores

Los errores de solicitud usan el [formato de error](https://transcript1.com/es/docs/errors) habitual. Estos son los que devuelven `POST /v1/live/sessions` y sus endpoints hermanos:

| Estado HTTP | `code` | ¿Reintentar? | Cuándo |
|---|---|---|---|
| 422 | `invalid_request` | No | Falta un campo o no es válido, por ejemplo un `sample_rate` fuera del rango de 8000 a 48000. |
| 422 | `unsupported_language` | No | `language` no está admitido para la transcripción en vivo. |
| 422 | `unsupported_translation` | No | El idioma de destino, o el par de idiomas, no está admitido. |
| 402 | `insufficient_minutes` | No | Tu saldo utilizable es menor que un minuto iniciado a la tarifa actual. |
| 403 | `email_not_verified` | No | Verifica primero tu correo. |
| 429 | `too_many_live_sessions` | Sí | Ya usas todas las sesiones en vivo simultáneas de tu plan. Espera `Retry-After`. |
| 503 | `live_capacity` | Sí | Toda la capacidad en vivo está ocupada. Espera `Retry-After` (30 segundos). |
| 503 | `live_unavailable` | No | La transcripción en vivo no está disponible por ahora. |
| 409 | `session_ended` | No | `POST /v1/live/sessions/:id/token` para una sesión que ya terminó. |

Los problemas en el socket llegan como eventos `error` y [códigos de cierre](https://transcript1.com/es/docs/live#close-codes).

## Transcripciones guardadas

Con `save: true` (el valor por defecto), la sesión se guarda como una transcripción normal cuando termina. El audio nunca se almacena.

- `transcription_id` se devuelve al crear la sesión. Su estado es `live` mientras la sesión está activa y `completed` cuando termina. Una sesión que nunca se conectó deja una transcripción `canceled`.
- La transcripción tiene `source: { "platform": "live", "session_id": "lv_…" }`. No hay enlace ni archivo. El panel la muestra como sesión en vivo.
- Cada línea se convierte en un segmento, con el mismo índice que el `id` de la línea. Lee la transcripción como JSON, texto, SRT o VTT igual que cualquier otra: consulta la [referencia de la API](https://transcript1.com/es/docs/api-reference).
- La traducción en vivo se guarda con ella. Léela con el parámetro `translation`, como se explica en [Traducción](https://transcript1.com/es/docs/translation#read-the-translation).
- Más adelante puedes agregar idiomas de traducción con `POST /v1/transcriptions/:id/translations`, cuando la transcripción esté en `completed`. Consulta [Traducción](https://transcript1.com/es/docs/translation#translate-an-existing-transcript).

Con `save: false` no se conserva nada cuando termina la sesión: no hay transcripción y `transcription_id` es `null`. Igualmente recibes todas las líneas por el socket y se aplica el cobro habitual.

## Seguridad

- El token que recibes al crear una sesión es de un solo uso y vence a los 60 segundos. Se envía como subprotocolo de WebSocket, no en la URL, así que no queda en los registros de acceso. Un token filtrado no sirve una vez usado o vencido.
- Usa una clave de API solo desde un servidor que controles. Nunca pongas una clave de API en una página web ni en una extensión.
- En un navegador o una extensión, inicia sesión de las personas con OAuth y pasa el token de acceso OAuth a `getAccessToken` de `createLiveClient`. El token necesita el permiso `transcriptions:write` para iniciar sesiones. Los tokens de acceso OAuth vencen a la hora, y cada reanudación pide uno nuevo a `getAccessToken`.
- Tu servidor también puede crear la sesión con su clave de API y entregar al navegador solo `websocket_url` y `token`. Entonces el cliente se conecta directamente, pero no puede reanudar sin un token nuevo que le des tú.
