Skip to main content

WebSocket

Real-time communication via WebSocket using a channel-based pub/sub protocol.

Connection

EnvironmentURL
Stagingwss://lumio.api.staging.zaflun.dev/v1/ws
Production Previewwss://lumio.api.prod.zaflun.dev/v1/ws
Productioncoming soon -- official domain not yet assigned

The endpoint is an HTTP GET /v1/ws request that is upgraded to a WebSocket connection. All fields in both directions use snake_case JSON.

Protocol

Lumio uses a custom JSON-message channel-subscription protocol. Clients subscribe to named channels (e.g. events:\{account_id\}, overlay:\{key\}) and receive messages pushed by the server whenever workers publish to the matching Redis pub/sub channel.

Lumio does not use graphql-ws and does not expose GraphQL subscriptions (EmptySubscription). All real-time updates flow through this WebSocket.

Authentication

Auth is resolved by the same middleware that guards REST and GraphQL. The server accepts either:

  • Authorization: Bearer <token> header -- System Key, User API Key, or JWT.
  • ?token=<token> query parameter on the WebSocket URL -- Popout Token (prefix lm_pop_) or JWT. Browsers must use this form because the native WebSocket API cannot send custom headers.

Anonymous connections are allowed but can only subscribe to public overlay:* channels.

Channel access is enforced at subscribe time:

Channel prefixAuth requirement
overlay:\{key\}Public -- any connection (used by OBS browser sources).
events:\{account_id\}Authenticated caller with access to the account.
spotify:\{account_id\}Authenticated caller with access to the account.
chat:\{account_id\}Account access + chat:read permission.
automations:\{account_id\}Account access + automations:read permission.

Client Messages

All client messages are JSON with a type discriminator.

typeFieldsPurpose
subscribechannel: stringSubscribe to a channel. Server responds with subscribed or error.
unsubscribechannel: stringUnsubscribe. Server responds with unsubscribed.
broadcastchannel: string, payload: stringBroadcast a raw JSON string (payload is pre-serialized) to every session subscribed to channel. Requires the same access as subscribe for that channel. Wrapped by the server as \{"type":"event","data":<payload>\} before forwarding.
ping--Heartbeat. Server replies with pong.

Example:

{"type":"subscribe","channel":"events:550e8400-e29b-41d4-a716-446655440000"}

Messages larger than 64 KiB are rejected with MESSAGE_TOO_LARGE.

Server Messages

All server messages are JSON with a type discriminator.

typeFieldsMeaning
welcomesession_id: string, server_version: stringSent once immediately after the socket opens.
subscribedchannel: stringSubscription accepted.
unsubscribedchannel: stringUnsubscription accepted.
pong--Reply to ping.
errormessage: string, code: stringError. See codes below.
eventdata: objectEvent payload relayed from a subscribed channel. See "Relay envelope" below.
spotify:now-playingdata: objectSpotify playback-state update relayed from spotify:\{account_id\}.
chat:messagedata: objectChat event relayed from chat:\{account_id\} (new messages, deletions, moderation logs, etc.).
automationdata: objectAutomation-engine update relayed from automations:\{account_id\}.

Relay envelope

When a worker publishes to a Redis pub/sub channel, the API's event-relay worker forwards the raw payload to every subscribed WebSocket session, wrapped in a typed envelope. The type of the envelope depends on the Redis channel prefix:

Redis channelWebSocket envelope typePublished to sessions subscribed on
lumio:events:\{account_id\}eventevents:\{account_id\}
lumio:spotify:\{account_id\}spotify:now-playingspotify:\{account_id\}
lumio:chat:\{account_id\}chat:messagechat:\{account_id\}
lumio:automations:\{account_id\}automationautomations:\{account_id\}

The relay is one-way (Redis to WebSocket) and message-preserving -- data contains the exact JSON the publisher sent (including fields like type, event_type, payload, etc. that are specific to the event family).

Example relayed follower event on events:\{account_id\}:

{"type":"event","data":{"type":"twitch:follower","payload":{"username":"viewer123"}}}

Error codes

codeMeaning
UNAUTHORIZEDCaller lacks access to the requested channel (missing account access or permission).
SUBSCRIBE_FAILEDServer-side failure while registering the subscription.
INVALID_FORMATMessage was not valid JSON or did not match any known client-message shape.
MESSAGE_TOO_LARGEText frame exceeded the 64 KiB limit.

Heartbeats and timeouts

The server ticks a heartbeat every 5 seconds and disconnects any session that has been silent for 10 seconds. Clients should either respond to WebSocket-level pings, send a {"type":"ping"} every few seconds, or maintain any other traffic to keep the connection alive. Standard WebSocket Ping control frames are auto-answered with Pong.

Example Channels

  • events:\{account_id\} -- Stream events (follows, subs, cheers, raids, redemptions, tips, channel online/offline, bot-command updates).
  • spotify:\{account_id\} -- Spotify "now playing" state changes for the Spotify overlay widget.
  • chat:\{account_id\} -- Live chat messages, deletions, moderation logs, user-treatment updates.
  • automations:\{account_id\} -- Automation-engine lifecycle updates (triggers, execution progress).
  • overlay:\{key\} -- Per-overlay updates for browser-source popouts. Public channel keyed by the overlay's non-enumerable key.