Reference
Voice agent session reference
Every client and server event in a voice agent session, with payload shapes and close codes.
Voice agent sessions speak JSON text frames in both directions; binary WebSocket frames are ignored. Every frame has a type field, and audio travels base64-encoded inside the JSON.
/v2/voice-agent/wsAuthenticated with a short-lived session token| Parameter | Required | Default | Notes |
|---|---|---|---|
token | yes | none | Session token from POST /v2/voice-agent/token. Connect before expires_at. |
model | no | whisper-voicekit-agent | The voice agent model. Pass the same value used when minting the token. |
max_session_duration_sec | no | service ceiling | Session cap in seconds, 60 to 10800, clamped to the service ceiling (55 minutes). Pass the same value used when minting the token. |
Event flow
- The client connects and sends
session.updateas the first frame. - The server emits
session.readywith thesession_id. - The client streams
input.audioframes continuously. - While the caller speaks, the server emits
input.speech.started,transcript.user.deltapartials, theninput.speech.stoppedand a finaltranscript.user. - The server replies:
reply.started, a stream ofreply.audiochunks,transcript.agent, andreply.done. - When tools are configured the server may emit
tool.callmid-reply; the client answers withtool.resultand the reply continues. - The client sends
session.end; the server emitssession.endedand closes the connection.
Client to server events
| Event | Purpose |
|---|---|
session.update | Configure the agent. Must be the first frame; can also update settings mid-session. |
input.audio | Stream a chunk of caller audio as base64 PCM16. |
tool.result | Return the output of a tool.call. |
reply.create | Ask the agent to speak proactively with fresh instructions. |
session.end | Finalize the session immediately. |
session.update
Defines the agent. Send it as the first frame after connecting; the server confirms mid-session updates with session.updated.
{
"type": "session.update",
"session": {
"system_prompt": "You are a friendly support agent for Acme Internet. Keep answers short and confirm the account before making changes.",
"greeting": "Hi, thanks for calling Acme. How can I help today?",
"input": { "format": { "encoding": "audio/pcm" } },
"output": { "format": { "encoding": "audio/pcm" } },
"tools": [
{
"type": "function",
"name": "get_account_status",
"description": "Look up the status of a customer account",
"parameters": {
"type": "object",
"properties": { "account_id": { "type": "string" } },
"required": ["account_id"]
}
}
]
}
}| Field | Type | Notes |
|---|---|---|
session.system_prompt | string | Instructions that define the agent's persona, rules, and task. |
session.greeting | string | Opening line the agent speaks as soon as the session starts. |
session.input.format | object | Input audio format, for example { "encoding": "audio/pcm" }. |
session.input.keyterms | array | Domain terms to bias speech recognition toward. |
session.input.turn_detection | object | Turn-taking sensitivity tuning. |
session.output.voice | string | Voice used for the agent's spoken replies. |
session.output.format | object | Output audio format for reply.audio chunks. |
session.output.volume | number | Playback volume for agent replies. |
session.tools | array | Function tools the agent may call, declared with JSON Schema parameters. |
input.audio
{
"type": "input.audio",
"audio": "<base64 encoded PCM16 audio>"
}audio is a base64-encoded chunk of 16-bit PCM. Send frames continuously after session.ready; the server handles speech detection and turn taking.
tool.result
{
"type": "tool.result",
"call_id": "call_5d1a2b",
"result": "{\"status\": \"active\", \"plan\": \"fiber_500\"}"
}| Field | Type | Notes |
|---|---|---|
call_id | string | Copied from the tool.call you are answering. |
result | string | Tool output. Serialize structured data as a JSON string. |
reply.create
{
"type": "reply.create",
"instructions": "Let the caller know the account check is still running."
}Triggers a spoken reply without waiting for caller speech, guided by instructions. Useful for filling silences, for example while a slow tool runs.
session.end
{ "type": "session.end" }Finalizes the session immediately. The server responds with session.ended and closes the socket. No fields.
Server to client events
| Event | When | Purpose |
|---|---|---|
session.ready | after session.update | The agent is live. Carries the session_id. |
session.updated | after a mid-session update | Acknowledges a configuration change. |
input.speech.started | while streaming | The caller started speaking. |
input.speech.stopped | while streaming | The caller stopped speaking. |
transcript.user.delta | while the caller speaks | Partial transcript of the current caller turn. |
transcript.user | after a caller turn | Final transcript of the caller's turn. |
reply.started | before agent speech | The agent began composing a reply. |
reply.audio | during agent speech | A chunk of agent speech as base64 PCM16. |
transcript.agent | with agent speech | Text of what the agent said. |
reply.done | after agent speech | The reply finished, or was interrupted. |
tool.call | mid-reply | The agent requests a tool execution. |
session.ended | at the end | The session is finalized. Last event before close. |
session.error | on failure | Something went wrong. |
session.ready
{
"type": "session.ready",
"session_id": "sess_9f2c81"
}Sent once the agent has accepted your configuration. Do not send input.audio before it arrives.
transcript.user.delta and transcript.user
{
"type": "transcript.user.delta",
"text": "is my internet plan"
}{
"type": "transcript.user",
"text": "What is my internet plan called?",
"item_id": "item_2b8c4d"
}Deltas stream while the caller talks; the final transcript.user carries the full turn and an item_id you can use to correlate with the agent's reply.
reply.audio
{
"type": "reply.audio",
"data": "<base64 encoded PCM16 audio>"
}data is a base64-encoded chunk of 16-bit PCM agent speech in your configured output format. Decode and play chunks in arrival order.
transcript.agent
{
"type": "transcript.agent",
"text": "You are on the Fiber 500 plan.",
"reply_id": "reply_7e3f90",
"item_id": "item_2b8c4d",
"interrupted": false
}The text of the agent's reply. interrupted is true when the caller talked over the agent and the reply was cut short.
reply.done
{ "type": "reply.done", "status": "interrupted" }Marks the end of a reply. status is present when the reply did not complete normally, for example interrupted when the caller barged in; stop playback of any buffered reply.audio when that happens.
tool.call
{
"type": "tool.call",
"call_id": "call_5d1a2b",
"name": "get_account_status",
"arguments": { "account_id": "acct_1042" }
}| Field | Type | Notes |
|---|---|---|
call_id | string | Unique id for this call. Echo it in tool.result. |
name | string | Name of the tool, as declared in session.tools. |
arguments | object | Arguments matching the tool's JSON Schema parameters. |
session.ended
{
"type": "session.ended",
"session_duration_seconds": 312.4,
"audio_duration_seconds": 296.8,
"timestamp": 1784721600.512
}The final event of every session, with wall-clock and audio duration accounting. The socket closes right after it.
Ending a session
| How it ends | What happens |
|---|---|
You send session.end | The session finalizes immediately, session.ended is emitted, and the socket closes. |
| You close or lose the WebSocket | The service finalizes the session for you. Sessions never outlive the connection and are not resumable. |
| The session cap is reached | The service finalizes the session and closes the socket. Start a new session to continue. |
In all three cases billing stops when the connection ends. Prefer an explicit session.end so you receive session.ended and its duration accounting.
Errors and close codes
| Close code | Meaning |
|---|---|
4401 | The token is missing, expired, or not a voice agent session token. Mint a new token. |
4400 | The model query parameter is unknown or not a voice agent model. |
Failures during a session arrive as session.error events. When a session cannot start at all (for example the service is temporarily unavailable), you receive a session.error with only a message and the socket closes.
{
"type": "session.error",
"code": "invalid_format",
"message": "Invalid message format"
}| Field | Notes |
|---|---|
code | Machine-readable error code, when available. |
message | Human-readable description of what went wrong. |