# Live-Transkription

> Streame Mikrofon- oder Tab-Audio per WebSocket an transcript1 und erhalte Untertitelzeilen, auf Wunsch mit Übersetzung in Echtzeit, noch während gesprochen wird.

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

Die Live-Transkription verwandelt einen Audiostream in Untertitelzeilen, während noch gesprochen wird. Dein Client sendet rohes PCM-Audio über einen WebSocket, und transcript1 schickt Zeilen zurück, die zunächst als vorläufige Version erscheinen und dann endgültig werden. Mit `translate_to` enthält jede Zeile zusätzlich ihre Übersetzung. Wenn die Sitzung endet, wird das Ergebnis als normale Transkription gespeichert.

Für Live gibt es keine eigenen Tarife: Es nutzt dasselbe Minutenguthaben wie alles andere. Wie die Verbindungszeit gezählt wird, steht unter [Abrechnung](https://transcript1.com/de/docs/live#billing).

## Wann Live sinnvoll ist

| Nimm Live, wenn | Nimm [eine normale Transkription](https://transcript1.com/de/docs/getting-started), wenn |
|---|---|
| das Audio gerade erst entsteht: ein Anruf, ein Vortrag, ein Stream, ein Video, das du ansiehst. | du einen Link oder eine fertige Datei hast. |
| du Untertitel ein bis zwei Sekunden nach dem Gesprochenen brauchst. | du die höchste Genauigkeit willst und auf das Ergebnis warten kannst. |
| du neben jeder Zeile eine Übersetzung in Echtzeit willst. | du ein fertiges Transkript übersetzen willst, auch in mehrere Sprachen. |

Live nimmt nur Audio an, und zwar als rohes PCM. Videolinks, Uploads, WebM, Opus und MP3 werden nicht akzeptiert. Nimm das Audio in deinem Client auf und wandle es vorher in PCM um. [Audio im Browser aufnehmen](https://transcript1.com/de/docs/live#capture-audio-in-a-browser) zeigt, wie.

## Schnellstart mit dem SDK

Installiere SDK 1.5.0 oder neuer: `npm i @transcript1/sdk@latest`. `t1.live.start` legt die Sitzung an, verbindet sich und wird aufgelöst, sobald die Sitzung gestartet ist. Du kannst also sofort Audio senden.

```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);
```

Zeilen werden an Ort und Stelle aktualisiert. Ersetze eine vorhandene Zeile, sobald eine mit derselben `id` eintrifft, oder lies alle über `live.lines`. `end()` beendet die Sitzung, wartet auf die letzten Zeilen und speichert das Transkript. Alle Methoden und Events stehen unter [Node.js-SDK](https://transcript1.com/de/docs/sdk#live-transcription).

## Schnellstart mit einem rohen WebSocket

Ohne das SDK sind es zwei Schritte: eine Sitzung per HTTPS anlegen und dann den WebSocket mit dem Einmal-Token öffnen, das du zurückbekommen hast.

### Sitzung anlegen

```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}'
```

Die API antwortet mit `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` ist `null`, wenn `save` auf `false` steht. Das Token gilt nur einmal und läuft nach 60 Sekunden ab, verbinde dich also sofort. `rate` ist die Zahl der Guthabenminuten, die pro angefangener Minute dieser Sitzung abgebucht werden; siehe [Abrechnung](https://transcript1.com/de/docs/live#billing).

Der Request-Body kennt diese Felder. Alle sind optional, der nötige Scope ist `transcriptions:write`:

| Feld | Standard | Regel |
|---|---|---|
| `language` | `auto` | `auto` oder ein zweistelliger Code, den die Live-Transkription unterstützt. |
| `translate_to` | keiner | Ein zweistelliger Code. Pro Sitzung eine Zielsprache. Siehe [Live übersetzen](https://transcript1.com/de/docs/live#translate-live). |
| `diarize` | `false` | Sprecher kennzeichnen. Manche Konfigurationen können das nicht, etwa zusammen mit `language: "auto"`, und antworten mit `422`. Prüfe die [Fähigkeiten](https://transcript1.com/de/docs/live#capabilities). |
| `audio_format` | `pcm_s16le` | Der einzige Wert. Das Feld gibt es, damit später weitere Formate dazukommen können. |
| `sample_rate` | `16000` | Eine ganze Zahl von 8000 bis 48000. Nur Mono. |
| `save` | `true` | Mit `false` bleibt nach dem Ende der Sitzung nichts erhalten. Siehe [Gespeicherte Transkripte](https://transcript1.com/de/docs/live#saved-transcripts). |

### Verbinden und streamen

Öffne den WebSocket mit dem Token als zweitem Subprotokoll. Das funktioniert in Browsern und unter Node 22+ ohne eigene Header. Warte auf `session.started`, bevor du Audio sendest: Binärframes, die früher ankommen, werden verworfen, und du erhältst eine Warnung `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));
```

Unter Node 20 gibt es keinen eingebauten `WebSocket`: Nimm das Paket `ws`, das dieselbe Form `new WebSocket(url, protocols)` hat.

Auf ein gültiges Token antwortet der Server mit dem Subprotokoll `t1-live` und sonst nichts. Ein ungültiges Token lässt den Handshake nie scheitern: Der Socket öffnet sich, du erhältst ein `error`-Event und der Socket schließt mit einem 4xxx-Code. So zeigt ein Browser den echten Grund statt eines allgemeinen 1006. Siehe [Close-Codes](https://transcript1.com/de/docs/live#close-codes).

## Audio im Browser aufnehmen

Live-Audio muss 16-Bit-PCM mit Vorzeichen, Little-Endian, Mono, mit der `sample_rate` der Sitzung sein. Browser liefern Gleitkomma-Samples, wandle sie also in einem `AudioWorklet` um. Dieses Beispiel nimmt das Mikrofon als 16-kHz-PCM in 100-ms-Blöcken auf und sendet es über den Browser-Einstiegspunkt des SDK. `getAccessToken` gibt ein OAuth-Access-Token zurück; lege nie einen API-Schlüssel in einem Browser ab. Siehe [Sicherheit](https://transcript1.com/de/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();
}
```

Das Worklet ist für 16 kHz geschrieben. Läuft der Kontext mit einer anderen Rate, sind die Blöcke nicht mehr 100 ms lang, das PCM bleibt aber korrekt, weil `sampleRate` zum Kontext passt. Um statt des Mikrofons den Ton eines Tabs oder Videos aufzunehmen, erstelle den `MediaStream` mit `getDisplayMedia` oder aus dem `captureStream()` eines Medienelements und verbinde ihn genauso.

## Live übersetzen

Füge der Sitzung `translate_to` hinzu, und transcript1 übersetzt jede Zeile, sobald sie gesprochen wird.

- Pro Sitzung eine Zielsprache. Weitere Sprachen fügst du danach dem gespeicherten Transkript über den [Übersetzungs-Endpoint](https://transcript1.com/de/docs/live#saved-transcripts) hinzu.
- Jede `line` hat ein Original (`text`, `final`) und ein `translation`-Objekt (`language`, `text`, `final`). `translation` ist `null`, wenn `translate_to` nicht gesetzt ist.
- Original und Übersetzung werden unabhängig voneinander endgültig: Eine Zeile kann ein endgültiges Original haben, während ihre Übersetzung sich noch ändert. Zeige eine Zeile erst als endgültig, wenn beides endgültig ist.
- Eine Zeile, die schon in der Zielsprache ist, nutzt ihren eigenen Text als Übersetzung und ist endgültig, sobald die Zeile abgeschlossen wird.
- Eine Zeile wird abgeschlossen, wenn der Sprecher pausiert, der Sprecher wechselt, eine Pause zwischen Wörtern länger als 0.9 Sekunden dauert, die Zeile zwei Untertitelreihen (84 Zeichen) oder 15 Sekunden erreicht oder du `finalize` sendest. Bei aktiver Übersetzung kann das Abschließen einer Zeile etwa 3 Sekunden länger dauern, bis ihre Übersetzung da ist. Dann kann die Übersetzung offen bleiben (`final: false`) und weiterwachsen, obwohl das Original schon endgültig ist.
- Die Übersetzung ist kostenlos. Sie verbraucht keine Minuten.

Welche Zielsprachen und Sprachpaare verfügbar sind, hängt vom Live-Spracherkennungsanbieter ab. Lies die [Fähigkeiten](https://transcript1.com/de/docs/live#capabilities), statt Regeln fest einzubauen. Eine nicht unterstützte Zielsprache liefert `422 unsupported_translation`. Ein Anbieter kann zum Beispiel eine ausdrückliche `language` statt `auto` verlangen oder nur Paare mit Englisch erlauben.

## WebSocket-Protokoll

### Verbinden

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

Das Token wird als WebSocket-Subprotokoll übertragen. Es erscheint nie in einer URL oder einem Zugriffslog. Sende keinen `Authorization`-Header: Das Token ist die Zugangsberechtigung. Der Server sendet alle 30 Sekunden einen Ping; Browser und das Paket `ws` antworten automatisch. Ein Socket, der 60 Sekunden lang kein Pong sendet, wird getrennt.

Eine gültige Verbindung zu einer Sitzung, die schon einen Socket hat, ersetzt den alten. Siehe [Fortsetzen und Übernehmen](https://transcript1.com/de/docs/live#resume-and-takeover).

### Nachrichten des Clients

| Nachricht | Bedeutung |
|---|---|
| Binärframe | Audio: PCM s16le Mono mit der `sample_rate` der Sitzung. Jeder Frame hat eine gerade Länge und höchstens 256 KB. Etwa 100 ms pro Frame sind eine gute Größe. |
| `{"type":"finalize"}` | Schließt die aktuelle Zeile sofort ab, zum Beispiel wenn das Video pausiert wird. |
| `{"type":"end"}` | Beendet die Sitzung und speichert sie. Der Server antwortet mit `session.ended` und schließt den Socket. |
| Alles andere | Ein nicht fatales `error`-Event mit dem Code `invalid_message`. |

Ein Frame mit ungerader Länge oder zu groß ist ein Protokollverstoß und schließt den Socket mit `4008`.

Beginne erst nach `session.started` mit dem Senden. Wer Audio schneller als in Echtzeit sendet, bezahlt die Audiozeit statt der Verbindungszeit (siehe [Abrechnung](https://transcript1.com/de/docs/live#billing)). Sende also in dem Tempo, in dem das Audio entsteht.

### Events des Servers

Jedes Event ist ein JSON-Textframe mit einem `type`.

| Event | Wird gesendet, wenn |
|---|---|
| `session.started` | Die Verbindung akzeptiert wurde. Ab jetzt Audio senden. |
| `line` | Eine Untertitelzeile entstanden ist oder sich geändert hat. |
| `usage` | Nach jeder Abbuchung. |
| `warning` | Etwas Aufmerksamkeit braucht, die Sitzung aber weiterläuft. |
| `error` | Ein nicht fataler Fehler oder der Grund für ein sofortiges Schließen. |
| `reconnect` | Der Server gleich neu startet. Setze die Sitzung fort. |
| `session.ended` | Die Sitzung vorbei und gespeichert ist. Das letzte Event vor einem normalen Schließen. |

`session.started` sagt, ob es sich um ein Fortsetzen handelt und wie viel Audio die Sitzung schon verarbeitet hat:

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

`line` enthält den vollständigen aktuellen Zustand einer Zeile. Ersetze also die Zeile, die du mit derselben `id` hast. Siehe [Das Zeilenobjekt](https://transcript1.com/de/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` kommt nach jeder Abbuchung, mit der Verbindungszeit, der Audiozeit, den bisher abgebuchten Guthabenminuten und den Minuten, die dir bleiben:

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

`warning` hat einen `code`:

| `code` | Bedeutung |
|---|---|
| `balance_low` | Die nächste Abbuchung würde scheitern. Die Sitzung endet mit `balance_exhausted`, sobald sie fällig ist. Wird nicht gesendet, solange die [automatische Aufladung](https://transcript1.com/de/docs/billing-and-limits#auto-top-up) aktiv ist. |
| `audio_dropped` | Audio kam an, bevor die Sitzung bereit war, oder zu viel Audio, während sich der Spracherkennungsanbieter verband. Das älteste Audio wurde verworfen. |
| `idle_timeout_soon` | Seit 4 Minuten kein Audio. Die Sitzung endet nach 5 Minuten ohne Audio. Sende Audio, um sie zu behalten. |

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

`error` hat einen `code` und eine `message`. Diese Codes können auf einem Live-Socket ankommen:

| `code` | Fatal | Bedeutung |
|---|---|---|
| `invalid_message` | Nein | Ein Textframe, der weder `finalize` noch `end` ist. |
| `invalid_token` | Ja | Das Token ist ungültig, abgelaufen oder schon verwendet. Der Socket schließt mit `4001`. |
| `not_found` | Ja | Die Sitzung existiert nicht. Der Socket schließt mit `4004`. |
| `session_ended` | Ja | Die Sitzung ist schon beendet. Der Socket schließt mit `4004`. |

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

`reconnect` kommt, bevor der Server den Socket mit `1012` schließt, zum Beispiel bei einem Deployment. Hole ein neues Token und setze die Sitzung fort.

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

`session.ended` ist das letzte Event jeder Sitzung. Die Zahlen sind die Endsummen:

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

### Das Zeilenobjekt

| Feld | Bedeutung |
|---|---|
| `id` | Eine bei 0 beginnende Zahl, die mit jeder Zeile um eins wächst. Sie ist der Segmentindex im gespeicherten Transkript. |
| `start_ms`, `end_ms` | Beginn und Ende in der Audiozeit der Sitzung. Diese Zeit läuft über Wiederverbindungen hinweg weiter. |
| `speaker` | Das Sprecherlabel, wenn `diarize` aktiv ist, sonst `null`. Die Labels sind dieselben wie in normalen Transkripten. |
| `language` | Die erkannte oder angegebene Sprache der Zeile, oder `null`, solange sie noch nicht feststeht. |
| `text` | Der bisherige Originaltext. |
| `final` | `true`, sobald sich der Originaltext nicht mehr ändert. |
| `translation` | `null` ohne `translate_to`, sonst `language`, `text` und `final`. |

Zeige Zeilen mit `final: false` in einem helleren Stil: Ihr Text kann sich noch ändern. Der Text einer Zeile mit `final: true` ändert sich nie wieder, und eine endgültige Zeile wird nie entfernt.

Das SDK nutzt für Zeilenfelder `camelCase`: `startMs`, `endMs`.

### Endgründe

`session.ended` und `GET /v1/live/sessions/:id` liefern einen `end_reason`:

| Grund | Bedeutung |
|---|---|
| `client_ended` | Du hast `end` gesendet. |
| `balance_exhausted` | Die nächste Abbuchung ist gescheitert. Die Sitzung endet an einer Minutengrenze und wird gespeichert. |
| `idle_timeout` | 5 Minuten ohne Audio. |
| `max_duration` | Die Sitzung war 300 Minuten verbunden. Starte eine neue. |
| `disconnected` | Der Client war länger als 2 Minuten weg, ohne die Sitzung fortzusetzen. |
| `upstream_error` | Der Spracherkennungsanbieter ist ausgefallen. |
| `internal_error` | Bei uns ist ein Fehler aufgetreten. |
| `not_connected` | Die Sitzung wurde innerhalb von 5 Minuten nie verbunden. Sie wird nie berechnet und ist nur über `GET` sichtbar. |

### Close-Codes

| Code | Bedeutung |
|---|---|
| `1000` | Normales Schließen. Folgt immer auf `session.ended`. |
| `1011` | Unerwarteter Serverfehler. Der Server stellt die Sitzung wieder her: Setze sie mit einem neuen Token fort. |
| `1012` | Server-Neustart. Folgt auf `reconnect`: Setze die Sitzung mit einem neuen Token fort. |
| `4001` | Das Token ist ungültig, abgelaufen oder schon verwendet. |
| `4004` | Die Sitzung wurde nicht gefunden oder ist schon beendet. |
| `4008` | Protokollverstoß: ein Binärframe mit ungerader Länge oder zu groß. |
| `4010` | Durch eine neuere Verbindung zur selben Sitzung ersetzt. |

Während die API neu startet, kann eine neue WebSocket-Anfrage mit HTTP `503` beantwortet werden. Versuche es kurz danach erneut.

## Fortsetzen und Übernehmen

Eine Sitzung übersteht einen Netzwerkabbruch, ein API-Deployment und einen Absturz. Nach einem Abbruch wartet sie 2 Minuten auf dich; danach endet sie als `disconnected` und wird gespeichert.

1. Rufe `POST /v1/live/sessions/:id/token` auf. Die Antwort enthält `token`, `token_expires_at` und `websocket_url`, dieselben Felder wie beim Anlegen der Sitzung. Der Aufruf scheitert mit `409 session_ended`, sobald die Sitzung beendet ist.
2. Öffne einen neuen WebSocket mit diesem Token.
3. `session.started` kommt mit `resumed: true`. `offset_ms` ist die Menge an Audio, die die Sitzung schon verarbeitet hat: Mach mit dem Audio ab dieser Stelle weiter. Die Zeilen-IDs laufen weiter.

Zeilen, die schon endgültig waren, bleiben erhalten. Der laufende Rest zum Zeitpunkt des Abbruchs geht verloren. Sende daher ein paar Sekunden Audio ab `offset_ms` erneut, wenn du es zwischenspeicherst. Lücken beim Wiederverbinden werden nicht berechnet.

Das Übernehmen ist derselbe Aufruf. Hält der Server noch einen Socket für offen, ersetzt eine neue gültige Verbindung ihn, und der alte Socket wird mit `4010` geschlossen, nachdem seine Zeilen gespeichert sind. Das Limit für gleichzeitige Sitzungen blockiert ein Fortsetzen nie.

Das SDK erledigt das alles für dich: Bei einem Abbruch oder einem `reconnect`-Event holt es ein Token und verbindet sich mit einem Backoff von 0.5 bis 8 Sekunden neu, höchstens 2 Minuten lang, und sendet die letzten 10 Sekunden des Audios, das während der Lücke gesendet wurde, noch einmal.

## Abrechnung

Die Live-Transkription nutzt dasselbe Minutenguthaben wie Batch-Transkriptionen.

- **Abgerechnet wird die Verbindungszeit**, von dem Moment, in dem der Socket akzeptiert wird, bis er geschlossen wird. Stille und ein pausiertes Video zählen. Die Zeit zwischen einem Abbruch und dem Fortsetzen nicht.
- **Pro angefangener Minute, mindestens 1.** Die erste Minute wird abgebucht, wenn die erste Verbindung akzeptiert wird, jede weitere, sobald sie beginnt.
- **Schneller als Echtzeit wird als Audiozeit abgerechnet.** Ist das gesendete Audio länger als die Verbindungszeit, wird stattdessen die Audiozeit berechnet.
- **Die Rate liest du aus der API.** Eine angefangene Minute kostet `rate` Guthabenminuten, aufgerundet. Die Rate ist meist 1 und kann Dezimalstellen haben. Die Live-Rate steht in `rates.live` von `GET /v1/account`, in `rate` von `GET /v1/live/capabilities` und in `rate` der Sitzung, die du anlegst. Die Rate einer Sitzung steht nach dem Anlegen fest.
- **Die Übersetzung ist kostenlos.**
- **Nie verbunden heißt nicht berechnet.** Eine Sitzung, die innerhalb von 5 Minuten nicht verbunden wird, endet als `not_connected`.
- **Fehler werden erstattet.** Endet eine Sitzung mit `upstream_error` oder `internal_error`, bevor sie auch nur eine Zeile erzeugt hat, wird alles zurückerstattet. Eine durch Stille leere Sitzung wird berechnet und leer gespeichert.
- **Ist das Guthaben aufgebraucht** und die [automatische Aufladung](https://transcript1.com/de/docs/billing-and-limits#auto-top-up) aktiv, belastet transcript1 deine hinterlegte Karte, während die Sitzung weiter streamt, und die Sitzung läuft weiter. Ist die automatische Aufladung aus oder pausiert oder scheitert die Abbuchung, wird die Sitzung abgeschlossen, gespeichert und endet an einer Minutengrenze mit `balance_exhausted`. Vorher kommt die Warnung `balance_low`, außer die automatische Aufladung ist aktiv.

Das Anlegen einer Sitzung liefert `402 insufficient_minutes`, wenn dein nutzbares Guthaben unter einer angefangenen Minute zur aktuellen Rate liegt. Ist die automatische Aufladung aktiv, versucht transcript1 zuerst aufzuladen. Siehe [Abrechnung und Limits](https://transcript1.com/de/docs/billing-and-limits#live-transcription).

## Limits

| | Free | Monthly | Annual |
|---|---|---|---|
| Gleichzeitige Live-Sitzungen | 1 | 2 | 3 |

- Eine Sitzung zählt, solange sie `waiting`, `connected` oder `disconnected` (innerhalb ihres 2-Minuten-Fensters) ist. Über dem Limit antwortet `POST /v1/live/sessions` mit `429 too_many_live_sessions` und `Retry-After: 30`.
- Live-Sitzungen zählen nicht zu deinem Limit für aktive Jobs.
- Eine Sitzung dauert höchstens 300 Minuten Verbindungszeit. Danach endet sie mit `max_duration`.
- Eine Sitzung ohne Audio endet nach 5 Minuten mit `idle_timeout`; nach 4 Minuten erhältst du `idle_timeout_soon`. Ein pausierter Stream berechnet weiter Verbindungszeit und gilt als untätig, beende die Sitzung also bei einer langen Pause.
- transcript1 begrenzt alle Live-Sitzungen zusammen. Sind alle belegt, antwortet `POST /v1/live/sessions` mit `503 live_capacity`, was sich wiederholen lässt. Warte `Retry-After` (30 Sekunden) ab und versuche es erneut. Das wird nur beim Anlegen einer Sitzung geprüft, nie mittendrin.
- Das Anlegen von Sitzungen und das Abrufen von Tokens zählen zu deinen Requests pro Minute.
- Audio ist nur rohes PCM: `pcm_s16le`, Mono, 8000 bis 48000 Hz.

## Fähigkeiten

`GET /v1/live/capabilities` beschreibt, was die Live-Transkription gerade unterstützt, damit Clients keine Anbieterregeln fest einbauen müssen. Der nötige Scope ist `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
}
```

Die Listen sind hier gekürzt. Lies sie aus der API.

- `translation.pairs` ist `"any"` (jede erkannte Sprache in jede Zielsprache) oder eine Liste von Objekten mit `from` und `to`.
- `translation.requires_language` ist `true`, wenn die Übersetzung eine ausdrückliche `language` statt `auto` braucht.
- Ist die Live-Transkription nicht konfiguriert, lautet die Antwort `available: false` und enthält nur `rate`; das Anlegen einer Sitzung liefert dann `503 live_unavailable`.

Mit dem SDK: `await t1.live.capabilities()`.

## Sitzung abrufen

`GET /v1/live/sessions/:id` liefert den Zustand einer Sitzung. Der nötige Scope ist `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` ist `waiting`, `connected`, `disconnected` oder `ended`. `end_reason` ist `null`, bis die Sitzung beendet ist. Mit dem SDK: `await t1.live.get(id)`.

## Fehler

Request-Fehler verwenden die übliche [Fehlerstruktur](https://transcript1.com/de/docs/errors). Diese liefern `POST /v1/live/sessions` und die zugehörigen Endpoints:

| HTTP-Status | `code` | Erneut versuchen? | Wann |
|---|---|---|---|
| 422 | `invalid_request` | Nein | Ein Feld fehlt oder ist ungültig, zum Beispiel eine `sample_rate` außerhalb von 8000 bis 48000. |
| 422 | `unsupported_language` | Nein | `language` wird für die Live-Transkription nicht unterstützt. |
| 422 | `unsupported_translation` | Nein | Die Zielsprache oder das Sprachpaar wird nicht unterstützt. |
| 402 | `insufficient_minutes` | Nein | Dein nutzbares Guthaben liegt unter einer angefangenen Minute zur aktuellen Rate. |
| 403 | `email_not_verified` | Nein | Bestätige zuerst deine E-Mail-Adresse. |
| 429 | `too_many_live_sessions` | Ja | Die gleichzeitigen Live-Sitzungen deines Tarifs sind ausgeschöpft. Warte `Retry-After` ab. |
| 503 | `live_capacity` | Ja | Die gesamte Live-Kapazität ist belegt. Warte `Retry-After` (30 Sekunden) ab. |
| 503 | `live_unavailable` | Nein | Die Live-Transkription ist gerade nicht verfügbar. |
| 409 | `session_ended` | Nein | `POST /v1/live/sessions/:id/token` für eine bereits beendete Sitzung. |

Probleme auf dem Socket kommen als `error`-Events und [Close-Codes](https://transcript1.com/de/docs/live#close-codes).

## Gespeicherte Transkripte

Mit `save: true` (Standard) wird die Sitzung beim Beenden als normale Transkription gespeichert. Das Audio wird nie gespeichert.

- `transcription_id` bekommst du beim Anlegen der Sitzung. Der Status ist `live`, solange die Sitzung läuft, und `completed`, wenn sie beendet ist. Eine Sitzung, die nie verbunden war, hinterlässt eine Transkription mit dem Status `canceled`.
- Die Transkription hat `source: { "platform": "live", "session_id": "lv_…" }`. Es gibt weder Link noch Datei. Das Dashboard listet sie als Live-Sitzung.
- Jede Zeile wird zu einem Segment mit demselben Index wie die `id` der Zeile. Lies das Transkript wie jede andere Transkription als JSON, Text, SRT oder VTT: siehe [API-Referenz](https://transcript1.com/de/docs/api-reference).
- Die Live-Übersetzung wird mitgespeichert. Lies sie mit dem Parameter `translation`, wie unter [Übersetzung](https://transcript1.com/de/docs/translation#read-the-translation) beschrieben.
- Weitere Übersetzungssprachen fügst du später mit `POST /v1/transcriptions/:id/translations` hinzu, sobald die Transkription `completed` ist. Siehe [Übersetzung](https://transcript1.com/de/docs/translation#translate-an-existing-transcript).

Mit `save: false` bleibt nach dem Ende der Sitzung nichts erhalten: Es gibt keine Transkription, und `transcription_id` ist `null`. Du bekommst trotzdem jede Zeile über den Socket, und die übliche Abrechnung gilt.

## Sicherheit

- Das Token, das du beim Anlegen einer Sitzung erhältst, gilt nur einmal und läuft nach 60 Sekunden ab. Es wird als WebSocket-Subprotokoll gesendet, nicht in der URL, und bleibt so aus Zugriffslogs heraus. Ein durchgesickertes Token ist nutzlos, sobald es verwendet wurde oder abgelaufen ist.
- Verwende einen API-Schlüssel nur auf einem Server, den du kontrollierst. Lege einen API-Schlüssel nie in eine Webseite oder Erweiterung.
- Melde Nutzer in einem Browser oder einer Erweiterung per OAuth an und gib das OAuth-Access-Token an `getAccessToken` von `createLiveClient`. Zum Starten von Sitzungen braucht das Token den Scope `transcriptions:write`. OAuth-Access-Tokens laufen nach 1 Stunde ab, und jedes Fortsetzen fragt `getAccessToken` nach einem frischen.
- Dein Server kann die Sitzung auch mit seinem API-Schlüssel anlegen und dem Browser nur `websocket_url` und `token` übergeben. Der Client verbindet sich dann direkt. Ohne ein neues Token von dir kann er die Sitzung aber nicht fortsetzen.
