Handshake
The first message on the WebSocket — client to server — must be a handshake. The server replies with handshake_ack and the conversation can begin.
Client → server: handshake
{
"type": "handshake",
"content_type": "json",
"content": {
"conversationId": "optional — resume an existing conversation",
"conversationType": "text" | "audio",
"audioConfiguration": {
"input": { "format": "...", "sampleRate": ..., "frameDurationMs": ... },
"output": { "format": "...", "sampleRate": ..., "frameDurationMs": ... }
},
"capabilities": ["local_tools_discover"]
}
}
| Field | Required | Type | Notes |
|---|---|---|---|
conversationId | no | string | If you pass one and it exists for this agent, the conversation is resumed. Otherwise a new one is created. See Reconnect. |
conversationType | no | "text" | "audio" | Defaults to "text". |
audioConfiguration | no | object | Optional for voice (defaults to PCM/8000/8000 if omitted). Ignored for text. |
capabilities | no | string array | Optional list of optional client capabilities the server may use. The only value the server consumes today is "local_tools_discover" — see Local tool discovery below. |
audioConfiguration shape
audioConfiguration has input and output blocks with the same fields:
| Field | Type | Allowed values | Required |
|---|---|---|---|
format | string | "audio/pcm" (default), "audio/opus" | optional |
sampleRate | number | 8000 (default), 16000 | optional |
frameDurationMs | number | 20, 40, 60 | required when that side's format is "audio/opus" — input and output are validated independently. |
Examples:
// Default voice config (PCM 8 kHz)
"audioConfiguration": {
"input": { "format": "audio/pcm", "sampleRate": 8000 },
"output": { "format": "audio/pcm", "sampleRate": 8000 }
}
// Opus, 16 kHz, 20 ms frames — common for low-bandwidth devices
"audioConfiguration": {
"input": { "format": "audio/opus", "sampleRate": 16000, "frameDurationMs": 20 },
"output": { "format": "audio/opus", "sampleRate": 16000, "frameDurationMs": 20 }
}
Server → client: handshake_ack
{
"type": "handshake_ack",
"content_type": "json",
"content": {
"conversationId": "cnv_abcdef...",
"status": "created" | "resumed"
},
"metadata": {
"model_id": "anthropic:claude-sonnet-4-6"
}
}
| Field | Type | Notes |
|---|---|---|
content.conversationId | string | The active conversation ID. Save this for resuming later. |
content.status | "created" | "resumed" | Whether you got a fresh conversation or picked up an existing one. |
metadata.model_id | string | Which LLM the agent is using for this turn. |
After the handshake_ack, you can start sending user messages (text mode) or audio_stream_start + binary audio (voice mode).
Bad handshakes
If the server rejects your handshake, you'll see an error envelope followed by a close. Handshake-time errors use the structured error shape — content_type: "json", with code / message / details inside content.
content.code | Cause |
|---|---|
| (no envelope code) | First message wasn't a handshake. The server emits a generic error and closes. |
INVALID_INPUT_FORMAT | audioConfiguration.input.format is not one of "audio/pcm", "audio/opus". |
INVALID_INPUT_SAMPLE_RATE | audioConfiguration.input.sampleRate is not 8000 or 16000. |
INVALID_OUTPUT_FORMAT | audioConfiguration.output.format is not one of "audio/pcm", "audio/opus". |
INVALID_OUTPUT_SAMPLE_RATE | audioConfiguration.output.sampleRate is not 8000 or 16000. |
INVALID_FRAME_DURATION_MS | Opus chosen on input or output, but the matching side's frameDurationMs is missing or not 20/40/60. |
QUOTA_EXCEEDED | The user has exceeded their daily quota. content also includes current_usage, limit, remaining, percentage. |
See Errors & Lifecycle for the full list and the two error envelope shapes.
Examples
Minimal text handshake
{
"type": "handshake",
"content_type": "json",
"content": { "conversationType": "text" }
}
Resume an existing conversation
{
"type": "handshake",
"content_type": "json",
"content": {
"conversationType": "text",
"conversationId": "cnv_abc123"
}
}
Voice with PCM at 8 kHz
{
"type": "handshake",
"content_type": "json",
"content": {
"conversationType": "audio",
"audioConfiguration": {
"input": { "format": "audio/pcm", "sampleRate": 8000 },
"output": { "format": "audio/pcm", "sampleRate": 8000 }
}
}
}
Voice with Opus at 16 kHz
{
"type": "handshake",
"content_type": "json",
"content": {
"conversationType": "audio",
"audioConfiguration": {
"input": { "format": "audio/opus", "sampleRate": 16000, "frameDurationMs": 20 },
"output": { "format": "audio/opus", "sampleRate": 16000, "frameDurationMs": 20 }
}
}
}
Local tool discovery
If your client supports running local tools (for example, a device with on-board MCP servers), include "local_tools_discover" in the handshake's capabilities array. When the agent has at least one local-fetch tool configured, the server runs an extra round-trip before the handshake_ack:
- Server → client:
local_tools_discoverenvelope listing the local MCP servers the agent expects to reach. - Client → server:
local_tools_supportedenvelope listing the tools your client actually has available, per server. - Server → client:
handshake_ack.
The server gives the client up to 10 seconds to reply with local_tools_supported. If the client doesn't respond in time, the server proceeds without local tools (no error).
Reference: see client_user.py for a working implementation of both messages.
Implementation notes
- The server defaults
conversationTypeto"text"if you omit it. - Voice mode without an
audioConfigurationis accepted — the server fills in PCM / 8000 Hz / 8000 Hz defaults for both input and output. conversationIdis matched per-agent and per-user. If the conversation ID belongs to a different agent (or to a different user), the server silently creates a new conversation rather than erroring.metadata.model_idreflects the agent's currently configured text model (or speech model for voice). It can change between sessions if the admin reconfigures the agent.- The handshake-ack envelope only carries
metadata.model_id— the standardsequence_number/timestampmetadata is added to subsequent messages, not tohandshake_ackitself.