Turn lifecycle
Between user turns, the session is idle. When the user begins speaking,turn.start fires, followed by turn.update events as the transcript builds.
The API emits these events to describe the state of the conversation.
The lifecycle comes with a few guarantees:
- The first event in every turn is
turn.start. turn.eager_endis always followed byturn.endorturn.resume.turn.resumeonly fires after a precedingturn.eager_end.turn.endalways closes a turn; the next turn begins with a newturn.start.
Transcript behavior
Thetranscript property is cumulative within a turn: it contains the full text transcribed so far in this user turn, not a delta. You do not need to concatenate partial results across events.
All emitted text is final: the model never revises text it has already sent. You can use the partial transcript from a turn.update the moment it arrives without worrying about it changing.
Example: one turn
The user says “Hi I need to cancel my subscription please.”turn.eager_end fires early, after “cancel,” but the user keeps going, so turn.resume follows. The second turn.eager_end is correct, and turn.end confirms it.
Configuring turn detection
Turn detection is driven by a single signal: at each moment, the model estimates the likelihood that the user is in an active turn, a value between0 and 1. The state machine compares this likelihood against three thresholds to decide when to fire turn.start, turn.eager_end, and turn.end.
Turn detection ships with defaults that work well for most voice agents. Tune it to optimize your agent’s conversational flow for your use case, balancing latency against how accurately the model detects where each turn starts and ends.
Each parameter trades latency against accuracy. The table below shows what moving it in either direction does.
The three thresholds are strictly ordered:
start_threshold > eager_end_threshold > end_threshold. In addition to the ranges above, each value is constrained by its neighbors to preserve this ordering, so you cannot set an eager end threshold above the start threshold or an end threshold above the eager end threshold.
Set these per connection with query parameters (turn_start_threshold, turn_eager_end_threshold, turn_end_threshold, turn_end_timeout_ms), or change them mid-session by sending a config command.
Common configurations
Below are some example configurations that make useful starting points. Pick the one closest to your use case, then tune individual thresholds from there.- Balanced is a good place to start and works well for many voice agent conversations.
- Responsive is best when latency is a high priority, such as fast conversational back-and-forth.
- Patient is best when accuracy is a high priority, such as when users pause to think or look up information, or where cutting them off mid-turn is costly.
Example code
Handleturn.start and turn.end to get a working agent: interrupt when the user starts speaking, and generate a reply when they finish. To cut latency, also handle turn.eager_end: start generating a reply the moment it fires, then cancel that work if turn.resume arrives, or play it the instant turn.end confirms the user is done.
Edge cases
No audio vs silence
Our API expects a continuous stream of audio. If you stop sending audio, the server will wait for more audio chunks to arrive rather than assuming that the user is silent. This is normally desired behavior to handle network lag, but it does mean that your client needs to send silence (all zeros) when your audio input is muted.Draining events
Once you are done sending all audio for a session, send{"type": "close"} to tell the model to flush any buffered audio and emit remaining events. The server will close the socket for you once the model is done.
The server buffers some audio to improve transcription accuracy. If you don’t send the close command or stop reading messages early, that buffered audio will not be processed. This is okay if you don’t care about the last second of audio.
Joining transcripts
Thetranscript field is cumulative within a turn — each turn.update, turn.eager_end, and turn.end event already holds the full text of the turn so far.
If you only care about the final transcript: take the transcript property from each turn.end, one per completed turn. Join transcript verbatim. Never strip() it, normalize it, or add your own separators.
turn.update and turn.eager_end events is a classic source of duplicated text: because each update is cumulative, joining them repeats parts of the transcript.
Consider turn.update and turn.eager_end as updates to the turn state, not transcript chunks.
Read turn.end only for the final transcript.
Where to go next
Try it out online
See turn detection in action with no sign-up or code required
Use the API
Start building with our Realtime STT API
Use the SDK
Take a look at some real code