Overview
VIZOCHOK uses a WebSocket-based protocol for real-time streaming chat. The protocol is designed around three principles:
- Auth via first message — credentials are sent as the first WebSocket message, not as query parameters (which would appear in server logs).
- Structured messages — all messages are JSON objects with a
type field for routing.
- Streaming text — LLM responses are delivered token-by-token via
text_delta messages for real-time display.
Connection Lifecycle
WebSocket Connect
Client establishes WebSocket connection. Server accepts.
Authentication
Client sends {"type":"auth","token":"pk_..."} (must be first message).
Server responds with {"type":"auth_ok"}.
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.
Message Exchange
Client sends {"type":"message","text":"..."}.
Server responds with status updates, text_delta chunks (repeated), text_end, and response_complete.
Disconnect
Client closes connection or disconnects. Session is saved to Redis for reconnection.
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. |
{
"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". |
{
"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). |
{
"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.
pong
Response to a server-initiated ping. Must be sent within 60 seconds to avoid heartbeat timeout.
Server to Client Messages
auth_ok
Confirms successful authentication. Sent immediately after a valid auth message.
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. |
{
"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 |
{
"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. |
{ "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.
text
A complete text block (non-streaming). Used for short, pre-formed responses.
| Field | Type | Description |
|---|
type | string | "text" |
text | string | Complete text content. |
{ "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 … | Шукаю … |
thinking | Thinking… | Думаю… |
adding_to_cart | Adding to cart… | Додаю … |
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. |
{
"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. |
{
"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. |
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.
Server response to a client-initiated ping:
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:
- Server loads the session from Redis (if still within the 1-hour TTL)
- Server sends a
session_restored message with cart state and any pending tool
- 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
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.