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

# Streaming transcription

> Real-time transcription over WebSocket with partial results and turn detection presets

`wss://api.withhopper.com/stt/websocket` transcribes audio as it arrives. Stream binary PCM16 frames and receive partial transcripts while the speaker is talking, then a final transcript when the turn ends. The model is [Nemotron ASR](/models#nemotron-asr), NVIDIA's open speech-to-text model, served by Hopper with turn detection built in. To transcribe a complete file in one HTTP request, use [batch transcription](/stt/transcription).

Authenticate at upgrade with `Authorization: Bearer sk_hopper_...`, or pass `?api_key=sk_hopper_...` from clients that cannot set headers.

## Connection parameters

All configuration is query parameters on the connect URL.

| Param                                                                       | Values                                                                                                     | Default        |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------- |
| `api_key`                                                                   | API key, alternative to the `Authorization` header                                                         | —              |
| `model`                                                                     | STT model id                                                                                               | `nemotron-asr` |
| `language`                                                                  | language hint, forwarded to the model                                                                      | none           |
| `keyterms`                                                                  | comma-separated list of terms to bias recognition toward                                                   | none           |
| `sample_rate`                                                               | int; must be in the model's allowlist (`nemotron-asr`: 8000, 16000, 22050, 24000, 44100, 48000)            | 16000          |
| `turn_detection`                                                            | `balanced` \| `patient` \| `responsive`; anything else fails the upgrade with 400 `invalid_turn_detection` | `balanced`     |
| `start_threshold`, `eager_end_threshold`, `end_threshold`, `end_timeout_ms` | numeric overrides applied on top of the chosen preset                                                      | preset values  |

## Turn detection presets

| Preset       | `start_threshold` | `eager_end_threshold` | `end_threshold` | `end_timeout_ms` | Why                                                                                     |
| ------------ | ----------------- | --------------------- | --------------- | ---------------- | --------------------------------------------------------------------------------------- |
| `balanced`   | 0.8               | 0.4                   | 0.2             | 5600             | Default trade-off between end-of-turn latency and cut-off risk.                         |
| `patient`    | 0.9               | 0.5                   | 0.3             | 10000            | Waits longer before finalizing a turn — for callers who pause mid-sentence.             |
| `responsive` | 0.6               | 0.3                   | 0.15            | 2800             | Cuts a turn earlier — lower end-of-turn latency, more risk of splitting a slow speaker. |

Start with a preset. Override individual thresholds only if the preset misses for your callers.

## Message protocol

Client to server:

* **Binary frames**: raw PCM16 mono audio at the negotiated `sample_rate`. 100 ms per frame is the reference pacing. These bytes are what gets metered.
* **Text frame `"END"`**: flush — the server finalizes any buffered audio.

Server to client:

* `{"type": "Ready"}` — sent once when the session is ready. Send audio after this.
* Transcript events: `{"text": "...", "is_final": false}` partials while a turn is in progress, `is_final: true` when it ends; final events carry `words` with per-word `start`/`end` timestamps when available.
* Text frame `"END"` — echoed after a flush completes; all finals have been delivered.

Full message schemas are in the [WebSocket reference](/api-reference/stt-websocket).

## Example

Stream a raw PCM16 file at real-time pace and print transcripts as they arrive.

```python theme={null}
import asyncio
import json
import websockets

URL = (
    "wss://api.withhopper.com/stt/websocket"
    "?api_key=sk_hopper_..."
    "&model=nemotron-asr&sample_rate=16000&turn_detection=balanced"
)

async def main():
    pcm = open("call.pcm", "rb").read()  # raw PCM16 mono, 16 kHz
    frame = 3200  # 100 ms: 16000 samples/s × 2 bytes × 0.1 s

    async with websockets.connect(URL) as ws:
        json.loads(await ws.recv())  # {"type": "Ready"}

        async def send():
            for i in range(0, len(pcm), frame):
                await ws.send(pcm[i : i + frame])
                await asyncio.sleep(0.1)  # real-time pacing
            await ws.send("END")  # flush buffered audio

        async def recv():
            async for msg in ws:
                if msg == "END":  # server flushed; all finals delivered
                    return
                event = json.loads(msg)
                tag = "final" if event.get("is_final") else "partial"
                print(f"[{tag}] {event['text']}")

        await asyncio.gather(send(), recv())

asyncio.run(main())
```

## Upgrade errors

Failures at upgrade time are written to the socket as a raw HTTP response with the standard [error envelope](/platform/errors) before any WebSocket frames.

| Status | Code                     | When                                       |
| ------ | ------------------------ | ------------------------------------------ |
| 400    | `invalid_turn_detection` | Unknown `turn_detection` value             |
| 400    | `invalid_sample_rate`    | `sample_rate` not in the model's allowlist |
| 401    | `invalid_api_key`        | Missing or bad key                         |
| 402    | `insufficient_quota`     | Zero balance                               |
| 429    | `rate_limit_exceeded`    | Over 600 requests/min for the key          |
| 502    | `upstream_unavailable`   | Transcription backend unreachable          |
| 503    | `model_offline`          | Model has no healthy upstream              |

## Billing

Streaming transcription costs \$0.30 per hour of audio, metered on the binary bytes you send: `audio_seconds = bytes / (sample_rate × 2)`. Usage settles against your balance every 60 s during the session and again at close.

<Warning>If the balance reaches zero mid-session, the server closes the connection with code **1008** ("insufficient credits").</Warning>
