Skip to main content
Realtime speech to text is in preview and is enabled per team. Contact support@fish.audio to turn it on for your team. Until then, connecting returns 403.

Prerequisites

Sign up for a free Fish Audio account to get started with our API.
  1. Go to fish.audio/auth/signup
  2. Fill in your details to create an account, complete steps to verify your account.
  3. Log in to your account and navigate to the API section
Once you have an account, you’ll need an API key to authenticate your requests.
  1. Log in to your Fish Audio Dashboard
  2. Navigate to the API Keys section
  3. Click “Create New Key” and give it a descriptive name, set a expiration if desired
  4. Copy your key and store it securely
Keep your API key secret! Never commit it to version control or share it publicly.
The SDKs do not wrap the realtime speech-to-text endpoint yet, so this recipe speaks the WebSocket protocol directly. Install a WebSocket client and a MessagePack library:
You also need a 16-bit PCM mono WAV file, at 8000, 12000, 16000, 22050, 24000, 32000, 44100 or 48000 Hz. To convert a recording, run ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le speech.wav.

Recipe

The script connects to wss://api.fish.audio/v1/asr/realtime, sends a config message, waits for session_started, and then streams the file in 100 ms messages at real-time speed, as a live microphone would. It prints every message the server sends until the session ends.
The audio goes in data as the raw PCM bytes, which must travel as MessagePack bin, not as base64 text: msgpack for Python packs bytes as bin by default (do not set use_bin_type=False), and @msgpack/msgpack packs a Buffer or Uint8Array as bin. The server closes a session after 30 seconds without any client message, but a WebSocket ping counts as one, and Python’s websockets sends a ping every 20 seconds by default, so the session stays open even if your audio stops; see Capture audio well for a live source that can stall. Run it with your API key in FISH_API_KEY:
For a 15-second customer-support clip, the output looks like this (... marks drafts left out):
The clip ends mid-sentence, so the last turn is committed by audio_done. Drafts can hold partial words, such as deli-; the committed text is final.

Read the events

  • speech_start opens a turn when voice activity begins. In the default auto mode, noise and other voices open turns too.
  • draft is the whole turn so far, and replaces the previous draft. Drafts come only with results.drafts: true; use them for live captions.
  • user_turn_taken says the server judged that the user is taking their turn, rather than noise or someone else nearby.
  • eager_end_of_turn comes with turn_detection.eager: true when the turn has probably ended. Its text equals the final transcript unless turn_resumed follows, so an agent can start its reply early.
  • committed is the final transcript of the turn. user_turn: false marks a non-user turn, such as background noise or a short acknowledgement while your agent is speaking. Speaker tags such as <|speaker:0|> are stable within the session; strip them and trim the text, or send results.diarize: false. Text can also contain sound tags such as [laughter]. language_code is omitted when the text is too short to tell, as for “Uh-huh.” above.
  • session_done follows audio_done, and the server then closes the connection with code 1000.
Ignore message types and fields you do not know: new ones can be added. Every message is described in the WebSocket reference.
The script streams the file at real-time speed so that the events arrive as they would for live audio. To transcribe a recording you already have, use POST /v1/asr instead; it is simpler and returns the whole transcript at once.

Capture from a browser microphone

The API key travels in a request header, and browsers cannot set headers on a WebSocket. Never put your API key in a web page: run a small relay on your server that adds the Authorization header and passes MessagePack frames through unchanged. The page then speaks the same protocol as the scripts above.
Install aiohttp, put both files in one folder, run python relay.py, and open http://localhost:8080.
  • The page captures audio at the device’s own sample rate and sends that rate in config; the server converts it, so the page does not resample. The rate must be one of the supported rates.
  • An AudioWorklet converts the samples to 16-bit PCM and posts about 100 ms per message.
  • Stop sends audio_done, so the server commits the last turn before it closes the connection.
  • The relay forwards the API’s close code to the page. In production, add your own user authentication, per-user session limits and input checks to the relay.