WebSocket
The WebSocket endpoint is wss://api.lumio.vision/v1/ws. It uses a custom JSON protocol, not graphql-ws.
Server-side clients can authenticate API keys with the upgrade request header:
Authorization: Bearer lm_svc_your_key
Browser clients cannot set upgrade headers. API keys are not accepted in ?token=, so browser clients must mint a WebSocket token first with POST /v1/auth/ws-token or GraphQL issueWsToken, then connect with wss://api.lumio.vision/v1/ws?token=<ws_token>.
Invalid, expired, or revoked API keys resolve as anonymous. The upgrade can still return 101 Switching Protocols; the first guarded subscribe returns an error frame such as {"type":"error","code":"UNAUTHORIZED","message":"Not authorized to subscribe to events:..."}.
Frames
Client frames use a type discriminator:
| Client frame | JSON shape |
|---|---|
| Subscribe | {"type":"subscribe","channel":"events:<account_id>"} |
| Unsubscribe | {"type":"unsubscribe","channel":"events:<account_id>"} |
| Ping | {"type":"ping"} |
Server frames also use type:
| Server frame | JSON shape |
|---|---|
| Welcome | {"type":"welcome","session_id":"...","server_version":"..."} |
| Pong | {"type":"pong"} |
| Subscribed | {"type":"subscribed","channel":"events:<account_id>"} |
| Unsubscribed | {"type":"unsubscribed","channel":"events:<account_id>"} |
| Event | {"type":"event","channel":"events:<account_id>","event_type":"bootstrap","data":{}} |
| Error | {"type":"error","code":"UNAUTHORIZED","message":"..."} |
The socket is read-only for clients: there is no publish frame. Every channel is published to by Lumio itself. Any other frame type, including broadcast, gets an INVALID_FORMAT error frame and has no effect. To emit an event, use the REST/GraphQL mutation for it, for example POST /v1/events/emit.
Limits And Heartbeat
| Limit | Value |
|---|---|
| Client heartbeat interval | Send a ping at least every 5 seconds |
| Idle timeout | 30 seconds without inbound activity |
| Maximum inbound text message | 65,536 bytes |
| Maximum subscribed channels per session | 100 |
The server replies to client ping frames with pong. It does not send application-level pings; liveness depends on inbound client traffic. The same 5-second heartbeat tick also checks token expiry, and every 30 seconds it re-validates the credential the session was opened with (any token kind) against the database.
Channel Gates
| Channel | Subscribe gate | Required scope | Feature gate |
|---|---|---|---|
overlay:<key> | Overlay token for that overlay | None | feature:overlays |
events:<account_id> | Account access plus permission | events:read | feature:events |
spotify:<account_id> | Account access plus permission | spotify:read | feature:music |
chat:<account_id> | Account access plus permission | chat:read | feature:multichat |
automations:<account_id> | Account access plus permission | automations:read | feature:automation |
ext-storage:<install_id> | The install's extension token, access to the install's owning account plus permission, or an overlay/widget token that renders the install | extension-store:read | feature:extensions |
login-assignments:<id> | Public | None | None |
ideas:<id> | Public when enabled | None | system:ideas_hub; anonymous also requires system:ideas_hub_public |
widget:<widget_instance_id> | Widget token for that widget | None | feature:widgets |
presence:<resource_channel> | Account that owns the wrapped resource | None | None |
ext-logs:<extension_id> | Extension owner gate, currently fail-closed | None | feature:extension_development |
ext-install-logs:<install_id> | Access to the install's owning account plus its category permission | bot-modules:read (bot module) or automations:read (automation node) | feature:bot_module_extensions |
sounds:<account_id> | Account access | None | feature:sounds |
history:<account_id> | Account access plus permission | history:read | feature:stream_history |
public-stats:<platform>:<broadcast_id> | Public when enabled | None | system:public_stats |
API keys can use the account-scoped channels whose gates are based on account access and scopes. API keys cannot use overlay, widget, or ext-logs channels by construction. ext-storage is the SDK storage-sync channel for extension installs, not a general account event stream: its id is an install id, and a user API key or service key passes only for an install of its own account. presence and login-assignments have no WebSocket feature gate.
Error Codes
| Code | When it appears |
|---|---|
UNAUTHORIZED | Subscribe failed the channel gate. |
FEATURE_DISABLED | The channel's feature flag is disabled. |
INVALID_FORMAT | The frame is not valid JSON or is not a known client frame (subscribe, unsubscribe, ping). |
MESSAGE_TOO_LARGE | The inbound text frame exceeds 65,536 bytes. |
TOKEN_EXPIRED | A JWT or shared overlay token expires during the session. |
TOKEN_REVOKED | The session's credential failed its 30-second re-validation: an API key, service key, developer key, popout, overlay, shared overlay, extension or widget token was revoked, rotated, deleted or expired, or its bound member left the account. |
AUTH_UNAVAILABLE | The credential could not be re-validated twice in a row (database timeout or error); the session is closed rather than kept open unchecked. Reconnect with backoff. |
CHANNEL_DROPPED | A channel forwarder dropped because the session could not keep up. |
RATE_LIMITED | A subscribe or unsubscribe frame exceeded the per-session limit: 120 back-to-back, then 5 per second. The frame is dropped and the session stays open; retry after the delay in the message. |