# Go live

> Publish live audio to a stored transcription, and put it on a broadcast link listeners open. Control is HTTP; audio is one WebSocket.

| Endpoint | What it does |
|---|---|
| `POST /v1/transcriptions/live` | Create a live transcription |
| `POST /v1/broadcasts` | Create a broadcast |
| `PUT /v1/transcriptions/{id}/broadcasts/{broadcast}` | Attach a broadcast |
| `POST /v1/transcriptions/{id}/recordings` | Start a recording |
| `POST /v1/recordings/{id}/pause` | Pause a recording |
| `POST /v1/recordings/{id}/resume` | Resume a recording |
| `POST /v1/recordings/{id}/stop` | Stop a recording |
| `WS /v1/realtime/sessions/{token}/socket` | Connect to the `streamUrl` a realtime session returned (treat it as opaque). |

A realtime session is gone when its socket closes. To keep what is said, and let other people follow it as it happens, publish to a **live transcription**: it is stored as it is spoken, listed under Transcriptions, and its recordings can be read, translated, exported and shared afterwards. A **broadcast** is the link listeners open (`code`, `shareUrl`); it is fed by the live transcriptions attached to it. The order: create the live transcription, optionally put it on a broadcast, start a recording, publish audio to its `streamUrl`, and control it over HTTP.

## 1. Create a live transcription

`POST /v1/transcriptions/live` with a `name` and optional `settings` (`spokenLanguage`, `speakerLabels`, `latencyMode`…) creates it idle. Nothing is recorded until a recording starts. Its `source` can also be a stream the server pulls itself (a YouTube live, an internet radio address), in which case there is no audio for you to send.

## 2. Put it on a broadcast

`POST /v1/broadcasts` creates the link (`code`, and `shareUrl` to hand out; `access: "private"` and a `password` keep it to the people you give them). `PUT /v1/transcriptions/{id}/broadcasts/{broadcast}` feeds it from the live transcription. One transcription can feed several broadcasts and a broadcast several transcriptions. Listeners follow it with the subscribe socket ([Listen](https://api.vocalstack.com/docs/guides/listen.md)).

## 3. Start a recording

`POST /v1/transcriptions/{id}/recordings` starts one and answers with a publish grant: `recording` (its `id` is what you control and read it by), `sessionId` and `streamUrl`. A `409 conflict` with `details.takeover` means another device is recording it now; repeat with `{ "takeover": true }` to record from here instead.

## 4. Publish audio

Connect to `streamUrl` and publish exactly as on a realtime session ([Stream a microphone](https://api.vocalstack.com/docs/guides/stream-a-microphone.md)): wait for `session.ready`, send raw PCM (`pcm_f32le`, 16 kHz, mono) as binary frames, and read commits back. The difference is that every commit is also stored and fanned out to the recording's subscribers, translated into the languages they read. A cold machine can take a minute to become ready: give `connect` a long ready deadline (`readyDeadlineMs`, `ready_deadline_s`).

## 5. Pause, resume and stop

Control is HTTP, never a frame on the socket: `pause` (idempotent), `resume`, and `stop`, which is allowed only from paused. Sending `eof` on the publisher socket also ends the recording. A recording moves `live` ⇄ `paused` → `ended` → `finalized` (its transcript settled); listeners stay connected through `ended`. A recording left paused longer than `settings.maxPauseS` ends by itself.

## Complete code

Each program runs as it is once `VOCALSTACK_API_KEY` holds one of your API keys.

### TypeScript SDK: `live.ts`

```typescript
import { spawn } from 'node:child_process';
import { VocalStack } from '@vocalstack/sdk';

const vs = new VocalStack({ baseUrl: 'https://api.vocalstack.com', apiKey: process.env.VOCALSTACK_API_KEY });

const tx = await vs.transcriptions.createLive({ name: 'Town hall', settings: { spokenLanguage: 'en' } });

const broadcast = await vs.broadcasts.create({ name: 'Town hall' });
await vs.transcriptions.attachBroadcast(tx.id, broadcast.id);
console.log('listeners open', broadcast.shareUrl);

const grant = await vs.transcriptions.startRecording(tx.id);
const recordingId = grant.recording.id;

const stream = await vs.recordings.publish(grant, { readyDeadlineMs: 180_000 }); // a cold machine can take a while
// -re reads the file at its own pace, as a microphone would arrive. For a
// microphone: -f avfoundation -i :0 (macOS), -f pulse -i default (Linux).
const ffmpeg = spawn('ffmpeg', ['-loglevel', 'error', '-re', '-i', './talk.mp3', '-f', 'f32le', '-ac', '1', '-ar', '16000', '-']);
ffmpeg.stdout.on('data', (chunk: Buffer) => stream.sendAudio(new Uint8Array(chunk)));

// eof ends the recording when the audio does; pause, resume and stop are HTTP.
ffmpeg.on('close', () => stream.eof());
process.once('SIGINT', async () => {
  ffmpeg.kill();
  await vs.recordings.pause(recordingId);
  await vs.recordings.stop(recordingId); // only from paused
});

for await (const ev of stream) {
  if (ev.type === 'transcript.committed') console.log(`#${ev.segment.seq} ${ev.segment.text}`);
}

console.log('ended:', (await vs.recordings.get(recordingId)).state);
```

### Python SDK: `live.py`

```python
import asyncio
import os

from vocalstack import VocalStack, pace_realtime

# -re is not needed here: pace_realtime sends the PCM at its own pace.
DECODE = ["ffmpeg", "-loglevel", "error", "-i", "./talk.mp3", "-f", "f32le", "-ac", "1", "-ar", "16000", "-"]


async def main() -> None:
    async with VocalStack("https://api.vocalstack.com", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
        tx = await vs.transcriptions.create_live(name="Town hall", settings={"spokenLanguage": "en"})

        broadcast = await vs.broadcasts.create(name="Town hall")
        await vs.transcriptions.attach_broadcast(tx.id, broadcast.id)
        print("listeners open", broadcast.share_url)

        grant = await vs.transcriptions.start_recording(tx.id)
        recording_id = grant.recording.id

        ffmpeg = await asyncio.create_subprocess_exec(*DECODE, stdout=asyncio.subprocess.PIPE)
        pcm, _ = await ffmpeg.communicate()
        async with await vs.recordings.publish(grant, ready_deadline_s=180) as stream:
            feeder = asyncio.create_task(pace_realtime(stream, pcm))  # real time, then eof(): the recording ends
            async for event in stream:
                if event.type == "transcript.committed":
                    print(f"#{event.segment.seq} {event.segment.text}", flush=True)
            await feeder

        # To end without eof: await vs.recordings.pause(recording_id), then stop(recording_id).
        print("ended:", (await vs.recordings.get(recording_id)).state)


asyncio.run(main())
```

### HTTP + WebSocket: `live-raw.ts`

```typescript
// No SDK: fetch for the calls, the built-in WebSocket for the audio (Node 22 or newer).
import { spawn } from 'node:child_process';

const API = 'https://api.vocalstack.com';
const headers = { Authorization: `Bearer ${process.env.VOCALSTACK_API_KEY}`, 'Content-Type': 'application/json' };
async function call<T>(method: string, path: string, body?: unknown): Promise<T> {
  const r = await fetch(`${API}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
  if (!r.ok) throw new Error(`${method} ${path}: ${r.status} ${await r.text()}`);
  return (await r.json()) as T;
}

const tx = await call<{ id: string }>('POST', '/v1/transcriptions/live', { name: 'Town hall', settings: { spokenLanguage: 'en' } });

const broadcast = await call<{ id: string; shareUrl: string }>('POST', '/v1/broadcasts', { name: 'Town hall' });
await call('PUT', `/v1/transcriptions/${tx.id}/broadcasts/${broadcast.id}`);
console.log('listeners open', broadcast.shareUrl);

const grant = await call<{ recording: { id: string }; streamUrl: string }>('POST', `/v1/transcriptions/${tx.id}/recordings`, {});

const ws = new WebSocket(grant.streamUrl);
ws.addEventListener('message', (m: MessageEvent) => {
  if (typeof m.data !== 'string') return;
  const f = JSON.parse(m.data);
  if (f.type === 'session.ready') startAudio();
  if (f.type === 'transcript.committed') console.log(`#${f.segment.seq} ${f.segment.text}`);
  if (f.type === 'session.done') console.log('ended:', f.reason);
});

function startAudio(): void {
  const ffmpeg = spawn('ffmpeg', ['-loglevel', 'error', '-re', '-i', './talk.mp3', '-f', 'f32le', '-ac', '1', '-ar', '16000', '-']);
  ffmpeg.stdout.on('data', (chunk: Buffer) => ws.send(new Uint8Array(chunk)));
  ffmpeg.on('close', () => ws.send(JSON.stringify({ type: 'eof' })));
  process.once('SIGINT', async () => {
    ffmpeg.kill();
    await call('POST', `/v1/recordings/${grant.recording.id}/pause`);
    await call('POST', `/v1/recordings/${grant.recording.id}/stop`, {});
  });
}
```


---

All guides: https://api.vocalstack.com/llms.txt. Every operation: https://api.vocalstack.com/llms-full.txt. The OpenAPI document, with every WebSocket frame under `x-websocket`: https://api.vocalstack.com/v1/openapi.json.
