Skip to main content
Every message exchanged over the Voice Agent API WebSocket, grouped by direction. You’ll send session.update to configure, input.audio to stream mic audio, tool.result to respond to tool calls, and session.end to cleanly end the call. The server streams everything else back, including session.ended on every clean teardown.

Event flow

A typical voice agent session moves through the events in this order:

Client → Server

input.audio

Stream PCM16 audio to the agent.
See Audio format for the full format specification.

session.update

Configure the session. Send immediately on WebSocket connect (before session.ready). Can also be sent mid-conversation to update most fields. See Mutability after session.ready for which fields can change once the session is established.
All fields are optional. Include only what you want to set or change. After session.ready, only a subset of fields can be changed; changing greeting, session.output.voice, or session.output.format raises immutable_field. session.output.volume is mutable mid-session.

session.resume

Reconnect to an existing session using the session_id from a previous session.ready. Preserves conversation context across dropped connections.
Sessions are preserved for 30 seconds after every disconnection before expiring. If the session has expired, the server returns a session.error with code session_not_found or session_forbidden. Start a fresh connection without session.resume.
Example. Capture session_id from session.ready on the first connection, then send session.resume as the first message when reconnecting:

session.end

End the session cleanly. Send this when the call is over and you don’t intend to reconnect. The server emits a final session.ended and closes the WebSocket; the session_id is dead immediately and cannot be resumed.
No other fields. What happens next:
  1. The server emits session.ended.
  2. The server closes the WebSocket.
  3. The session_id is dead. Sending session.resume with it returns session_not_found.
When to send it vs. just closing the socket. If you just close the WebSocket, the server holds the session for 30 seconds so you can session.resume from a new connection, and that 30-second window is billable. session.end short-circuits the grace window and stops billing immediately.

tool.result

Send a tool result back to the agent. Send this when reply.done is the latest event you’ve received (and nothing has happened since). The simplest pattern is to accumulate on tool.call and drain inside the reply.done handler. See Client-side tools.

reply.create

Ask the agent to generate a reply right now, optionally with custom instructions. Useful for delivering status updates during long-running hold-mode tool calls, or any time you want the agent to speak without a user utterance triggering it.
The agent generates a normal reply (reply.startedreply.audiotranscript.agentreply.done) using the provided instructions on top of the existing system prompt and conversation history.

Server → Client

session.ready

Session is established and ready to receive audio. Save session_id for reconnection. Start sending input.audio only after this event.

session.updated

Sent after session.update is applied successfully.

session.ended

Final event emitted on every clean teardown, right before the server closes the WebSocket. Always handle it.
When you’ll see it:
  • You sent session.end.
  • The session hit max_session_duration_seconds.
  • The server hit an unrecoverable error.
  • You disconnected and the 30-second grace window expired.
How to use it:
  • Got session.ended before the WebSocket closed: clean server-initiated close. Done. Don’t try to resume.
  • WebSocket closed without session.ended: probably a network drop. If you want to keep going, reconnect and call session.resume with your saved session_id within 30 seconds.

input.speech.started

Turn detection determined the user has started speaking.

input.speech.stopped

Turn detection determined the user has stopped speaking.

transcript.user.delta

Partial transcript of what the user is saying, updating in real-time.
Live user transcripts pause while a hold-mode tool is in flight and resume once the hold ends. Anything the user said during the hold is preserved in the conversation context.

transcript.user

Final transcript of the user’s utterance.

reply.started

Agent has begun generating a response.

reply.audio

A chunk of the agent’s spoken response as base64 PCM16. Decode and play immediately.
See Audio format for playback guidance.

transcript.agent

Full text of the agent’s response, sent after all audio for the response has been delivered. If the agent was interrupted, interrupted is true and text contains only what was actually spoken before the interruption.

reply.done

Agent has finished speaking. The optional status field indicates why the reply ended.

tool.call

Agent wants to call a registered tool. arguments is a dict, ready to use directly as-is.
See Tool calling for the full pattern.

session.error

Session or protocol error. The payload always includes type, timestamp, code, and message. Some errors (like session.update validation failures) also include a param field naming the offending field.
Connection and handshake errors Sent before or instead of session.ready. The WebSocket closes after these with the indicated close code. Session resume errors Sent when session.resume fails. The WebSocket closes after these. Agent startup errors Sent after the WebSocket is accepted but before session.ready. Client message errors Sent on the open socket when an inbound message is invalid. The session stays alive (except session_expired). Live session errors
If the server cancels the session due to an internal error, the WebSocket closes with code 1011 without any session.error payload. In browsers, pre-handshake failures (like UNAUTHORIZED) surface as a close event with code 1006. You won’t receive a session.error. Always fetch a fresh token immediately before each connection attempt.

Interruptions

When the user speaks mid-response (barge-in), the server stops the agent and emits reply.done with status: "interrupted" and transcript.agent with interrupted: true. The decision is semantic. Back-channels like “uh-huh” don’t trigger an interruption. On reply.done with status: "interrupted":
  1. Flush your local audio playback buffer.
  2. Discard any pending tool.result accumulators from the just-ended reply.
  3. Restart the playback stream so it’s ready for the next response.
See Turn detection and interruptions for how the model decides what counts as an interruption, and Handling interruptions for the platform-specific flush pattern.