WebSocket events
| {
"title": "WebSocket events",
"audience": "developer",
"summary": "Every event type ScaiWave streams over WebSocket, with payload schemas.",
"sort_order": 1
}
|
WebSocket events
The WebSocket stream at wss://<host>/v1/stream?token=<jwt> carries
every real-time event the caller can see. JSON-over-WS; each frame
is one event with a type discriminator.
See
Subscribe to WebSocket events
for connection lifecycle.
Common envelope
Every event has:
{
"type": "swp.room.message",
"id": "evt-…", // unique event id (for swp.* types)
"room_id": "room-…", // for room-scoped events
"tenant_id": "abc-…",
"ts": "2026-05-17T11:00:00Z"
}
Plus type-specific fields below.
Hello
Sent once by the server after a successful connection.
| { "type": "hello", "session_id": "uuid", "server_name": "...", "tenant_id": "..." }
|
If you don't see this within ~2 seconds of upgrade, the connection
failed (likely token).
Room message events
swp.room.message
| {
"type": "swp.room.message",
"id": "evt-…",
"room_id": "room-…",
"sender_id": "5e4d…",
"content": { "msgtype": "swp.text", "body": "Hello" },
"stream_position": 12345,
"origin_ts": 1778939467
}
|
swp.room.message.chunk
Streaming chunk during AI generation. Multiple chunks per response;
the final one has content.done=true.
| {
"type": "swp.room.message.chunk",
"room_id": "room-…",
"sender_id": "ai-…",
"content": { "chunk_text": "partial token…", "done": false }
}
|
swp.room.redaction
| {
"type": "swp.room.redaction",
"room_id": "room-…",
"event_id": "evt-redacted",
"redacted_by": "5e4d…",
"reason": "..."
}
|
swp.room.reaction
| {
"type": "swp.room.reaction",
"room_id": "room-…",
"event_id": "evt-target",
"sender_id": "5e4d…",
"emoji": "🚀",
"added": true
}
|
swp.room.member
{
"type": "swp.room.member",
"room_id": "room-…",
"participant_id": "5e4d…",
"membership": "join", // join | leave | ban | invite | knock
"changed_by": "5e4d…"
}
swp.room.name / swp.room.topic
| {
"type": "swp.room.name",
"room_id": "room-…",
"content": { "name": "New name" }
}
|
swp.room.read_marker
Fires when you update your read marker (cross-device sync).
Doesn't broadcast other people's read markers.
| {
"type": "swp.room.read_marker",
"room_id": "room-…",
"participant_id": "<your-id>",
"event_id": "evt-…"
}
|
swp.room.ai_engagement
| {
"type": "swp.room.ai_engagement",
"room_id": "room-…",
"mode": "default",
"ai_participant_id": "ai-…"
}
|
AI events
swp.ai.status
{
"type": "swp.ai.status",
"room_id": "room-…",
"participant_id": "ai-…",
"status": "calling web_search" // null when idle
}
swp.ai.context_usage
| {
"type": "swp.ai.context_usage",
"room_id": "room-…",
"participant_id": "ai-…",
"content": { "used": 4231, "budget": 8192, "pct": 51.6, "dropped": [] }
}
|
swp.plan.updated
Full plan snapshot whenever the plan transitions or a step
changes.
{
"type": "swp.plan.updated",
"room_id": "room-…",
"content": {
"plan": { /* full Plan dict — see Sidekicks and plans API */ }
}
}
Sidekicks
swp.sidekick.spawned
1
2
3
4
5
6
7
8
9
10
11
12
13 | {
"type": "swp.sidekick.spawned",
"room_id": "<parent-room>",
"content": {
"agent_task_id": "task-…",
"sidekick_room_id": "room-…",
"codename": "brave-penguin",
"task_description": "...",
"model_id": "...",
"context_mode": "task_only",
"timeout_seconds": 300
}
}
|
swp.sidekick.result
| {
"type": "swp.sidekick.result",
"room_id": "<parent-room>",
"content": {
"agent_task_id": "...",
"codename": "...",
"summary": "Found three candidate papers…"
}
}
|
swp.sidekick.cancel
| {
"type": "swp.sidekick.cancel",
"room_id": "<parent-room>",
"content": {
"agent_task_id": "...",
"sidekick_room_id": "...",
"task_description": "...",
"reason": "Cancelled by user"
}
}
|
Presence and typing
typing
| {
"type": "typing",
"room_id": "room-…",
"participant_id": "5e4d…",
"typing": true
}
|
Auto-clears after 4 seconds without a refresh.
presence
{
"type": "presence",
"participant_id": "5e4d…",
"status": "online" // online | idle | busy | appear_offline | offline
}
Not scoped to a room — fires across the whole tenant for whichever
participants you have visibility into.
Calls
swp.call.invite
| {
"type": "swp.call.invite",
"room_id": "room-…",
"call_id": "call-…",
"call_type": "video",
"initiator_id": "5e4d…",
"expires_at": "..."
}
|
swp.call.state
State transitions: someone joined, left, muted, started screen
share, recording started/stopped.
| {
"type": "swp.call.state",
"call_id": "call-…",
"state": "active",
"participants": [...]
}
|
Federation
swp.federation.event
Opaque wrapper for federation-state changes that don't fit other
categories (peer first-seen, key rotation observed, …).
Bridges
swp.bridge.message
Foreign message arrived from a bridge. Looks like a regular
swp.room.message but with content._imported_sender set.
swp.bridge.status
{
"type": "swp.bridge.status",
"bridge_id": "...",
"state": "active" // active | paused | error
}
Misc
swp.model.error
When AI generation fails (e.g. ScaiGrid error). Routed to the room
so it appears in the timeline.
| {
"type": "swp.model.error",
"room_id": "room-…",
"participant_id": "ai-…",
"content": { "code": "MODEL_NOT_FOUND", "message": "..." }
}
|
Reconnect
Server doesn't replay events on reconnect. Use /v1/sync?since=...
to bridge the gap.