> ## 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.

# Speech to text

Hopper transcribes English audio with Nemotron ASR (`nemotron-asr`), as a file upload or as a live WebSocket stream.

## Transcribe a file

`POST /v1/audio/transcriptions` takes a `multipart/form-data` upload and returns the transcript with word timestamps.

* `file` (file, required) — PCM16 mono or WAV, up to 25 MB. Compressed audio is rejected; decode it first.
* `model` (string, required) — `nemotron-asr`.
* `sample_rate` (integer, optional, default 16000) — One of 8000, 16000, 22050, 24000, 44100, 48000.

<CodeGroup>
  ```python Python theme={null}
  import os

  import requests

  response = requests.post(
      "https://api.withhopper.com/v1/audio/transcriptions",
      headers={"Authorization": f"Bearer {os.environ['HOPPER_API_KEY']}"},
      files={"file": open("call.wav", "rb")},
      data={"model": "nemotron-asr", "sample_rate": 16000},
  )
  print(response.json()["text"])
  ```

  ```typescript TypeScript theme={null}
  import { readFile } from "node:fs/promises";

  const form = new FormData();
  form.append("file", new Blob([await readFile("call.wav")]), "call.wav");
  form.append("model", "nemotron-asr");
  form.append("sample_rate", "16000");

  const response = await fetch(
    "https://api.withhopper.com/v1/audio/transcriptions",
    {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.HOPPER_API_KEY}` },
      body: form,
    },
  );
  const { text, words, duration_seconds } = await response.json();
  ```

  ```bash cURL theme={null}
  curl https://api.withhopper.com/v1/audio/transcriptions \
    -H "Authorization: Bearer $HOPPER_API_KEY" \
    -F file=@call.wav \
    -F model=nemotron-asr \
    -F sample_rate=16000
  ```
</CodeGroup>

The response carries the transcript, one entry per word with its start and end offset in seconds, and the duration of the audio.

```json theme={null}
{
  "text": "thanks for calling, how can I help",
  "words": [{ "word": "thanks", "start": 0.12, "end": 0.34 }],
  "duration_seconds": 12.345
}
```

## Stream live audio

`wss://api.withhopper.com/stt/websocket` transcribes audio as it arrives. Authenticate with the `Authorization` header, or with the `api_key` query parameter from clients that cannot set headers.

* `api_key` (string, optional) — Your key, when no `Authorization` header is sent.
* `model` (string, optional, default `nemotron-asr`) — STT model id.
* `sample_rate` (integer, optional, default 16000) — One of 8000, 16000, 22050, 24000, 44100, 48000.
* `turn_detection` (string, optional, default `balanced`) — `balanced`, `patient` to wait longer before ending a turn, or `responsive` to end it sooner.

The server sends `{"type": "Ready"}` once, then you send binary PCM16 mono frames and receive `{"text", "is_final", "words"}` events — `is_final` marks the end of a turn. Send the text frame `END` to flush; the server replies with `END` when every final has been delivered.

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

import websockets

URL = (
    "wss://api.withhopper.com/stt/websocket"
    f"?api_key={os.environ['HOPPER_API_KEY']}"
    "&model=nemotron-asr&sample_rate=16000&turn_detection=balanced"
)


async def main():
    pcm = open("call.pcm", "rb").read()  # raw PCM16 mono at 16 kHz
    frame = 3200  # 100 ms: 16000 samples/s x 2 bytes x 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")

        async def receive():
            async for message in ws:
                if message == "END":
                    return
                event = json.loads(message)
                print("final" if event["is_final"] else "partial", event["text"])

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


asyncio.run(main())
```

A session is billed on the audio bytes it receives, settled every 60 seconds and at close. If the balance reaches zero mid-session the server closes the socket with code 1008.

Error codes are listed in [Errors](/errors).
