Skip to main content
The realtime endpoint delivers live hub events to connected members over Server-Sent Events (SSE). Use it to drive badges, live discussion feeds, DM notifications, and online-presence indicators without polling.

Connect

GET /realtime/events?hub_id={hub_id}
Authorization: Bearer <contact-access-token>
Accept: text/event-stream
The connection stays open indefinitely. The server sends events as they happen and a :heartbeat comment every 25 seconds to keep the TCP connection alive.

Error responses before the stream opens

StatusCause
400Last-Event-ID is present but is not a valid UUIDv7
401Token is missing, expired, or invalid
404Hub does not exist, contact is not an active member, or contact is banned
429Contact already has 5 open SSE connections (across all hubs on this instance)
503The server is draining (shutting down). Reconnect after a short delay.

Event frame format

Each event follows the standard SSE wire format:
id: <UUIDv7 event id>
event: <event type>
data: <JSON payload>

The id field is a UUIDv7 string. Store it as Last-Event-ID for gap-free reconnects.

Event types

All emitted event types are registered at startup. Current set (updated with each release):

Community — spaces

TypeWhen
community.space.createdA space was created
community.space.updatedA space was updated
community.space.deletedA space was deleted
community.space.member_joinedA member joined a space
community.space.member_leftA member left a space
community.space.member_mutedA member was muted in a space
community.space.member_unmutedA member was unmuted in a space

Community — discussions

TypeWhen
community.discussion.publishedA discussion was published
community.discussion.updatedA discussion was updated
community.discussion.deletedA discussion was deleted
community.discussion.lockedA discussion was locked
community.discussion.unlockedA discussion was unlocked
community.discussion.pinnedA discussion was pinned
community.discussion.unpinnedA discussion was unpinned
community.discussion.reaction_addedA reaction was added
community.discussion.reaction_removedA reaction was removed

Community — profiles and DMs

TypeWhen
community.profile.updatedA community profile was updated
community.conversation.createdA new DM conversation was created
community.conversation.member_addedA member was added to a group DM
community.conversation.member_leftA member left a DM conversation
community.message.createdA DM message was sent
community.message.editedA DM message was edited
community.message.deletedA DM message was tombstoned
community.message.readA DM message was marked read

Broadcasts

TypeWhen
community.broadcast.publishedA broadcast post was published to the hub

Test

TypeWhen
test.pingSynthetic ping for development and smoke tests

Payload shape

Every event’s data is a JSON object. The exact fields vary by event type, but all payloads include identifiers (for example discussion_id, space_id, contact_id) so the client can look up the full resource if needed. Payloads intentionally carry IDs rather than full bodies to keep the event stream lean.

Access scoping

Events are delivered based on the member’s access at connect time:
  • Hub-scoped events (spaces, directory) reach all connected members.
  • Space-scoped events (discussions, reactions) are filtered: restricted spaces are only delivered to members whose segment grants access.
  • Direct-targeted events (DMs, 1:1 presence) are delivered only to the named contact IDs.
If a member’s access changes while they are connected (for example, they are removed from a restricted space), the server updates the connection’s access set without requiring a reconnect.

Terminal events

The server closes the stream by emitting a terminal event before dropping the TCP connection. Terminal events have no id field and are not resumable.
Event typeCause
stream.expiredThe contact’s JWT expired. Refresh the token and reconnect.
stream.unauthorizedThe contact was banned or had access revoked mid-stream.
stream.drainingThe server pod is shutting down. The data includes a retry_ms hint; wait that many milliseconds before reconnecting.
stream.stale_resumeThe Last-Event-ID cursor is older than the 5-minute retention window. The client must do a full page refresh to reconcile state before reconnecting.

Reconnect and gap-free resume

Store the most recent id value from received events. On reconnect, send it as Last-Event-ID:
GET /realtime/events?hub_id={hub_id}
Authorization: Bearer <contact-access-token>
Last-Event-ID: 018f4b3c-1234-7abc-8def-000000000001
The server replays all events emitted after that ID from the last 5 minutes, then switches to live delivery. If the cursor is older than 5 minutes the server emits stream.stale_resume — on receiving that event, perform a full page refresh to reload state, then reconnect without a Last-Event-ID. Last-Event-ID must be a UUIDv7 string. Sending any other format returns 400.

Heartbeat

The server sends an SSE comment line every 25 seconds:
: heartbeat

This keeps the TCP connection alive through proxies. No action is needed on the client; SSE clients ignore comment lines. If neither an event nor a heartbeat is received for 90 seconds the server closes the connection — reconnect using the standard Last-Event-ID flow.

Connection limit

A single contact may hold at most 5 concurrent SSE connections across all hubs. Opening a sixth connection returns 429. Close unused connections (for example, when navigating away from a hub view) to stay within this limit.

X-API-Version

Every SSE response (including the initial HTTP 200 that establishes the stream) includes the X-API-Version response header set to the running build version. See X-API-Version below.

X-API-Version header

Every response from the API — including SSE streams, webhooks, and preflight CORS responses — includes:
X-API-Version: 0.3.1
The value is the build version of the currently running instance (same as the version field from GET /version). Use it to:
  • Confirm a deploy has completed and the expected version is live.
  • Emit version metadata in client telemetry.
  • Debug which build served a specific request when the X-Request-ID alone is not enough.
The header is set on the outermost middleware layer, so it is present even on responses that short-circuit before reaching any route handler (for example, CORS preflight responses). You do not need to send X-API-Version on requests — it is a response-only header.