Skip to Content
API ReferenceWebSocket

WebSocket

Nexora streams agent responses over WebSocket. Connect once per chat session.

Connect

wss://your-nexora-instance.com/ws/chat/{chat_id}?token=<access_token>

The WebSocket route is mounted at the server root (/ws/chat/{chat_id}), not under the /api prefix used by the REST endpoints.

Obtain an access_token via the login endpoint:

POST /api/auth/login Content-Type: application/json { "email": "user@example.com", "password": "password" }

The token query param accepts either a JWT access token (from the login endpoint) or an API key (nxr_...). Pass whichever you have — both authenticate the WebSocket connection.

Client → Server

Send message

{ "type": "message", "content": "What is the status of the deployment?", "client_message_id": "unique-client-id" }

client_message_id is used for deduplication — safe to retry on disconnect. If the same client_message_id is received again, the server silently reuses the existing message row (it does not emit a distinct acknowledgement frame).

Ping

{ "type": "ping" }

Server replies with { "type": "pong" }.

Server → Client

Chunk (streaming text)

{ "type": "chunk", "content": "Partial response text..." }

Chunk frames carry only type and content — the assistant message_id is not known until the message is saved, so it is delivered in the terminal stream_end frame.

Tool / sub-agent step events

The server does not emit dedicated tool_call / tool_result frames. When an agent runs a tool (and when sub-agents are involved), activity is surfaced through sub-agent step events:

{ "type": "sub_agent_step_start", "task_id": "uuid", "step_id": "uuid", "step_name": "github_read", "step_label": "Reading issues…" } { "type": "sub_agent_step_done", "task_id": "uuid", "step_id": "uuid", "status": "success" }

Sub-agent events

{ "type": "sub_agent_start", "task_id": "uuid", "agent_name": "Research Assistant", "task_title": "...", "sub_chat_id": "uuid" } { "type": "sub_agent_done", "task_id": "uuid", "agent_name": "Research Assistant", "output": "...", "usage": { "input_tokens": 0, "output_tokens": 0 } }

Stream end

{ "type": "stream_end", "message_id": "uuid", "content": "...", "metadata": {}, "created_at": "2026-01-01T00:00:00Z" }

Response complete. The assistant message has been saved to the database. (There is no done frame — stream_end is the terminal event.)

Error

{ "type": "error", "message": "All LLM providers failed" }

The error frame carries a human-readable message only (no machine-readable code field).

Reconnection

Implement exponential backoff with jitter:

let attempt = 0 function connect(chatId, token) { const ws = new WebSocket( `wss://nexora.example.com/ws/chat/${chatId}?token=${token}` ) ws.onopen = () => { attempt = 0 } ws.onclose = () => { const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000) attempt++ setTimeout(() => connect(chatId, token), delay) } ws.onmessage = (event) => { const msg = JSON.parse(event.data) handleMessage(msg) } return ws }

Full example

Get a token and create a chat

const { access_token } = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'user@example.com', password: 'password' }), }).then(r => r.json()) const { id: chatId } = await fetch('/api/chats', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${access_token}`, }, body: JSON.stringify({ agent_id: 'your-agent-uuid' }), }).then(r => r.json())

Connect and listen

const ws = new WebSocket( `wss://nexora.example.com/ws/chat/${chatId}?token=${access_token}` ) ws.onmessage = (event) => { const msg = JSON.parse(event.data) if (msg.type === 'chunk') process.stdout.write(msg.content) if (msg.type === 'stream_end') console.log('\n[done]') if (msg.type === 'error') console.error(msg.message) }

Send a message

ws.send(JSON.stringify({ type: 'message', content: 'Review the latest deployment logs', client_message_id: crypto.randomUUID(), }))