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

# WebSocket Protocol

> Complete specification for the VIZOCHOK real-time chat protocol

## Overview

VIZOCHOK uses a WebSocket-based protocol for real-time streaming chat. The protocol is designed around three principles:

1. **Auth via first message** -- credentials are sent as the first WebSocket message, not as query parameters (which would appear in server logs).
2. **Structured messages** -- all messages are JSON objects with a `type` field for routing.
3. **Streaming text** -- LLM responses are delivered token-by-token via `text_delta` messages for real-time display.

## Connection Lifecycle

<Steps>
  <Step title="WebSocket Connect">
    Client establishes WebSocket connection. Server accepts.
  </Step>

  <Step title="Authentication">
    Client sends `{"type":"auth","token":"pk_..."}` (must be first message).
    Server responds with `{"type":"auth_ok"}`.
  </Step>

  <Step title="Session Initialization">
    **New conversation:** Server sends `{"type":"conversation_started", "conversation_id":"..."}`

    **Reconnection:** Server sends `{"type":"session_restored", "conversation_id":"...", "selected_items":[...]}` with cart state and pending tools.
  </Step>

  <Step title="Message Exchange">
    Client sends `{"type":"message","text":"..."}`.
    Server responds with `status` updates, `text_delta` chunks (repeated), `text_end`, and `response_complete`.
  </Step>

  <Step title="Disconnect">
    Client closes connection or disconnects. Session is saved to Redis for reconnection.
  </Step>
</Steps>

## Endpoint

```
wss://api.vizochok.com/api/v1/ws/chat
```

For self-hosted deployments, replace the host with your API base URL. The SDK automatically converts `https://` to `wss://`.

## Client to Server Messages

### auth

**Must be the first message sent after connection.** The server waits up to 10 seconds for this message before closing the connection with code `4001`.

| Field      | Type     | Required | Description                                                        |
| ---------- | -------- | -------- | ------------------------------------------------------------------ |
| `type`     | `string` | Yes      | Must be `"auth"`.                                                  |
| `token`    | `string` | Yes      | API key (public `pk_` or secret `sk_`).                            |
| `store_id` | `string` | No       | Store identifier. Used for webhook routing.                        |
| `user_id`  | `string` | No       | User identifier. Enables per-user rate limits and personalization. |

```json theme={null}
{
  "type": "auth",
  "token": "pk_live_abc123",
  "store_id": "my-store",
  "user_id": "user_42"
}
```

### message

Send a text message from the user.

| Field             | Type     | Required | Description                                                                                          |
| ----------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `type`            | `string` | Yes      | Must be `"message"`.                                                                                 |
| `text`            | `string` | Yes      | The user's message text. Max 64 KB.                                                                  |
| `store_id`        | `string` | No       | Store identifier.                                                                                    |
| `user_id`         | `string` | No       | User identifier.                                                                                     |
| `conversation_id` | `string` | No       | Conversation ID from `conversation_started` or `session_restored`. Omit to start a new conversation. |
| `language`        | `string` | No       | Language code: `"uk"`, `"en"`, or `"ru"`. Default: `"uk"`.                                           |

```json theme={null}
{
  "type": "message",
  "text": "Find me some milk",
  "store_id": "my-store",
  "user_id": "user_42",
  "conversation_id": "conv_abc123"
}
```

### action

Send a user action in response to an interactive tool (product selection, quick reply, checklist, meal plan).

| Field             | Type       | Required | Description                                                                                  |
| ----------------- | ---------- | -------- | -------------------------------------------------------------------------------------------- |
| `type`            | `string`   | Yes      | Must be `"action"`.                                                                          |
| `action`          | `string`   | Yes      | Action type: `"select"`, `"checklist_confirm"`, `"meal_plan_approve"`, `"meal_plan_modify"`. |
| `store_id`        | `string`   | No       | Store identifier.                                                                            |
| `user_id`         | `string`   | No       | User identifier.                                                                             |
| `conversation_id` | `string`   | No       | Current conversation ID.                                                                     |
| `sku`             | `string`   | No       | Product SKU (for `"select"` action).                                                         |
| `quantity`        | `number`   | No       | Product quantity (for `"select"` action).                                                    |
| `recipe_name`     | `string`   | No       | Recipe name (for `"checklist_confirm"` action).                                              |
| `selected`        | `string[]` | No       | Deselected ingredient names (for `"checklist_confirm"` action).                              |
| `text`            | `string`   | No       | Modification text (for `"meal_plan_modify"` action).                                         |

```json theme={null}
{
  "type": "action",
  "action": "select",
  "sku": "milk-001",
  "quantity": 2,
  "store_id": "my-store",
  "conversation_id": "conv_abc123"
}
```

### ping

Client-initiated keepalive message. The SDK sends this every 30 seconds.

```json theme={null}
{ "type": "ping" }
```

### pong

Response to a server-initiated `ping`. Must be sent within 60 seconds to avoid heartbeat timeout.

```json theme={null}
{ "type": "pong" }
```

## Server to Client Messages

### auth\_ok

Confirms successful authentication. Sent immediately after a valid `auth` message.

```json theme={null}
{ "type": "auth_ok" }
```

### conversation\_started

Sent when a new conversation is created (first message with no `conversation_id`).

| Field             | Type     | Description                       |
| ----------------- | -------- | --------------------------------- |
| `type`            | `string` | `"conversation_started"`          |
| `conversation_id` | `string` | The new conversation's unique ID. |

```json theme={null}
{
  "type": "conversation_started",
  "conversation_id": "conv_abc123"
}
```

### session\_restored

Sent on reconnect when the client provides an existing `conversation_id` that has an active Redis session. Contains the current cart state and any pending interactive tool.

| Field             | Type         | Description                                                  |
| ----------------- | ------------ | ------------------------------------------------------------ |
| `type`            | `string`     | `"session_restored"`                                         |
| `conversation_id` | `string`     | The restored conversation ID.                                |
| `selected_items`  | `CartItem[]` | Current cart items (optional, present if cart is non-empty). |
| `cart_total`      | `number`     | Total cart value (optional).                                 |
| `pending_tool`    | `object`     | Pending interactive tool awaiting user input (optional).     |

The `pending_tool` object varies by tool type:

| Tool Name               | Fields                                                       |
| ----------------------- | ------------------------------------------------------------ |
| `show_products_to_user` | `name`, `id`, `products: ProductItem[]`                      |
| `ask_user`              | `name`, `id`, `options: string[]`                            |
| `show_recipe_checklist` | `name`, `id`, `recipe_name: string`, `ingredients: string[]` |
| `show_meal_plan`        | `name`, `id`, `title`, `days`, `total_ingredients`           |

```json theme={null}
{
  "type": "session_restored",
  "conversation_id": "conv_abc123",
  "selected_items": [
    { "sku": "milk-001", "name": "Milk 2.5%", "price": 42.90, "quantity": 1 }
  ],
  "cart_total": 42.90
}
```

### text\_delta

A single token (or small chunk) of streaming text from the LLM. Multiple `text_delta` messages form a complete text block, terminated by `text_end`.

| Field   | Type     | Description              |
| ------- | -------- | ------------------------ |
| `type`  | `string` | `"text_delta"`           |
| `delta` | `string` | Text fragment to append. |

```json theme={null}
{ "type": "text_delta", "delta": "Here " }
{ "type": "text_delta", "delta": "are some " }
{ "type": "text_delta", "delta": "options:" }
```

### text\_end

Signals the end of a streaming text block. After this, the streaming cursor should stop.

```json theme={null}
{ "type": "text_end" }
```

### text

A complete text block (non-streaming). Used for short, pre-formed responses.

| Field  | Type     | Description            |
| ------ | -------- | ---------------------- |
| `type` | `string` | `"text"`               |
| `text` | `string` | Complete text content. |

```json theme={null}
{ "type": "text", "text": "Added to cart!" }
```

### status

Progress indicator during tool execution. Replaced in the UI when a new status arrives for the same message.

| Field     | Type     | Description                                           |
| --------- | -------- | ----------------------------------------------------- |
| `type`    | `string` | `"status"`                                            |
| `code`    | `string` | Status code (mapped to localized text by the SDK).    |
| `text`    | `string` | Fallback text if the SDK does not recognize the code. |
| `query`   | `string` | Search query (for `"searching"` status).              |
| `product` | `string` | Product name (for `"adding_to_cart"` status).         |

**Status codes:**

| Code                 | English                       | Ukrainian                     |
| -------------------- | ----------------------------- | ----------------------------- |
| `searching`          | Searching products {query}... | Шукаю {query}...              |
| `thinking`           | Thinking...                   | Думаю...                      |
| `adding_to_cart`     | Adding {product} to cart...   | Додаю {product}...            |
| `removing_from_cart` | Removing from cart...         | Видаляю з кошика...           |
| `updating_cart`      | Updating cart...              | Оновлюю кількість...          |
| `suggesting`         | Selecting the best options... | Підбираю найкращі варіанти... |
| `generating_plan`    | Creating a meal plan...       | Створюю план харчування...    |
| `checking_list`      | Checking the list...          | Перевіряю список...           |
| `updating_profile`   | Updating preferences...       | Оновлюю налаштування...       |
| `loading_cart`       | Loading cart...               | Завантажую кошик...           |
| `clearing_cart`      | Clearing cart...              | Очищую кошик...               |
| `loading_details`    | Loading details...            | Завантажую деталі...          |
| `preparing_results`  | Preparing results...          | Готую результати...           |
| `completing_session` | Completing session...         | Завершую сесію...             |
| `processing`         | Processing...                 | Обробляю...                   |

### product\_cards

Display a set of products for the user to select from.

| Field   | Type            | Description                                              |
| ------- | --------------- | -------------------------------------------------------- |
| `type`  | `string`        | `"product_cards"`                                        |
| `text`  | `string`        | Introductory text (e.g., "Here are some options:").      |
| `items` | `ProductItem[]` | Array of products to display.                            |
| `query` | `string`        | The search query that produced these results (optional). |

### quick\_replies

Display quick reply buttons for the user to choose from.

| Field     | Type       | Description                  |
| --------- | ---------- | ---------------------------- |
| `type`    | `string`   | `"quick_replies"`            |
| `options` | `string[]` | Array of reply option texts. |

```json theme={null}
{
  "type": "quick_replies",
  "options": ["Yes, add all", "Show cheaper options", "No thanks"]
}
```

### confirmation

Confirms a cart operation (add, remove, update).

| Field     | Type          | Description                                 |
| --------- | ------------- | ------------------------------------------- |
| `type`    | `string`      | `"confirmation"`                            |
| `text`    | `string`      | Confirmation message.                       |
| `product` | `ProductItem` | The product that was added/removed/updated. |

### ingredient\_checklist

Display a recipe ingredient checklist for user selection.

| Field         | Type               | Description                                |
| ------------- | ------------------ | ------------------------------------------ |
| `type`        | `string`           | `"ingredient_checklist"`                   |
| `recipe_name` | `string`           | Name of the recipe.                        |
| `text`        | `string`           | Description text.                          |
| `ingredients` | `IngredientItem[]` | Array of ingredients with selection state. |

Each `IngredientItem`:

| Field      | Type      | Description                             |
| ---------- | --------- | --------------------------------------- |
| `id`       | `string`  | Unique ingredient identifier.           |
| `name`     | `string`  | Ingredient display name.                |
| `selected` | `boolean` | Whether the ingredient is pre-selected. |

### meal\_plan

Display a generated meal plan for the user to approve or modify.

| Field               | Type            | Description                               |
| ------------------- | --------------- | ----------------------------------------- |
| `type`              | `string`        | `"meal_plan"`                             |
| `title`             | `string`        | Plan title.                               |
| `days`              | `MealPlanDay[]` | Array of days with meals.                 |
| `total_ingredients` | `string[]`      | Combined ingredient list across all days. |
| `message`           | `string`        | Summary message.                          |

### session\_summary

Display a summary of the current session's cart.

| Field   | Type         | Description                    |
| ------- | ------------ | ------------------------------ |
| `type`  | `string`     | `"session_summary"`            |
| `items` | `CartItem[]` | All items in the session cart. |
| `total` | `number`     | Total cart value.              |

### cart\_changed

Notification that the cart was modified. This is a UI-only event -- the actual cart state lives on the client's backend.

| Field      | Type         | Description                                                                     |
| ---------- | ------------ | ------------------------------------------------------------------------------- |
| `type`     | `string`     | `"cart_changed"`                                                                |
| `action`   | `string`     | `"item_selected"`, `"item_removed"`, `"quantity_updated"`, or `"cart_cleared"`. |
| `sku`      | `string`     | SKU of the affected product.                                                    |
| `name`     | `string`     | Name of the affected product.                                                   |
| `quantity` | `number`     | New quantity.                                                                   |
| `price`    | `number`     | Product price.                                                                  |
| `items`    | `CartItem[]` | Complete cart after the change.                                                 |
| `total`    | `number`     | New cart total.                                                                 |

### response\_complete

Signals the end of an agent response. The conversation is ready for the next user message.

| Field             | Type     | Description                                                 |
| ----------------- | -------- | ----------------------------------------------------------- |
| `type`            | `string` | `"response_complete"`                                       |
| `conversation_id` | `string` | Current conversation ID.                                    |
| `session_summary` | `object` | Optional. Contains `selected_items`, `total`, `item_count`. |

```json theme={null}
{
  "type": "response_complete",
  "conversation_id": "conv_abc123",
  "session_summary": {
    "selected_items": [
      { "sku": "milk-001", "name": "Milk 2.5%", "price": 42.90, "quantity": 1 }
    ],
    "total": 42.90,
    "item_count": 1
  }
}
```

### error

An error occurred while processing the message.

| Field         | Type     | Description                                                            |
| ------------- | -------- | ---------------------------------------------------------------------- |
| `type`        | `string` | `"error"`                                                              |
| `code`        | `string` | Machine-readable error code. See [Error Codes](/concepts/error-codes). |
| `text`        | `string` | Fallback error text (optional).                                        |
| `retry_after` | `number` | Seconds to wait before retrying (for `rate_limit_exceeded`).           |
| `max_size`    | `number` | Maximum allowed message size in bytes (for `message_too_large`).       |
| `limit`       | `number` | The limit that was exceeded (for user limit errors).                   |

### event

Internal server events. Currently ignored by the SDK but can be used for analytics.

| Field   | Type     | Description          |
| ------- | -------- | -------------------- |
| `type`  | `string` | `"event"`            |
| `event` | `string` | Event name.          |
| `data`  | `object` | Event-specific data. |

### ping / pong

Server-initiated heartbeat. The client must respond with `{"type": "pong"}` within 60 seconds.

```json theme={null}
{ "type": "ping" }
```

Server response to a client-initiated ping:

```json theme={null}
{ "type": "pong" }
```

## Close Codes

| Code   | Reason                           | Description                                                                                |
| ------ | -------------------------------- | ------------------------------------------------------------------------------------------ |
| `4001` | Auth timeout / Invalid auth      | First message was not `auth`, timed out (10s), invalid format, or invalid/expired API key. |
| `4002` | Missing chat scope               | API key does not have the `chat` scope.                                                    |
| `4003` | Origin not allowed / Deactivated | WebSocket origin not in allowed list, or tenant is deactivated.                            |
| `4004` | LLM not configured               | Tenant has no LLM provider configured.                                                     |
| `4008` | Heartbeat timeout                | No pong received within 60 seconds of a ping.                                              |
| `4029` | Too many connections             | API key has more than 3 concurrent WebSocket connections.                                  |

## Reconnection Strategy

The SDK implements automatic reconnection with exponential backoff and jitter:

| Parameter        | Value                          |
| ---------------- | ------------------------------ |
| Initial delay    | 1,000 ms                       |
| Maximum delay    | 10,000 ms                      |
| Backoff formula  | `min(1000 * 2^attempt, 10000)` |
| Jitter           | +/- 25% of the computed delay  |
| Maximum attempts | 30                             |

### Reconnection Rules

* **Reconnects on**: Network errors, unexpected disconnects, heartbeat timeouts
* **Does not reconnect on**: Auth errors (4001, 4002, 4003, 4004), clean close, explicit `disconnect()` call
* **Attempt counter resets**: On successful connection (before auth)
* **Network awareness**: Listens for browser `online`/`offline` events; reconnects when the network comes back

### Session Restore on Reconnect

When the client reconnects with a `conversation_id`:

1. Server loads the session from Redis (if still within the 1-hour TTL)
2. Server sends a `session_restored` message with cart state and any pending tool
3. Client can continue the conversation seamlessly

If the Redis session has expired, the server creates a new conversation.

## Heartbeat

Both client and server send periodic heartbeat messages to detect dead connections:

| Side   | Interval | Timeout | Message            |
| ------ | -------- | ------- | ------------------ |
| Client | 30s      | --      | `{"type": "ping"}` |
| Server | 30s      | 60s     | `{"type": "ping"}` |

The server closes the connection with code `4008` if no pong is received within 60 seconds.

## Message Size Limits

| Limit          | Value       | Enforced By                               |
| -------------- | ----------- | ----------------------------------------- |
| Client message | 64 KB       | SDK (pre-send check) and server           |
| Message queue  | 50 messages | SDK (client-side queue during disconnect) |

The SDK checks message size before sending and returns a `message_too_large` error via `onError` without transmitting the message. The server also enforces the 64 KB limit and responds with an error if exceeded.

## Origin Validation

The server checks the `Origin` header of the WebSocket connection against the configured allowed origins list:

* If the origin is not in the allowed list, the connection is closed with code `4003`
* This prevents unauthorized websites from connecting to the WebSocket API
* The origin list is configured via the `CORS_ORIGINS` environment variable

<Note>
  Due to a Starlette/FastAPI limitation, the WebSocket must be accepted before the origin can be checked. The connection is accepted first, then immediately closed with a custom code if the origin is not allowed.
</Note>
