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

# Migrate to the canonical Omni WebSocket

> Move a client from the discontinued Omni v2 chat URL to /v1/omni, including auth, query parameters, configure frames, binary framing, and error handling.

The old Omni v2 chat URL was discontinued on August 13, 2026. Connections to
that URL now return `404`. Move every Omni client to:

```text theme={null}
wss://api.pyai.com/v1/omni?format=pcm16&rate=24000
```

This is a URL and handshake migration for clients that were working on the old
URL immediately before discontinuation. The audio and control protocol remains
the native Omni binary protocol described below. Clients that still send an old
query alias, auth parameter, bare audio, or text control frame need the
corresponding updates in this guide.

## Migration checklist

### 1. Replace the URL and query parameters

```diff theme={null}
- wss://api.pyai.com/v2/omni/chat?agent=call-123
+ wss://api.pyai.com/v1/omni?session_label=call-123&format=pcm16&rate=24000
```

| Old client value     | Canonical value                                              |
| -------------------- | ------------------------------------------------------------ |
| `agent=<value>`      | `session_label=<same value>`                                 |
| `agent_id=<value>`   | `session_label=<same value>`                                 |
| no agent value       | Omit `session_label`; it is optional                         |
| `access_token=<key>` | Use the `pyai-key.<key>` subprotocol or `api_key=<key>`      |
| `model=<value>`      | Remove it; the Omni endpoint does not accept model selectors |

`session_label` is an opaque, optional tag. It can also be a managed Agent
profile id. Values must be no more than 256 characters and safe to place in an
HTTP header. Do not send `agent`, `agent_id`, `access_token`, or `model` to
`/v1/omni`. The route rejects `agent` and `agent_id` with
`400 unsupported_parameter`; `access_token` is not an auth mechanism; `model`
is outside the canonical Omni contract.

Keep `format` and `rate` explicit. Use `pcm16` and `24000` for a browser or
24 kHz audio pipeline. Use the actual negotiated rate if your telephony
pipeline is 16 kHz or 8 kHz.

### 2. Use a supported auth mechanism

For browser clients, send the API key as a WebSocket subprotocol:

```text theme={null}
Sec-WebSocket-Protocol: pyai-key.pyai_live_...
```

For server-side clients, use one of:

```text theme={null}
Authorization: Bearer pyai_live_...
x-api-key: pyai_live_...
```

If your WebSocket library cannot set headers, append `api_key` to the URL. Do
not place the key in `access_token` or another query parameter. The key needs
the `omni:session` scope.

### 3. Send `configure` after the socket opens

Send one `0x03`-prefixed binary control frame. Its JSON body is keyed on
`type`, not `event`:

```js theme={null}
const body = Buffer.from(JSON.stringify({
  type: "configure",
  voice_id: "stock_dorit_en_us",
  persona: "You are a concise appointment scheduling assistant.",
}));
ws.send(Buffer.concat([Buffer.from([0x03]), body]));
```

If you use a managed Agent profile through `session_label`, you can omit fields
already stored on that profile. An inline `configure` value overrides the
stored value for the session.

### 4. Keep native Omni binary framing

Every WebSocket message uses a one-byte type prefix:

| Tag    | Direction        | Payload                                                        |
| ------ | ---------------- | -------------------------------------------------------------- |
| `0x01` | client to server | PCM16 little-endian caller audio at the requested input `rate` |
| `0x01` | server to client | PCM16 agent audio at `hello.audio_out`                         |
| `0x02` | server to client | Plain UTF-8 caller-transcript delta (not JSON)                 |
| `0x03` | both             | UTF-8 control JSON                                             |

Prefix every caller-audio chunk with `0x01`. Prefix client controls such as
`configure`, `dtmf`, and `tool_result` with `0x03`. Client controls use a
`type` key. Server control payloads use an `event` key; transcript payloads are
plain text.

```js theme={null}
const frame = new Uint8Array(pcm.byteLength + 1);
frame[0] = 0x01;
frame.set(new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength), 1);
ws.send(frame);
```

Do not send bare PCM or bare text JSON. Do not treat every binary message as
audio. Strip and inspect the first byte before decoding the payload.

## Error and close behavior

Handshake failures happen before a WebSocket session exists:

| Result                           | Meaning                                        | Action                            |
| -------------------------------- | ---------------------------------------------- | --------------------------------- |
| HTTP `404`                       | The client is still using the discontinued URL | Replace it with `/v1/omni`        |
| HTTP `400 invalid_session_label` | The label is malformed                         | Fix or omit `session_label`       |
| HTTP `400 unsupported_parameter` | `agent` or `agent_id` remains                  | Move its value to `session_label` |
| HTTP `401`                       | The key is missing or invalid                  | Fix auth; do not retry            |
| HTTP `403`                       | The key lacks `omni:session`                   | Add the scope; do not retry       |

After a successful upgrade, handle these WebSocket closes:

| Close code | Meaning                           | Action             |
| ---------- | --------------------------------- | ------------------ |
| `1000`     | Normal closure                    | No retry needed    |
| `4401`     | Bad or expired session credential | Fix credentials    |
| `4403`     | Missing or insufficient scope     | Add `omni:session` |
| `4429`     | Rate or concurrency cap           | Retry with backoff |
| `1011`     | Transient service error           | Retry with backoff |

Do not rely on every auth or parameter failure arriving as a WebSocket close.
The canonical edge can reject the HTTP upgrade first. A reconnect starts a new
session and requires a fresh `configure` frame.

## Troubleshooting

* **`404` during connect:** the URL still points at the discontinued route.
* **`400 unsupported_parameter`:** copy an `agent` or `agent_id` value into
  `session_label`, then remove the old parameter.
* **The socket opens but the agent hears nothing:** caller audio is missing the
  `0x01` prefix, is not PCM16 little-endian, or does not match the URL `rate`.
* **The socket opens but the persona is ignored:** the outbound configure body
  uses `event: "configure"` instead of `type: "configure"`, or it was sent as
  text instead of a `0x03` binary frame.
* **Control data sounds like an audio glitch:** demultiplex `0x02` transcript
  and `0x03` control frames instead of sending them to audio playback.
* **`401` or `4401`:** use a valid PyAI key through a supported auth mechanism.
* **`403` or `4403`:** add the `omni:session` scope to the key.

For the complete event catalog and current configure fields, see the
[Omni wire protocol](/realtime/omni-protocol). If you need help validating a
client, contact [support@pyai.com](mailto:support@pyai.com) with the request id
and close code. Never send an API key.
