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(),
}))