Skip to content

Turns

The WebSocket API provides turn-level speech-to-text. You send audio data and receive events at the start and end of each turn, along with the transcript of each turn.

Use it for voice agents, where every millisecond of processing time counts: turn end candidates let the agent start its reply before the user has finished, and the turn start lets it stop talking when the user interrupts (barge-in). If you build voice agents with LiveKit, the LiveKit integration uses this endpoint for you. When in doubt, see Choosing an Endpoint.

Speaker diarization is not available on this endpoint.

A turn opens with a turn_start message when speech begins. As the model grows more confident that the turn is ending, it emits turn_end_candidate messages carrying the transcript so far - each new candidate replaces the previous one. A turn_end message confirms the last candidate as the final transcript of the turn.

End-of-turn detection is not based on silence alone: the model combines acoustic cues with the semantic content of what has been said. A thinking pause mid-sentence is less likely to end a turn than a pause after a completed thought.

  1. Connect - Open a WebSocket connection to wss://api.reson8.dev/v1/speech-to-text/turns with an authentication header.
  2. Configure - Use query parameters to set the audio encoding, language, phrases, and turn probabilities.
  3. Stream audio - Send audio data as binary WebSocket frames.
  4. Receive turn events - The server emits JSON text frames for turn start, candidate transcripts, and the final turn end.
  5. Optionally flush - End the current turn immediately with a flush request.
  6. Close - Close the WebSocket connection when done.

See the API reference for full details on messages, fields, and error codes.

Each turn event maps to one action in your agent:

Event What your agent does
First turn_end_candidate Start generating a reply from the candidate’s text, but don’t play it yet
Another turn_end_candidate Previous candidate is replaced: stop its reply and start one for this candidate. If the text is unchanged, keep the reply running
turn_end Play the reply generated from the last candidate
turn_start The user is speaking: stop any reply that is playing or about to play

Start generating a reply (LLM and text-to-speech) as soon as a turn_end_candidate arrives, so it is ready by the time the turn ends. Hold the output until turn_end: no event is sent when the user resumes speaking after a candidate, so only turn_end tells you the user is done.

Comparing the text of each new candidate pays off: the server often sends one last candidate right before turn_end, and it usually matches the previous one, so the reply you already started can keep running.

Only add a reply to your conversation history once it plays, so your LLM never refers to a discarded reply the user never heard. If the user interrupts, keep only the part that was played.

A turn_start while your agent is speaking means the user is interrupting (barge-in): stop playback, drop queued audio, and cancel the reply if it is still generating. Keep streaming microphone audio while the agent speaks, otherwise the server cannot hear the interruption.

turn_start is sent once the speech transcribes to actual words, so a cough or background noise does not interrupt your agent. Your agent’s own voice might, though: use echo cancellation so its audio does not reach the microphone.

generate_reply (your LLM plus text-to-speech) and play are placeholders for your own code; play should stop the audio when its task is cancelled.

import asyncio
import json
async def speak(reply):
audio = await reply # waits if generation is still running
await play(audio)
async def handle_events(ws):
reply_text = None # transcript the pending reply is generated from
reply = None # task generating the pending reply
speaking = None # task playing the committed reply
async for message in ws:
event = json.loads(message)
if event["type"] == "turn_start":
if speaking:
speaking.cancel() # barge-in: stop generating and playing
elif event["type"] == "turn_end_candidate":
if event["text"] != reply_text:
if reply:
reply.cancel() # replaced: stop the previous reply
reply_text = event["text"]
reply = asyncio.create_task(generate_reply(reply_text))
elif event["type"] == "turn_end" and reply:
speaking = asyncio.create_task(speak(reply)) # play the reply
reply_text, reply = None, None

Generation and playback run as separate tasks so the loop keeps reading events. A client that stops reading for 10 seconds is disconnected with close code 4000 (message_backlog).

With the default encoding=auto, the server detects the audio format from the container headers at the start of the stream; for raw audio, set encoding explicitly. See Audio Formats for the supported formats, streaming containers, and the reconnecting caveat.

The server auto-detects the language by default; use the language query parameter to pin one or constrain detection to a list. See Languages. To decide which language to pin on future connections, set include_language=true to receive the detected language on each turn_end_candidate.

Two query parameters tune turn detection: eager_turn_probability controls how early candidates appear, and final_turn_probability controls when the turn end is confirmed. A lower eager_turn_probability gives a voice agent a bigger head start, at the cost of more premature candidates and discarded LLM calls. A lower final_turn_probability ends turns sooner, but makes it more likely your agent replies while the user is only pausing. The defaults suit most agents.

The server sends a WebSocket ping frame every 30 seconds to keep the connection alive. This uses the built-in WebSocket ping/pong mechanism - clients should respond with a pong for every ping received. Most WebSocket libraries and browsers handle this automatically.

Send a flush_request text message when the client needs to finalize the current turn without waiting for turn-end detection, for example when the user releases a push-to-talk button:

{
"type": "flush_request"
}

For an active turn, the server responds with a final turn_end_candidate and then turn_end. See the API reference for the complete message format.