import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs";
import { Callout } from "zudoku/ui/Callout";

# Live API (Realtime)

The Live API streams audio over a WebSocket connection and returns incremental
transcription (and, optionally, translation) results as the audio arrives — no need to wait
for the full recording before you get text back.

<Callout type="tip" title="Migrating from OpenAI's Realtime API?">
  This API deliberately mirrors the shape of [OpenAI's Realtime
  API](https://developers.openai.com/api/docs/guides/realtime-websocket) (same connection
  path, same two-step auth flow, same client/server event names) so that existing Realtime
  integrations can switch to Vulavula largely by changing an endpoint. See [Differences from
  OpenAI's Realtime API](#differences-from-openais-realtime-api) below for the parts that
  don't carry over.
</Callout>

## Connecting

Connecting has two steps: mint a short-lived client secret from your backend, then open the
WebSocket from your client using that secret.

### Step 1 — Mint a client secret

`POST` [https://api.lelapa.ai/v1/realtime/client_secrets](https://api.lelapa.ai/v1/realtime/client_secrets)

This call must be made **server-side** — it requires your real `X-CLIENT-TOKEN` and should
never be embedded in browser or mobile client code. Your backend calls this endpoint, then
hands the returned short-lived `value` to the client that will actually open the WebSocket.

#### Headers

| Header | Type | Required | Description |
| --- | --- | --- | --- |
| `X-CLIENT-TOKEN` | string | Yes | API token generated for the project |
| `Content-Type` | string | Yes | Must be set to `application/json` |

#### Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `expires_after.anchor` | string | No | Currently only `"created_at"` (default) |
| `expires_after.seconds` | integer | No | Client secret lifetime in seconds (default `600`, min `10`, max `7200`) |
| `session` | object | No | Initial session config — see [Session Configuration](#session-configuration). Can also be sent later via `session.update`. |

<details>
  <summary>🟢 `200 OK`</summary>
  <div>
    <div>Returns the client secret plus the effective session config.</div>

```json
{
  "value": "vv_ek_68af296e8e408191a1120ab6383263c2",
  "expires_at": 1756310470,
  "session": {
    "id": "sess_abc123",
    "type": "translation",
    "audio": {
      "input": { "format": { "type": "audio/pcm", "rate": 24000 } },
      "output": { "language": null }
    }
  }
}
```
  </div>
</details>

<details>
  <summary>🟠 `401 Unauthorized`</summary>
  <div>
    <div>The client token is missing or invalid.</div>
  </div>
</details>

### Step 2 — Open the WebSocket

`WSS` [wss://api.lelapa.ai/v1/realtime](https://api.lelapa.ai/v1/realtime)

Browsers cannot set arbitrary headers during a WebSocket handshake, so authentication is
passed via the `Sec-WebSocket-Protocol` field instead — the same workaround OpenAI's Realtime
API uses. Pass an array of subprotocols to your WebSocket client:

```json
["realtime", "vulavula-insecure-api-key.vv_ek_68af296e8e408191a1120ab6383263c2"]
```

<Callout type="info">
  The `vulavula-insecure-api-key.` prefix is named that way deliberately (mirroring OpenAI's
  own `openai-insecure-api-key.` naming) — it's a reminder that this value is a bearer
  credential that will be visible to anything that can see the WebSocket handshake (e.g.
  browser devtools), which is exactly why it should be short-lived and minted server-side
  rather than a copy of your real `X-CLIENT-TOKEN`.
</Callout>

If you're connecting from a trusted backend process (not a browser), you can skip the
client-secret step entirely and pass your `X-CLIENT-TOKEN` directly the same way — but the
two-step flow is required for any client-side code.

## Session Configuration

Send a `session.update` event right after connecting (or include `session` in the
client-secret request) to configure the session:

```json
{
  "type": "session.update",
  "session": {
    "audio": {
      "input": {
        "format": { "type": "audio/pcm", "rate": 24000 },
        "transcription": { "model": "vulavula-live", "language": "zul" }
      },
      "output": { "language": "eng" }
    }
  }
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `audio.input.format.type` | string | No | Input audio encoding: `"audio/pcm"` (default), `"audio/pcmu"` (G.711 μ-law), or `"audio/pcma"` (G.711 A-law) — the same set OpenAI's Realtime API accepts. |
| `audio.input.format.rate` | integer | No | Input sample rate in Hz for `"audio/pcm"` (default `24000`; `16000` is also supported). Ignored for `"audio/pcmu"`/`"audio/pcma"`, which are always 8kHz. |
| `audio.input.noise_reduction` | object \| null | No | Accepted for compatibility but not implemented — any value is logged and ignored. |
| `audio.input.transcription.language` | string | No | Source language — see [Supported Language Codes](#supported-language-codes) below. Defaults to `zu` (isiZulu) if omitted. |
| `audio.input.transcription.model` | string | No | Accepted for compatibility but not implemented — this API runs one fixed ASR model; any value is logged and ignored. |
| `audio.output.language` | string \| null | No | Target language to translate into — see [Supported Language Codes](#supported-language-codes) below. **Omit or set to `null` for transcription-only** — this is the single switch between the two modes. |

### Supported Language Codes

Language codes are **ISO 639-1** (two-letter) or **ISO 639-3** (three-letter), matching
[OpenAI's Speech-to-Text convention](https://developers.openai.com/api/docs/guides/speech-to-text#supported-languages)
— either form works.

| Language | ISO 639-1 | ISO 639-3 | Transcribe | Translate target |
| --- | --- | --- | --- | --- |
| isiZulu | `zu` | `zul` | ✅ | ✅ |
| Sesotho | `st` | `sot` | ✅ | ✅ |
| Afrikaans | `af` | `afr` | ✅ | ✅ |
| South African English | `en` | `eng` | ✅ | ✅ |
| African French | `fr` | `fra` | ✅ | — |
| Northern Sotho | — | `nso` | — | ✅ |
| Swati | `ss` | `ssw` | — | ✅ |
| Tsonga | `ts` | `tso` | — | ✅ |
| Tswana | `tn` | `tsn` | — | ✅ |
| Xhosa | `xh` | `xho` | — | ✅ |
| Swahili | `sw` | `swh` | — | ✅ |

<Callout type="info">
  Code-switched isiZulu (alpha) is available for transcription via the Vulavula-specific
  extension code `cs-zul` — this one isn't part of ISO 639 since it's not a single language.
</Callout>

## Client Events

| Event | Description |
| --- | --- |
| `session.update` | Set the session configuration (see above). Only the *first* `session.update` (or the config given at client-secret creation) takes effect — config is fixed once audio streaming starts, and a later `session.update` gets an `error` instead. |
| `session.input_audio_buffer.append` | Append audio to the input buffer. `audio` is base64-encoded mono audio in whatever `audio.input.format` was configured. |
| `session.close` | Gracefully close the session after flushing any pending audio. |

<Tabs defaultValue="py">
  <TabsList>
    <TabsTrigger value="py">Python</TabsTrigger>
    <TabsTrigger value="js">JavaScript</TabsTrigger>
  </TabsList>
  <TabsContent value="py">

```python showLineNumbers
import base64
import json

await ws.send(json.dumps({
    "type": "session.input_audio_buffer.append",
    "audio": base64.b64encode(pcm16_chunk).decode(),
}))
```

  </TabsContent>
  <TabsContent value="js">

```js showLineNumbers
ws.send(JSON.stringify({
  type: "session.input_audio_buffer.append",
  audio: base64Encode(pcm16Chunk),
}));
```

  </TabsContent>
</Tabs>

## Server Events

| Event | Description |
| --- | --- |
| `session.created` | Sent automatically as the first event after the WebSocket opens. |
| `session.updated` | Confirms a `session.update` was applied. Same shape as `session.created`. Only sent for the *first* `session.update` on a session — config is fixed once audio streaming starts, so later `session.update`s get an `error` (`session_already_active`) instead. |
| `session.input_transcript.delta` | The finalized **source-language** text for one completed segment of speech. `delta` is safe to append to a running transcript — segments are sent once each, only after the model has settled on that segment's text (interim/revisable hypotheses are not sent). |
| `session.output_transcript.delta` | The finalized **translated** text for one completed segment. Only sent when `audio.output.language` is configured. Same append-safety guarantee as above. |
| `session.closed` | Sent after a `session.close` (or server-initiated close) once the session has fully ended. |
| `error` | A recoverable or non-recoverable error. See `error.message`/`error.code` for details. |

<details>
  <summary>Example: <code>session.input_transcript.delta</code></summary>
  <div>

```json
{
  "type": "session.input_transcript.delta",
  "event_id": "event_123",
  "delta": "Sawubona"
}
```

  </div>
</details>

<details>
  <summary>Example: <code>session.output_transcript.delta</code></summary>
  <div>

```json
{
  "type": "session.output_transcript.delta",
  "event_id": "event_124",
  "delta": "Hello"
}
```

  </div>
</details>

<details>
  <summary>Example: <code>error</code></summary>
  <div>

```json
{
  "type": "error",
  "event_id": "event_125",
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_event",
    "message": "The 'type' field is missing.",
    "param": null
  }
}
```

  </div>
</details>

## Differences from OpenAI's Realtime API

This API is intentionally close to OpenAI's Realtime API, but it is not a byte-for-byte
clone. Known gaps:

- **No WebRTC transport.** WebSocket only — OpenAI recommends WebRTC for browser clients;
  we don't currently offer that transport.
- **No translated-audio output.** Vulavula's translation is text-only. OpenAI's translation
  mode can also stream synthesized translated speech (`session.output_audio.delta`); this
  API never emits that event.
- **One unified session type**, not separate transcription-only and translation resources.
  Toggle translation on/off per session with `audio.output.language`, rather than connecting
  to a different endpoint. `session.type` is always reported as `"translation"`, matching
  OpenAI's translation session discriminator, regardless of whether translation is enabled
  for that session.
- **Different subprotocol prefix.** `vulavula-insecure-api-key.` instead of
  `openai-insecure-api-key.` — same mechanism, different namespace.

## Full Example

See the runnable Python and browser examples in
[`vulavula-examples`](https://github.com/Lelapa-AI/vulavula-examples) for a complete
end-to-end client.
