WebSocket API
The WebSocket API enables real-time, streaming chat with your Biel.ai project. Use it when you need token-by-token AI responses instead of waiting for the full response via the REST API.
Endpoint: wss://app.biel.ai/ws/messages/{projectId}/
To continue an existing chat: wss://app.biel.ai/ws/messages/chat/{chatId}/
Connect
Open a WebSocket connection to:
wss://app.biel.ai/ws/messages/{projectId}/
Replace {projectId} with your project slug (e.g., jfvzioa17q).
Send a message
Send a JSON message to the WebSocket:
{
"projectId": "your-project-slug",
"from": "user",
"message": "How do I authenticate?",
"url": "https://docs.example.com/auth",
"email": "[email protected]"
}
| Field | Type | Required | Description |
|---|---|---|---|
projectId | string | No | Project slug for client bookkeeping; the connection path selects the project. |
from | string | No | Client message label; use "user" when included. |
message | string | Yes | The user's message (Markdown supported). |
chatId | string | No | Chat session UUID. Omit for the first message — included in the AI response. |
url | string | No | Page URL where the chat is happening. Used for domain validation. |
email | string | No | Email or ID for conversation tracking. |
apiKey | string | No | API key with chat_create permission. Required for private projects. |
thinkMode | boolean | No | Enable reasoning mode for deeper analysis. |
retrievalMode | string | No | Regular-mode retrieval: auto or instant. Omit for the existing default. Unsupported values are ignored; thinkMode: true takes precedence. See Reasoning modes. |
capabilities | array of strings | No | Include "activity.v1" together with a valid requestId to receive operational progress updates. |
requestId | string | No | Unique caller-generated identifier for this turn: 1–80 characters from A-Z, a-z, 0-9, ., _, :, or -. Negotiating servers echo it on answer and status frames. |
restoreToken | string | No | Opaque conversation restore token returned by the server. Retain it with the chat ID and send it on subsequent turns. A chat ID alone is not authorization. |
metadata | string | No | Tag to identify the conversation source. |
Receive a response
The server sends JSON messages back. Partial messages stream token-by-token; the final message includes sources.
{
"projectId": "your-project-slug",
"chatId": "550e8400-e29b-41d4-a716-446655440000",
"from": "ai",
"messageId": "msg-uuid",
"message": "To authenticate, use the...",
"timestamp": "2026-01-15T10:30:00Z",
"isPartial": false,
"sources": [
{
"title": "Authentication guide",
"url": "https://docs.example.com/auth"
}
]
}
| Field | Type | Description |
|---|---|---|
projectId | string | Project slug. |
chatId | string | Chat session UUID. Use this in subsequent messages to continue the conversation. |
from | string | Always "ai". |
messageId | string | Message UUID. |
message | string | AI response content (Markdown). |
timestamp | string | ISO 8601 timestamp. |
isPartial | boolean | true for streaming tokens, false for the final message. |
sources | array | Source references (only in final message). Each has title and url. |
incomplete | boolean | true if generation could not complete, including output limits or interruption. Only in the final message. |
stopReason | string | Provider stop reason or a classified recovery reason on the final frame. |
continuationCount | integer | Number of automatic continuations for this generation request. |
continuationAvailable | boolean | Whether the latest capped response can be continued through the REST API. |
requestId | string | Turn identifier, when progress was negotiated. May be absent on older servers. |
restoreToken | string | Opaque conversation restore token. Store it with chatId; older servers may omit it. |
interrupted | boolean | Optional negotiated marker that generation was interrupted. Keep any useful draft and offer an explicit retry. |
isError | boolean | Optional negotiated error marker. Do not treat an error message as a replacement for an existing draft. |
retryable | boolean | Optional negotiated indication that retry is appropriate. Wait for the connection to recover before sending a retry. |
Receive progress updates
To request progress, include both the capability and a new turn identifier in your message:
{
"projectId": "your-project-slug",
"from": "user",
"message": "How do I authenticate?",
"capabilities": ["activity.v1"],
"requestId": "turn-1",
"retrievalMode": "auto"
}
A supporting server sends status frames alongside the answer stream:
{
"type": "status",
"version": 1,
"requestId": "turn-1",
"sequence": 2,
"stage": "searching_docs",
"state": "started",
"detail": { "searchRound": 1 }
}
| Field | Description |
|---|---|
type, version | "status" and 1 identify this progress extension. |
requestId | Identifies the active turn. |
sequence | Increases within each turn, starting at 1. |
stage | contextualizing, searching_docs, reviewing_results, preparing_docs, searching_web, writing_answer, checking_sources, completed, failed, or interrupted. |
state | started, completed, or failed. |
detail | Optional details within the object: searchRound, documentCount, reason, or retryable. reason can be generation_failed, stream_interrupted, or disconnected. |
Status frames describe actual operations, rather than private model reasoning. They have no from or message field: handle them separately from chat messages. Only accept supported frames for the active turn and ignore stale or duplicate sequence numbers. Not every operation appears in every answer, and a final completed status can arrive after the final answer.
Hide successful progress when the final answer arrives. Preserve a useful interrupted draft and let the user choose whether to retry; do not automatically replay requests. The widget follows this behavior and offers spinner or loading dots as a display option.
Compatibility
Clients that omit progress negotiation retain the existing answer stream, without status frames. New clients must also accept answers from older servers that omit requestId, restoreToken, and progress updates. Keep a generic loading indicator in that case. Updating the backend alone does not change an older installed widget's interface.
Authentication
The WebSocket API uses the same authentication model as the REST API:
- Public projects — No API key required. Domain restrictions still apply.
- Private projects — Include
apiKeyin the first message payload withchat_createpermission.
Domain validation is checked on every message using the url field. If your project has allowed domains configured, the url must match one of them.
Rate limiting
| Limit | Threshold | Penalty |
|---|---|---|
| Per chat session | 15 messages/minute | Rejected until window resets |
| Per IP address | 100 messages/minute | IP blocked for 1 hour |
| Burst detection | 10 messages within a 30-second window | Rejected while the burst threshold is met |
Continue a capped answer
Automatic continuation happens on the backend for both REST and WebSocket clients. For a manual continuation, use the REST message endpoint with chat_uuid and continue_message_id, omit message, and send the stored restore token in X-Chat-Restore-Token. The response contains the original assistant message UUID and its complete text including the appended suffix. Replace that message in your UI; do not append a new user or assistant turn.