# The talk WebSocket

`WS /calls/{call_id}/talk` is the streaming protocol for a live call: you send mic audio, you get transcript text, reply text, and reply audio back, one turn at a time. `call_id` comes from [`POST /agents/{agent_id}/calls`](/docs/calls-api#start-a-call) and is single-use.

## Connecting

```js
const socket = new WebSocket(wsUrl); // wss://api.wakili.dev/calls/{call_id}/talk
socket.binaryType = "arraybuffer";
```

No auth header, no query-string token — see [Authentication](/docs/authentication#the-talk-websocket-doesnt-take-a-token) for why. The server closes with code `4404` if `call_id` is unknown, already used, or expired.

## call_config frame

Immediately after the connection opens, the server sends one JSON text frame carrying the agent's realtime knobs — barge-in, endpointing, language lock, and tier — so the client can configure its own capture/playback behaviour:

```json
{
  "type": "call_config",
  "interruption": { "enabled": true, "sensitivity": 0.5 },
  "endpointing": { "sensitivity": 0.5 },
  "language_lock": null,
  "model_tier": "balanced",
  "latency_profile": "balanced"
}
```

These come from the [agent's configuration](/docs/agents-api#realtime-pipeline-knobs). The frame is additive — a client that doesn't recognise `type: "call_config"` can ignore it and behave exactly as before.

## Sending a turn

1. While the user is speaking, send their mic audio as **binary frames** (any chunk size — the server just concatenates everything it receives per turn). The reference client records `audio/webm` via `MediaRecorder` and sends a chunk every 250ms.
2. When they stop, send **one text frame**: `{"type":"end_turn"}`.

The server buffers all binary frames received since the last turn ended, and on `end_turn` runs them through STT → LLM → TTS for that turn.

Sending `end_turn` with no buffered audio is a no-op (ignored, no events emitted) — safe to call defensively.

## Receiving events

Every server→client message is either a **JSON text frame** (an event, shape below) or a **binary frame** (raw reply audio bytes — TTS output, not chunked/framed further).

```ts
type TalkEvent =
  | { type: "call_config"; interruption: object; endpointing: object; language_lock: string | null; model_tier: string; latency_profile: string } // sent once, right after connect
  | { type: "transcript"; text: string }        // STT result for the turn
  | { type: "reply_text"; text: string }         // full LLM reply text
  | { type: "reply_audio_end" }                  // all binary audio frames for this reply have been sent
  | { type: "error"; message: string }           // "stt_failed" | "llm_failed" | "tts_failed"
  | { type: "call_ended"; reason: string };       // "idle_timeout" | "provider_error" | normal disconnect
```

Sequence for one turn:

```
client → binary frame(s)      (mic audio)
client → {"type":"end_turn"}
server → {"type":"transcript","text":"..."}
server → {"type":"reply_text","text":"..."}
server → binary frame(s)      (reply audio)
server → {"type":"reply_audio_end"}
```

Concatenate the binary frames received between `reply_text` and `reply_audio_end` to reassemble the full reply audio (e.g. `AudioContext.decodeAudioData` on the merged buffer).

## Errors and call end

If STT, the LLM, or TTS fails mid-turn, the server sends `{"type":"error","message":"stt_failed"|"llm_failed"|"tts_failed"}` immediately followed by `{"type":"call_ended","reason":"provider_error"}`, then closes the socket (code `1011`). The call is billed for whatever duration elapsed before the failure.

If no message arrives from the client for the idle timeout window, the server sends `{"type":"call_ended","reason":"idle_timeout"}` and closes (code `1000`). A normal client-initiated close ends the call with no `call_ended` event (just a disconnect).

**The server always closes the socket right after `call_ended`** — don't wait on anything further.

## Billing

The whole call is billed as one debit when it ends: wall-clock duration × the agent's per-minute rate (sum of its LLM/STT/TTS component rates). Individual turns aren't billed separately. If your account balance can't cover the call, `POST /agents/{agent_id}/calls` returns `402` before a `call_id` is ever issued — the WebSocket step is never reached.

## Reference client

A minimal push-to-talk implementation (`TalkClient`, ~90 lines, no dependencies) is in the [Next.js integration guide](/docs/nextjs-integration) — send mic audio while a button is held, send `end_turn` on release, play back the reply.
