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

# Streaming voice sockets

> Stream text in and audio out, or audio in and transcripts out, over one WebSocket with Inworld TTS-2 and STT-1 — mint a session credential from the gateway, connect to the voice relay, billed per character or per audio second.

> [!NOTE]
>
> Streaming voice shares the realtime voice gate: contact your AI Reserve representative to have
> `realtimeVoiceEnabled` turned on for your organization — until then, session mints return `403`.

Streaming voice gives your application a long-lived WebSocket for one of two jobs:

* **Streaming text-to-speech** (`inworld-tts-2`, `inworld-tts-2-flash`): send sentences, or
  individual LLM tokens, as your model produces them and receive audio chunks back as they are
  synthesized, with several independent contexts on one socket.
* **Streaming speech-to-text** (`inworld-stt-1`): send microphone audio as it is captured and
  receive interim and final transcripts with turn boundaries.

Both are Inworld's own streaming sockets, carried by the same voice relay that serves
[realtime voice sessions](https://docs.aireserve.com/capabilities/realtime-voice.md) and minted by the same endpoint. The relay
speaks Inworld's native frames; nothing is translated, so Inworld's own client samples work once
the URL and credential are swapped. Billing, budgets and usage reporting are handled by AI Reserve
like every other surface.

## Models

| Model | Job | Pricing |
| - | - | - |
| `inworld-tts-2` | Text-to-speech | \$25 per 1M characters of text sent (UTF-16 code units) |
| `inworld-tts-2-flash` | Text-to-speech | \$15 per 1M characters of text sent |
| `inworld-stt-1` | Speech-to-text | \$0.15 per hour of connected audio, billed per second |

The per-character rate is the same rate the [Inworld HTTP passthrough](https://docs.aireserve.com/migrate/from-inworld.md)
books, so a character costs the same whichever transport carried it.

## How it works

1. **Mint** a session credential: `POST /v1/realtime/sessions` with the model (and, for TTS, the
   voice). The gateway authenticates you, checks the org gate, reserves worst-case funds from your
   wallet, and returns a short-lived `ephemeral_key` and a `wss_url` on the relay's `/v1/audio/tts`
   or `/v1/audio/stt` path.
2. **Connect** your WebSocket to `wss_url` with `ephemeral_key` and speak Inworld's frames. The
   relay opens Inworld's socket on your behalf with the platform credential; neither your AI
   Reserve key nor the Inworld key ever reaches your code.

## Step 1: Mint a session

```bash TTS theme={"dark"}
curl -X POST https://api.aireserve.com/v1/realtime/sessions \
  -H "Authorization: Bearer $AIRESERVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "inworld-tts-2", "voice": "Ashley"}'
```

```bash STT theme={"dark"}
curl -X POST https://api.aireserve.com/v1/realtime/sessions \
  -H "Authorization: Bearer $AIRESERVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "inworld-stt-1"}'
```

`voice` on a TTS session is an Inworld voice id: any stock voice, or one of **your organization's**
cloned or professionally cloned voices. It is pinned for the whole session — every `create` and
`updateConfig` you send has its `voiceId` replaced with it — so a ticket cannot be used to speak with
a voice the mint never authorized. Another organization's voice answers `404` at mint, or closes
the socket with code `1008` if the relay's own check refuses it. Defaults to `Ashley`. An STT session
takes no `voice` (sending one is a `400`).

The response is the realtime mint's: `wss_url` points at `/v1/audio/tts` or `/v1/audio/stt`,
`ephemeral_key` is a one-use HMAC ticket valid for five minutes, `max_session_seconds` is 3540. A
mint answers `503` with `Retry-After` when the deployment has no Inworld credential; nothing is
charged.

## Step 2: Connect and stream

**From a server**, pass the key as a bearer header; **from a browser**, offer the
`browser_subprotocols` the mint returned. Then speak Inworld's protocol.

### Text-to-speech

```javascript theme={"dark"}
const ws = new WebSocket(session.wss_url, { headers: { Authorization: `Bearer ${session.ephemeral_key}` } });
ws.onopen = () => {
  ws.send(JSON.stringify({ create: { autoMode: true, autoModeStrategy: "SENTENCE_BOUNDARY", audioConfig: { audioEncoding: "MP3" } }, contextId: "turn-1" }));
  ws.send(JSON.stringify({ send_text: { text: "Hello from AI Reserve." }, contextId: "turn-1" }));
  ws.send(JSON.stringify({ close_context: {}, contextId: "turn-1" }));
};
ws.onmessage = (event) => {
  const { result } = JSON.parse(event.data);
  if (result.audioChunk) play(Buffer.from(result.audioChunk.audioContent, "base64"));
};
```

Client frames: `create`, `send_text`, `flush_context`, `close_context`, `cancel_context` and
`updateConfig`, each with an optional `contextId`, exactly as Inworld documents them. `voiceId` and
`modelId` are set by the relay. `enableCustomPronunciation` is removed (it selects the shared
workspace dictionary, which is not available through AI Reserve); a frame selecting named
dictionaries (`pronunciationDictionarySettings`) closes the socket with `1008` — select dictionaries
on the HTTP synthesis endpoints instead. Any other frame, a non-JSON frame, or a `send_text` over
Inworld's 2000-unit limit closes the socket.

### Speech-to-text

```javascript theme={"dark"}
const ws = new WebSocket(session.wss_url, { headers: { Authorization: `Bearer ${session.ephemeral_key}` } });
ws.onopen = () => {
  ws.send(JSON.stringify({ transcribeConfig: { audioEncoding: "LINEAR16", sampleRateHertz: 16000, numberOfChannels: 1 }, audioProtocol: "BINARY" }));
  microphone.on("data", (pcm) => ws.send(pcm)); // raw binary frames
};
ws.onmessage = (event) => {
  const { result } = JSON.parse(event.data);
  if (result.transcript) render(result.transcript.text, result.transcript.isFinal);
};
```

The first frame must be `transcribeConfig` (the relay sets `modelId`); `audioProtocol: "BINARY"`
turns on raw binary audio frames, otherwise send base64 `audioChunk` frames. Then `endTurn` and
`closeStream` as Inworld documents. A binary frame before the config, or without `BINARY` selected,
closes the socket with `1008`.

## Billing

* **TTS** is billed on the characters of every `send_text` the relay forwards, UTF-16 code units
  as Inworld counts, at the model's rate. Inworld does not count text a `cancel_context` discards
  before synthesis; AI Reserve does, so an interruption-heavy client is charged for the last
  unsynthesized sentence — the conservative side. The mint reserves 3,000 characters a minute for
  the maximum session; the settle clamps at that reserve.
* **STT** is billed on connected seconds, like `grok-voice`. Inworld counts audio for as long as it
  keeps arriving, which for a live microphone is the connected session; a socket held open and
  silent is charged for the silence. Mute by not sending, or close the stream.
* The reserve is released and the actual charge booked by the settle sweep a few minutes after the
  socket closes; the row appears in your Event Log under `inworld-tts-2`, `inworld-tts-2-flash` or
  `inworld-stt-1` with provider `inworld`.

## Limits

* Sessions last at most 3540 seconds; Inworld closes a TTS socket after 10 minutes of inactivity.
* Frames are capped at 1 MiB; a client that outruns the relay's buffers is closed with `1013`.
* Concurrency is Inworld's account tier, shared across every organization on the platform.
* Inworld's speech-to-speech Realtime API is not served; use `grok-voice` or `eleven-realtime-*` on
  [realtime voice sessions](https://docs.aireserve.com/capabilities/realtime-voice.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.