Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Subscribe to WebSocket events

ScaiWave streams real-time events over a single WebSocket per client. The protocol is JSON-over-WS; subscribe to a tenant scope and the server fans out everything you have permission to see.

The endpoint#

text
1
wss://<host>/v1/stream?token=<jwt>

The token is passed via query string because browser WebSockets don't support custom headers. The server validates it the same way HTTP middleware does and rejects bad tokens with a close frame.

Don't log the URL. It contains a Bearer token. Most server log configs scrub it, but be careful in browser devtools and proxy logs.

Hello#

After the upgrade succeeds, the server sends:

json
1
2
3
4
5
6
7
{
  "type": "hello",
  "session_id": "<uuid>",
  "server_name": "scaiwave.scailabs.com",
  "tenant_id": "<your-tenant-id>",
  "ts": "2026-05-17T11:00:00Z"
}

If you don't see this within a couple of seconds, the token wasn't accepted; check the close frame's code and reason.

What you receive#

The same event types whether you sent the action or someone else did. Each event carries a type discriminator.

Room events#

  • swp.room.message — a message was sent.
  • swp.room.reaction — a reaction was added.
  • swp.room.redaction — an event was redacted.
  • swp.room.message.chunk — streaming chunk (during AI generation). Includes content.chunk_text and content.done.
  • swp.room.member — membership changed (join/leave/ban).
  • swp.room.ai_engagement — AI engagement state changed.
  • swp.room.name — room renamed.
  • swp.room.topic — topic changed.
  • swp.room.read_marker — your own read marker advanced (cross-device sync).

AI events#

  • swp.ai.status — AI's status update ("thinking", "calling web_search", "generating", null when done).
  • swp.ai.context_usage — current token usage on the AI's prompt.
  • swp.plan.updated — full plan snapshot (state + steps).

Sidekick events#

  • swp.sidekick.spawned — sidekick created in this room.
  • swp.sidekick.result — sidekick completed; result attached.
  • swp.sidekick.cancel — sidekick cancelled.

Presence / typing#

  • typing{room_id, participant_id, typing: bool}.
  • presence{participant_id, status: "online"|"idle"|...}.

Calls#

  • swp.call.invite — incoming call (with timeout).
  • swp.call.state — call participant state changed.

Federation#

  • swp.federation.event — opaque to the client; surfaces newly- federated room state changes.

Bridges#

  • swp.bridge.message — message arrived from a bridge.
  • swp.bridge.status — bridge state change.

Filtering#

The server fans out everything you can see (every room you're a member of). To narrow what you process, filter client-side on data.room_id or data.type.

Reconnect#

The server idles connections after 60s without activity from either side (ping/pong is automatic). On a clean close (code 1000) or abrupt disconnect, reconnect with exponential backoff.

ScaiWave doesn't replay missed events on reconnect — use the sync endpoint:

bash
1
GET /v1/sync?since=<last-stream-position>&limit=100&timeout=30000

Long-polls; returns everything you missed since <last-stream-position>, then either resolves (data available) or after timeout (idle — re-poll). The web client uses this on reconnect, then resumes WS.

Minimal client (Python)#

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import asyncio
import json
import websockets

async def main():
    uri = "wss://localhost:8000/v1/stream?token=mock-dev-token"
    async with websockets.connect(uri) as ws:
        async for raw in ws:
            event = json.loads(raw)
            if event["type"] == "swp.room.message":
                print(f"new message in {event['room_id']}: "
                      f"{event['content'].get('body', '')[:80]}")
            elif event["type"] == "swp.ai.status":
                print(f"AI {event['participant_id']}: {event.get('status')}")

asyncio.run(main())

Minimal client (TypeScript)#

ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const ws = new WebSocket(`wss://${HOST}/v1/stream?token=${encodeURIComponent(token)}`);

ws.addEventListener("message", (msg) => {
  const event = JSON.parse(msg.data);
  switch (event.type) {
    case "swp.room.message":
      console.log("new msg:", event.content.body);
      break;
    case "swp.ai.status":
      console.log(`AI status: ${event.status}`);
      break;
  }
});

ws.addEventListener("close", (e) => {
  console.warn(`WS closed: ${e.code} ${e.reason}`);
  // Reconnect with backoff…
});

Where to go next#

Updated 2026-05-18 12:07:12 View source (.md) rev 2