---
audience: developer
summary: Every event type ScaiWave streams over WebSocket, with payload schemas.
title: WebSocket events
path: reference/websocket-events
status: published
---

# 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](/docs/scaiwave/tutorials/developer/subscribe-to-websocket-events)
for connection lifecycle.

## Common envelope

Every event has:

```jsonc
{
  "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.

```json
{ "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`

```json
{
  "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`.

```json
{
  "type": "swp.room.message.chunk",
  "room_id": "room-…",
  "sender_id": "ai-…",
  "content": { "chunk_text": "partial token…", "done": false }
}
```

### `swp.room.redaction`

```json
{
  "type": "swp.room.redaction",
  "room_id": "room-…",
  "event_id": "evt-redacted",
  "redacted_by": "5e4d…",
  "reason": "..."
}
```

### `swp.room.reaction`

```json
{
  "type": "swp.room.reaction",
  "room_id": "room-…",
  "event_id": "evt-target",
  "sender_id": "5e4d…",
  "emoji": "🚀",
  "added": true
}
```

### `swp.room.member`

```jsonc
{
  "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`

```json
{
  "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.

```json
{
  "type": "swp.room.read_marker",
  "room_id": "room-…",
  "participant_id": "<your-id>",
  "event_id": "evt-…"
}
```

### `swp.room.ai_engagement`

```json
{
  "type": "swp.room.ai_engagement",
  "room_id": "room-…",
  "mode": "default",
  "ai_participant_id": "ai-…"
}
```

## AI events

### `swp.ai.status`

```jsonc
{
  "type": "swp.ai.status",
  "room_id": "room-…",
  "participant_id": "ai-…",
  "status": "calling web_search"   // null when idle
}
```

### `swp.ai.context_usage`

```json
{
  "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.

```jsonc
{
  "type": "swp.plan.updated",
  "room_id": "room-…",
  "content": {
    "plan": { /* full Plan dict — see Sidekicks and plans API */ }
  }
}
```

## Sidekicks

### `swp.sidekick.spawned`

```json
{
  "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`

```json
{
  "type": "swp.sidekick.result",
  "room_id": "<parent-room>",
  "content": {
    "agent_task_id": "...",
    "codename": "...",
    "summary": "Found three candidate papers…"
  }
}
```

### `swp.sidekick.cancel`

```json
{
  "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`

```json
{
  "type": "typing",
  "room_id": "room-…",
  "participant_id": "5e4d…",
  "typing": true
}
```

Auto-clears after 4 seconds without a refresh.

### `presence`

```jsonc
{
  "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`

```json
{
  "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.

```json
{
  "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`

```jsonc
{
  "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.

```json
{
  "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.
