Skip to main content
This guide is for building your own voice agent on the realtime speech-to-text API. To have Fish Audio run the whole agent for you, see Agents.
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.
Install a WebSocket client and a MessagePack library:
Read Transcribe a live audio stream first if you have not used the realtime endpoint before.

How a conversation flows

A voice agent has to answer three questions while audio streams in: has the user finished, is a sound the user talking to the agent at all, and is the user interrupting or just saying “mm-hm”? In the default auto mode, the server answers them with events. This is what the recipe below receives for a 15-second clip in which the user asks a question, says “uh-huh” while the agent answers, and then interrupts: Elapsed times are measured from the start of the script, which connects about 0.6 seconds before the audio starts.

Recipe

The microphone is a WAV file streamed in real time, and think() and speak() are placeholders for your LLM and your text-to-speech playback. Everything else is the agent loop as you would ship it.
voice_agent.py
Run it with your API key in FISH_API_KEY:
The recipe keeps a reply’s full text in its history even when the user interrupts it. In production, keep only the part the user heard.

Decide when to answer

Answer a turn when committed.user_turn is true and its text, without speaker tags, is not empty. Do not decide by reason: a turn can end for several reasons, and non-user turns end with eot too. If user_turn is ever missing, treat it as true. A turn with user_turn: false is not the user talking to your agent: usually background noise, a TV or another person nearby (turn_kind: "other"), or an acknowledgement while your agent was speaking (turn_kind: "backchannel"). Its text can be shown dimmed in a transcript, or left out. In auto mode, short pauses in the middle of a sentence usually do not end the turn: the server judges from what was said and how whether the speaker has finished. A speaker who stops mid-sentence and does not go on has the turn ended about 2 seconds later. silence_timeout_ms (default 5000) is only a safety net.

Start early with eager end of turn

With turn_detection.eager: true, the server sends eager_end_of_turn once the turn has probably ended, before committed. Its text is exactly what committed will say unless turn_resumed follows, so you can start your LLM on it:
  • On eager_end_of_turn with non-empty text, start preparing a reply. Do not send agent_speaking: true until you start playing it; waiting for committed before you play is the safe default.
  • On turn_resumed, the user went on: cancel the prepared reply. A new eager_end_of_turn can follow.
  • On committed, if you prepared a reply for this turn, use it; otherwise start one now.
Not every turn has an eager_end_of_turn, and it can come right before committed, so your agent must also work without it. In the clip above, the eager signal came about half a second before the final transcript.

Tell the server when your agent speaks

Send agent_speaking so the server can tell an acknowledgement from an interruption:
  • Send {"type": "agent_speaking", "speaking": true} when you commit to playing a reply: on committed, or, if you play a reply before committed, when playback starts. The wait for your speech synthesis and the gaps between sentences count as speaking. If the first audio is more than about 2 seconds away (a slow LLM or a tool call), treat the wait as a pause, as below.
  • Send {"type": "agent_speaking", "speaking": false} when the last audio sample has played to the user, not when synthesis finishes, or when you stop the reply. If turn_resumed cancels a reply that was already playing, stop it and send false.
  • If a reply pauses for more than about 2 seconds, for example while a tool call runs, send false for the pause and true when it goes on.
  • Send it on the same connection as your audio, at the moment the state changes, and only when it changes. It applies at its position in the audio stream.
Without agent_speaking, every acknowledgement takes the turn, and you have to tell “mm-hm” from an interruption yourself.

Handle interruptions and backchannels

While your agent is speaking:
  1. On speech_start, duck the agent’s audio: the user may be interrupting. For the earliest signal, turn on results.vad_events and duck on vad messages instead.
  2. On user_backchannel, the user only acknowledged (“mm-hm”, “yeah”, “right”, “uh-huh”): restore the volume and keep talking.
  3. On user_turn_taken, the user is taking the turn: stop the agent and send agent_speaking: false.
  4. On committed with user_turn: false, restore the volume.
  5. Cap any duck on your side to a few seconds: a turn over continuous background noise can last up to 20 seconds. When the user starts talking over such noise, user_turn_taken can come a few hundred milliseconds later than usual.
In vad and manual mode there is no user_turn_taken or user_backchannel: stop on speech_start instead.

Give the model context

The server uses the conversation to recognize names and follow-ups:
  • Custom vocabulary: send names, products and jargon in context.terms (up to 100 terms of up to 32 characters). Replace the list during the session with configure, for example after you look up the caller’s account.
  • What your agent said: send each line your agent speaks as an assistant item with context_add, right after the committed it answers. The recipe does this when a reply starts.
  • Conversation history: committed user turns are added automatically; non-user turns are not. Speaker labels stay the same for the whole session.
  • Prior conversation: to continue a conversation in a new session, pass earlier lines in context.history in config.

Capture audio well

  • Keep echo cancellation on. Without it, the agent’s own voice reaches the microphone, opens turns and can be taken for the user. In a browser, request echoCancellation: true from getUserMedia.
  • Turn automatic gain control off where you can (autoGainControl: false in a browser), so quiet background sound is not amplified between phrases.
  • Test with headphones first. They keep the agent’s voice out of the microphone, so you can check turn-taking before you tune echo cancellation.
  • Send audio as you capture it, mono 16-bit PCM in 80–100 ms messages, at the device’s own rate if it is one of the supported rates. The server converts the rate, so do not resample. Telephony audio at 8 kHz is supported; decode μ-law or A-law call audio to 16-bit PCM first.
  • Keep keys on your server. In a browser, capture audio and relay it through your server, as in Capture from a browser microphone. Send agent_speaking on that same connection so it stays in order with the audio.

Languages

Turn detection, including eager end of turn, backchannel detection and background awareness, is trained on a set of major languages. Transcription and language identification cover many more languages. In languages outside that set, a turn ends about 2 seconds after the speaker stops and there is no eager_end_of_turn. For faster turns in those languages, use vad mode with a shorter silence_timeout_ms, or end turns yourself with finalize.

Connect your LLM and speech

Replace think() with a call to your LLM, and speak() with text to speech and playback. To start speaking before the LLM has finished, stream its tokens into the TTS WebSocket and play audio chunks as they arrive. Send agent_speaking: false when the last chunk has played, not when synthesis ends.