> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kupe.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Realtime WebSocket

> Mint a session, connect to wss://x.kupe.in/v1/realtime, and handle OpenAI-compatible events.

Kupe Realtime is an OpenAI-shaped voice WebSocket. You mint an ephemeral session over HTTP, then attach to `wss://x.kupe.in/v1/realtime`.

The session always runs Kupe STT, TTS, and LLM. Voices are addressed by sanitized name (`priya`, not a vendor id). Tools attached to the agent run server-side. This path is **web-only** — it does not write telephony minutes.

## Mint

`POST /v1/realtime/sessions`

<ParamField path="agent_id" type="string" required>
  Agent to hydrate (prompt, greeting, tools, default voice).
</ParamField>

<ParamField path="voice" type="string">
  Catalog voice name. Overrides the agent's voice for this session.
</ParamField>

<ParamField path="variables" type="object">
  Values for `{{placeholders}}` in the prompt and greeting.
</ParamField>

<ParamField path="org_id" type="string">
  Optional when using an API key (`GET /v1/me` already has it).
</ParamField>

<ParamField path="project_id" type="string">
  Optional when using an API key.
</ParamField>

The response includes `client_secret.value` (single-use ticket) and `websocket_url`.

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  from kupe import Kupe

  client = Kupe()
  session = client.realtime.sessions.create(agent_id="agt_...", voice="priya")
  # session.client_secret.value, session.websocket_url
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import { Kupe } from "kupe-sdk";

  const kupe = new Kupe();
  const session = await kupe.realtime.sessions.create({
    agent_id: "agt_...",
    voice: "priya",
  });
  ```

  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST https://x.kupe.in/v1/realtime/sessions \
    -H "Authorization: Bearer sk-kupe-YOUR_KEY" \
    -H "Content-Type: application/json" \
    -d '{"agent_id":"agt_...","voice":"priya"}'
  ```
</CodeGroup>

## Connect

```
wss://x.kupe.in/v1/realtime?model=kupe-realtime&client_secret={secret}
```

You can also send `Authorization: Bearer {secret}` on the WebSocket handshake instead of the query string. `ticket` is an alias for `client_secret`.

The SDK helper does this for you:

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  with client.realtime.connect(session) as rt:
      rt.send_text("Remind them EMI is due tomorrow.")
      for event in rt:
          print(event.type)
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const rt = await kupe.realtime.connect(session);
  rt.sendText("Remind them EMI is due tomorrow.");
  for await (const event of rt) {
    console.log(event.type);
  }
  ```

  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  # Use a WebSocket client. After minting:
  # wss://x.kupe.in/v1/realtime?model=kupe-realtime&client_secret=...
  ```
</CodeGroup>

On accept, the server sends `session.created` with instructions, voice, tools, and `pcm16` in/out formats.

## Client → server

| `type`                                                 | Purpose                                                                   |
| ------------------------------------------------------ | ------------------------------------------------------------------------- |
| `conversation.item.create`                             | Push a user message. Use `content[].type = "input_text"` for a text turn. |
| `response.create`                                      | Ask the agent to produce a reply (audio + transcript).                    |
| `input_audio_buffer.append`                            | Base64 PCM16 mono **24 kHz** chunk. Same clock as the LiveKit web path.   |
| `response.cancel`                                      | Interrupt the current agent turn (barge-in).                              |
| `conversation.item.create` with `function_call_output` | Return a client-side tool result (`call_id` + `output`).                  |

Text-turn example (what `send_text` encodes):

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "conversation.item.create",
  "item": {
    "type": "message",
    "role": "user",
    "content": [{ "type": "input_text", "text": "Hello" }]
  }
}
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "type": "response.create" }
```

Mic path: send `input_audio_buffer.append` frames; server VAD emits speech start/stop and runs the turn.

## Server → client

Every event has `event_id` and `type`. Audio deltas are base64 PCM16.

| `type`                                                  | When                                           |
| ------------------------------------------------------- | ---------------------------------------------- |
| `session.created`                                       | Handshake complete. Hydrated agent + tools.    |
| `input_audio_buffer.speech_started`                     | User started speaking (VAD).                   |
| `input_audio_buffer.speech_stopped`                     | User stopped speaking.                         |
| `conversation.item.input_audio_transcription.delta`     | Partial user transcript.                       |
| `conversation.item.input_audio_transcription.completed` | Final user transcript.                         |
| `response.output_audio.delta`                           | Agent audio chunk (`delta` = base64 PCM16).    |
| `response.output_audio_transcript.delta`                | Partial agent transcript.                      |
| `response.output_audio_transcript.done`                 | Final agent transcript (`transcript`).         |
| `response.function_call_arguments.delta` / `.done`      | Tool call the client (or server) should run.   |
| `response.done`                                         | Turn finished (`response.usage` token totals). |
| `error`                                                 | Protocol or runtime error.                     |

The socket may also send dashboard-shaped `{ "kind": "transcript" | "latency" | "tool_call", ... }` frames on the same connection.

## Close codes

| Code   | Meaning                                            |
| ------ | -------------------------------------------------- |
| `4401` | Missing or invalid `client_secret`.                |
| `4400` | Ticket is not a realtime session.                  |
| `4429` | Too many concurrent realtime sockets on this node. |

## LiveKit web sessions

For the in-app web tester (room + JWT, not this WS), use `POST /v1/sessions` with `channel: "web"`. That returns a LiveKit `ws_url` and participant `token`. Realtime mint + `/v1/realtime` is the API you want from Python, TypeScript, or cURL.
