Skip to content

WebSocket API

Usejarvis uses a WebSocket connection for the live dashboard experience.

Default endpoint:

ws://localhost:3142/ws

If you serve the dashboard over HTTPS behind a proxy, the browser will use wss://.../ws.

Access is JWT-only. There is no shared dashboard token, and by default nothing is open: unauthenticated requests to /api/* and /ws get a 401 (JSON {"error":"Unauthorized"} for API and WebSocket routes, an HTML error page for page loads). The only bypass is auth.insecure_open_access: true, intended for pre-enrollment setup.

Two tokens exist:

  1. Enrollment token (long-lived, ES256 JWT): minted by jarvis enroll "<device-name>". It is accepted on exactly two endpoints: GET /sidecar/connect (the sidecar WebSocket upgrade) and POST /sidecar/token, both via an Authorization: Bearer header. It is deliberately rejected everywhere else, so a leaked data-plane credential is bounded by the access-token TTL.
  2. Access token (short-lived): minted by POST /sidecar/token, which returns { "access_token": "...", "expires_in": 600 }. It lives 10 minutes. This is the credential for the dashboard, /api/*, and /ws.

The access token is carried as a token cookie. A valid ?token=<access token> on any page request triggers a 302 redirect to the same path with the parameter stripped and sets token=...; Path=/; SameSite=Lax; HttpOnly (plus Secure when the request is HTTPS, including via x-forwarded-proto).

Custom client flow:

  1. jarvis enroll "my-client" on the brain machine to get an enrollment JWT
  2. POST /sidecar/token with Authorization: Bearer <enrollment JWT> to mint an access token
  3. Call the API or open /ws with the token cookie set; re-mint before the 10 minute expiry

Note for /ws: the upgrade checks the token cookie, not an Authorization header, and also enforces an Origin check (the origin must match the daemon’s origin or host), returning 403 on mismatch.

GET /health is public and returns { status, version, uptime, clients, timestamp }.

The daemon’s WebSocket messages use this envelope:

{
"type": "chat",
"payload": {},
"id": "optional-id",
"priority": "normal",
"timestamp": 1710000000000
}

Important note:

  • The field is payload, not data

The shipped daemon defines these top-level message types:

chat, command, status, stream, error, notification, tts_start, tts_text, tts_end, voice_start, voice_end, voice_text, voice_confirmation_expired, interview_start, interview_user_message, interview_assistant, interview_done, interview_error, thinking_start, thinking_end, realtime_status, realtime_transcript, task_event, workflow_event, goal_event, site_event, settings_applied

The server accepts exactly these types; anything else returns { "type": "error", "payload": { "message": "Unknown message type: <t>" } }:

  • chat: send a user message
  • command: system-style requests
  • status: request daemon status
  • voice_start: begin a voice turn ({ requestId, currentRoom? }); binary WAV audio frames follow
  • voice_end: end the audio stream and trigger transcription
  • voice_text: browser-side STT result ({ requestId?, text, currentRoom? }); skips daemon transcription and cancels any in-flight audio session
  • interview_start, interview_user_message: the profile interview flow
  • stream: token streaming during a response (sub-agent output uses payload.source: "sub-agent")
  • notification: typed events discriminated by payload.source:
payload.sourceMeaning
task_updateTask state changed
assistant_messageProactive assistant message
content_updateContent pipeline update
approval_requestGoverned action awaiting approval ({ request, shortId, impact, intent }, priority: "urgent" when urgent)
emergency_stateEmergency stop engaged or cleared
sidecar_eventSidecar connected/disconnected or status change
awareness_eventAwareness observation or suggestion
  • settings_applied: emitted after DB-backed settings hot-apply, payload { sections, ok, errors? } (section names only, never values)
  • tts_start / tts_text / tts_end: sentence-level TTS lifecycle; binary MP3 frames are interleaved. tts_start.payload.containsWake tells the client to suppress the wake listener during playback
  • task_event: task lifecycle events for consumers tracking long-running work

Voice audio (client → server, WAV) and TTS audio (server → client, MP3 per sentence) travel as binary WebSocket frames alongside the JSON stream.

The sidecar WebSocket (/sidecar/connect) speaks a separate message family (RPC request, rpc_result, rpc_progress, sidecar_event) with binary-reference semantics for payloads over 256 KB and a two-timeout RPC model. It also supports ?channel=audio for a dedicated realtime-audio pipe. If you are integrating a device rather than a dashboard client, use that protocol, documented in the product repo under docs/sidecar/SIDECAR_PROTOCOL.md, not this one.

  • Forward WebSocket upgrades for /ws and /sidecar/connect
  • Do not gate the public routes: /health, /sidecar/connect, /api/sidecars/.well-known/jwks.json, /api/webhooks/*, and OPTIONS requests
  • Preserve cookies and x-forwarded-proto
  • Set daemon.brain_domain to the external origin so minted tokens point sidecars at the right endpoints
  • The server closes idle connections after 30 seconds; keep WebSocket ping/pong flowing