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
Create a Fish Audio account
Create a Fish Audio account
Sign up for a free Fish Audio account to get started with our API.
- Go to fish.audio/auth/signup
- Fill in your details to create an account, complete steps to verify your account.
- Log in to your account and navigate to the API section
Get your API key
Get your API key
Once you have an account, you’ll need an API key to authenticate your requests.
- Log in to your Fish Audio Dashboard
- Navigate to the API Keys section
- Click “Create New Key” and give it a descriptive name, set a expiration if desired
- Copy your key and store it securely
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 defaultauto 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, andthink() 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
FISH_API_KEY:
Decide when to answer
Answer a turn whencommitted.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
Withturn_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_turnwith non-empty text, start preparing a reply. Do not sendagent_speaking: trueuntil you start playing it; waiting forcommittedbefore you play is the safe default. - On
turn_resumed, the user went on: cancel the prepared reply. A neweager_end_of_turncan follow. - On
committed, if you prepared a reply for this turn, use it; otherwise start one now.
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
Sendagent_speaking so the server can tell an acknowledgement from an interruption:
- Send
{"type": "agent_speaking", "speaking": true}when you commit to playing a reply: oncommitted, or, if you play a reply beforecommitted, 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. Ifturn_resumedcancels a reply that was already playing, stop it and sendfalse. - If a reply pauses for more than about 2 seconds, for example while a tool call runs, send
falsefor the pause andtruewhen 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.
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:- On
speech_start, duck the agent’s audio: the user may be interrupting. For the earliest signal, turn onresults.vad_eventsand duck onvadmessages instead. - On
user_backchannel, the user only acknowledged (“mm-hm”, “yeah”, “right”, “uh-huh”): restore the volume and keep talking. - On
user_turn_taken, the user is taking the turn: stop the agent and sendagent_speaking: false. - On
committedwithuser_turn: false, restore the volume. - 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_takencan come a few hundred milliseconds later than usual.
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 withconfigure, for example after you look up the caller’s account. - What your agent said: send each line your agent speaks as an
assistantitem withcontext_add, right after thecommittedit 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.historyinconfig.
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: truefromgetUserMedia. - Turn automatic gain control off where you can (
autoGainControl: falsein 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_speakingon 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 noeager_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
Replacethink() 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.

