Skip to main content

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"]
}
}
FieldRequiredTypeNotes
conversationIdnostringIf you pass one and it exists for this agent, the conversation is resumed. Otherwise a new one is created. See Reconnect.
conversationTypeno"text" | "audio"Defaults to "text".
audioConfigurationnoobjectOptional for voice (defaults to PCM/8000/8000 if omitted). Ignored for text.
capabilitiesnostring arrayOptional 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:

FieldTypeAllowed valuesRequired
formatstring"audio/pcm" (default), "audio/opus"optional
sampleRatenumber8000 (default), 16000optional
frameDurationMsnumber20, 40, 60required 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"
}
}
FieldTypeNotes
content.conversationIdstringThe 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_idstringWhich 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.codeCause
(no envelope code)First message wasn't a handshake. The server emits a generic error and closes.
INVALID_INPUT_FORMATaudioConfiguration.input.format is not one of "audio/pcm", "audio/opus".
INVALID_INPUT_SAMPLE_RATEaudioConfiguration.input.sampleRate is not 8000 or 16000.
INVALID_OUTPUT_FORMATaudioConfiguration.output.format is not one of "audio/pcm", "audio/opus".
INVALID_OUTPUT_SAMPLE_RATEaudioConfiguration.output.sampleRate is not 8000 or 16000.
INVALID_FRAME_DURATION_MSOpus chosen on input or output, but the matching side's frameDurationMs is missing or not 20/40/60.
QUOTA_EXCEEDEDThe 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:

  1. Server → client: local_tools_discover envelope listing the local MCP servers the agent expects to reach.
  2. Client → server: local_tools_supported envelope listing the tools your client actually has available, per server.
  3. 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 conversationType to "text" if you omit it.
  • Voice mode without an audioConfiguration is accepted — the server fills in PCM / 8000 Hz / 8000 Hz defaults for both input and output.
  • conversationId is 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_id reflects 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 standard sequence_number / timestamp metadata is added to subsequent messages, not to handshake_ack itself.

On this page