Claude Desktop Voice Protocol — WebSocket STT/TTS
Voice protocol extracted via runtime introspection (debugger spy + harness)
Claude Desktop Voice Protocol — WebSocket STT/TTS
Voice protocol of Claude Desktop 1.14271.0, extracted via runtime introspection (debugger spy + harness) on 2026-06-23/24.
Actual URL
wss://claude.ai/api/ws/voice/organizations/{org_uuid}/chat_conversations/{chat_uuid}
Query Parameters
| Parameter | Value | Description |
|---|---|---|
input_encoding | opus | Input codec (OPUS, not PCM!) |
input_sample_rate | 16000 | Microphone sample rate |
input_channels | 1 | Mono |
output_format | pcm_16000 | PCM 16kHz for TTS |
language | en-US | Language |
timezone | America/Sao_Paulo | Client timezone |
voice | buttery | TTS voice profile |
tts_speed | 1.00 | TTS speed |
server_interrupt_enabled | true | User can interrupt |
client_aec | true | Acoustic Echo Cancellation |
client_platform | desktop_app | Identification |
Headers
Connection: Upgrade
Sec-WebSocket-Key: ...
anthropic-client-app: com.anthropic.claudefordesktop
anthropic-client-os-platform: macOS
Response: 101 Switching Protocols, Cloudflare (CF-RAY, Server: cloudflare).
Audio
| Direction | Codec | Frame Size |
|---|---|---|
| Client → Server (input) | OPUS | ~428 bytes |
| Server → Client (output) | PCM 16kHz mono 16-bit | ~428 bytes (~22ms) |
Voice Mode Flow (complete)
Captured on 2026-06-24, 3656 frames. Session: chat_conversations/c84a1375-9ca6-4045-b2dd-ecf97f0274dc.
═══ 1. Connection ═══
WebSocket created → handshake → 101 Switching Protocols
═══ 2. Initialization ═══
S → session_server_initialized
S → message_sse: conversation_ready
═══ 3. STT (Speech-to-Text) ═══
S → transcription_start
C → OPUS frames (continuous)
S → transcript_interim: "However,"
S → transcript_interim: "However, there"
S → transcript_interim: "However, darkened his smile."
C → user_input_end
S → transcription_start ← may repeat if user keeps speaking
S → transcript_interim: "..."
C → user_input_end
S → transcript_interim: "However, darkened the smile of ranger."
C → user_input_end
═══ 4. Message commit ═══
S → message_complete: texts=['However, darkened the smile of ranger.']
═══ 5. Claude response (streaming) ═══
S → message_sse: message_start
S → message_sse: content_block_start
S → message_sse: content_block_delta ×4
S → message_sse: content_block_stop
S → message_sse: message_delta
S → message_sse: message_stop
═══ 6. TTS (Text-to-Speech) ═══
S → playback_start (server_vad_to_tts_start_ms: 3136)
S → tts_word: "I'm" pts=6465
S → [30× PCM frames 428B]
S → tts_word: " not" pts=6697
S → tts_word: " quite" pts=6860
S → tts_word: " sure" pts=7080
S → [30× PCM frames 428B]
S → tts_word: " what" pts=7266
... (interleaved: tts_word + PCM frames)
S → tts_word: " Aleffita." pts=8183
... (continues until sentence completes)
S → SpeechComplete
═══ 7. Disconnection ═══
WebSocket closed
All Events
Client → Server
| Type | Case | Description |
|---|---|---|
keep_alive | snake_case | Ping every 4s |
CloseStream | CamelCase | Close stream (Press+Hold) |
transcription_start | snake_case | Started speaking |
user_input_end | snake_case | Stopped speaking |
tools_register | snake_case | Register tools {"tools":[]} |
| (binary OPUS) | — | Compressed audio ~428B |
Server → Client
| Type | Case | Description | Payload |
|---|---|---|---|
session_server_initialized | snake_case | Session OK | {} |
message_sse | snake_case | Conversation SSE | {"event":{"type":"message_start",...}} |
transcription_start | snake_case | Transcription start | {} |
transcript_interim | snake_case | Partial transcription | {"text":"...","utterance_seq":0} |
TranscriptText | CamelCase | Final transcription (Press+Hold) | {"data":"..."} |
TranscriptEndpoint | CamelCase | Transcription end | {} |
user_input_end | snake_case | Echo | {} |
message_start | snake_case | Msg streaming start | {} |
message_delta | snake_case | Content chunk | {"data":"..."} |
message_stop | snake_case | Msg streaming end | {} |
message_complete | snake_case | Complete msg | {"data":{"content":[...]}} |
playback_start | snake_case | TTS start | {"server_vad_to_tts_start_ms":N,"type":"playback_start"} |
tts_word | snake_case | TTS word | {"text":"word","pts_ms":N} |
tts_segment_end | snake_case | TTS segment end | {} |
playback_end | snake_case | Playback end | {} |
SpeechComplete | CamelCase | All speech completed | {} |
error | snake_case | Error | {"data":{"error_code":"overloaded",...}} |
| (binary PCM) | — | TTS audio ~428B | — |
TTS Structure
TTS is interleaved: each tts_word is followed by ~30 PCM frames of ~428 bytes. The pts_ms (presentation timestamp) indicates the exact moment of each word.
tts_word: "I'm" pts=6465 → 30× PCM 428B
tts_word: "not" pts=6697 → more PCM frames
tts_word: "quite" pts=6860 → ...
Implementation Notes
- Input is OPUS: Requires OPUS codec (libopus). MiMo ASR accepts WAV/MP3/FLAC — gateway needs to decode OPUS → WAV
- TTS is PCM: MiMo TTS returns MP3 — gateway needs to convert MP3 → PCM 16kHz
- Per-conversation: URL contains
organizations/{org}andchat_conversations/{chat}— state is per conversation - Streaming required:
transcript_interimduring speech, not just at the end - Cloudflare: Traffic passes through CF — may require specific headers
- keep_alive: snake_case, every 4s
- CloseStream: CamelCase (Press+Hold)
- Mixed casing: Legacy events in CamelCase, new ones in snake_case
- server_interrupt_enabled: Client can interrupt TTS with new speech
- client_aec: Echo cancellation active — audio quality matters
Captures
| File | Frames | Mode | Transcription |
|---|---|---|---|
voice-session-2026-06-24.json | 3656 | Voice Mode + TTS | "However, darkened the smile of ranger." |
voice-captures.json (23/06 15:33) | 95 | STT Press+Hold | "Melifon Cinco Mata Muntaja in Chile" |
voice-captures.json (23/06 13:56) | 554 | Voice Mode | error: overloaded |
References
~/.bezetacil/scripts/001-debugger-spy.js— Spy v4 (captures WS metadata + frames)~/.claude/wikifita/bezetacil-toolset.md— ASAR pipeline + harness~/.claude/infra/bezetacil/— Toolset scripts