---
type: protocol
title: Claude Desktop Voice Protocol — WebSocket STT/TTS
description: Voice protocol extracted via runtime introspection (debugger spy + harness)
tags: [claude-desktop, voice, websocket, stt, tts, protocol]
timestamp: 2026-06-24
---

# 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

1. **Input is OPUS**: Requires OPUS codec (libopus). MiMo ASR accepts WAV/MP3/FLAC — gateway needs to decode OPUS → WAV
2. **TTS is PCM**: MiMo TTS returns MP3 — gateway needs to convert MP3 → PCM 16kHz
3. **Per-conversation**: URL contains `organizations/{org}` and `chat_conversations/{chat}` — state is per conversation
4. **Streaming required**: `transcript_interim` during speech, not just at the end
5. **Cloudflare**: Traffic passes through CF — may require specific headers
6. **keep_alive**: snake_case, every 4s
7. **CloseStream**: CamelCase (Press+Hold)
8. **Mixed casing**: Legacy events in CamelCase, new ones in snake_case
9. **server_interrupt_enabled**: Client can interrupt TTS with new speech
10. **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
