Realtime API
The Realtime API is the room-scoped long-lived connection protocol. It carries chat, playback commands, playback progress, WebRTC signaling, and subscriptions to cacheable room resources.
Use HTTP/OpenAPI or gRPC for one-shot reads and writes. Use Realtime when a client needs low-latency push, less polling, or continuously synchronized room state.
Connection and Authentication
Section titled “Connection and Authentication”WebSocket URL:
wss://<host>/ws/rooms/<roomId>Authentication options:
| Method | Clients | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | Native clients, CLI, server-side SDKs | Signed-in users |
Authorization: Bearer <guest_token> | Native clients, CLI, server-side SDKs | Guests; first call POST /api/auth/guest-token |
?ticket=<ticket> | Browsers and clients that cannot set headers | First call POST /api/tickets to create a short-lived, one-time, room-bound ticket |
WebSocket only handles binary protobuf frames. Clients send synctv.client.ClientMessage, and the server returns synctv.client.ServerMessage. Text frames, ping, pong, and other non-business frames are not processed as business messages.
const roomId = 'room_1';const ticket = ticketResponse.ticket;const ws = new WebSocket(`wss://app.example.com/ws/rooms/${roomId}?ticket=${ticket}`);ws.binaryType = 'arraybuffer';
ws.onmessage = async (event) => { const bytes = event.data instanceof Blob ? new Uint8Array(await event.data.arrayBuffer()) : new Uint8Array(event.data); const message = ServerMessage.decode(bytes); handleServerMessage(message);};
function sendClientMessage(message) { ws.send(ClientMessage.encode(message).finish());}Guest Connections
Section titled “Guest Connections”A guest is not a signed-in user. The server does not create a user row, member row, or login session for a guest. Guest identity is represented only by a room-bound guest token, and public guest IDs use the gst_ prefix.
A guest can enter only when all of these are true:
- Global
user.enable_guestis enabled. - Room
allowGuestJoinis enabled. - The room has no password and is not closed or banned.
- The guest token still matches the room guest version. Disabling guests, adding a password, or revoking guest access invalidates older tokens.
Guests receive only basic realtime room state by default. They cannot read or manage media resources or send chat. A room may grant guest-safe capabilities through guestAddedPermissions, such as member list, chat history, or WebRTC signaling; this ceiling is independent from member permissions. Even with extra room permissions, guests cannot use account, media-resource management, member administration, room administration, Provider credential, or management-plane APIs.
curl -sS "$BASE_URL/api/auth/guest-token" \ -H "Content-Type: application/json" \ -d '{"roomId":"room_1"}'Native WebSocket clients use the returned token:
Authorization: Bearer <guest_token>For gRPC MessageStream, send the same token:
x-room-id: room_1authorization: Bearer <guest_token>When to Use Realtime
Section titled “When to Use Realtime”| Scenario | Why Realtime |
|---|---|
| Synchronized playback | Play, pause, seek, and media changes need low-latency propagation |
| Chat | Chat message events are delivered after explicit subscription |
| Playback | Expired URLs, Provider credential changes, and media switches need refreshes |
| Room members | Joins, leaves, and permission changes need UI sync |
| WebRTC signaling | offer, answer, and ICE candidates need room-scoped forwarding; targets use public_actor_id:conn_id, where signed-in users are usr_* and guests are gst_* |
Before sending signaling messages, WebRTC clients call GET /api/rooms/<roomId>/webrtc/ice-servers to fetch ICE servers. That endpoint uses the same room actor permission model as Realtime: signed-in members and guests both authenticate with Authorization: Bearer <token>, and both need use_webrtc.
Message Scope
Section titled “Message Scope”Common client messages:
ClientMessage field | Purpose |
|---|---|
chat | Send chat |
heartbeat | Client heartbeat |
playback_progress | Report current playback position and play/pause state |
playback_update | Send a playback state update command |
webrtc_offer / webrtc_answer / webrtc_ice_candidate | Forward WebRTC signaling |
webrtc_join / webrtc_leave | Join or leave a WebRTC session |
observeResource | Subscribe to a realtime resource |
unobserveResource | Unsubscribe from a realtime resource |
Common server messages:
ServerMessage field | Purpose |
|---|---|
chat | Chat message received |
heartbeatAck | Heartbeat acknowledgement |
error | General business error |
playbackState / playingChanged | Playback state or playback target changed |
roomSettings | Room settings changed |
mediaAdded / mediaUpdated / mediaRemoved | Media changed |
playlistCreated / playlistUpdated / playlistDeleted | Playlist changed |
permissionChanged / userJoined / userLeft / roomMembers | Member or permission changed |
chatEvent or resourceChanged.chatEvent | Chat message event after explicit subscription |
resourceObserved / resourceChanged / resourceObserveError | Resource observation result |
Use synctv-proto/proto/client.proto as the source of truth for complete fields. End-to-end examples are in SDK and API Examples.
Message Dispatch
Section titled “Message Dispatch”All Realtime API business messages live in the ClientMessage.message and ServerMessage.message oneof fields. Dispatch by oneof type, not text fields or log strings.
function handleServerMessage(message) { if (message.chatEvent) { appendChatEvent(message.chatEvent); return; }
if (message.chat) { appendChat(message.chat); return; }
if (message.playbackState) { updatePlayback(message.playbackState); return; }
if (message.resourceChanged) { if (message.resourceChanged.chatEvent) { appendChatEvent(message.resourceChanged.chatEvent); return; }
handleResourceChanged(message.resourceChanged); return; }
if (message.error) { handleRealtimeError(message.error); }}After connecting, subscribe to the resources needed by the current view. See Realtime Resource Observation.
Chat message events use their own cursor: in bidirectional streams, send observeResource.chatEvents.afterEventSequence to replay events after a known event; HTTP SSE GET /api/rooms/<roomId>/watch/chat-events accepts the afterEventSequence query parameter and reads the browser’s Last-Event-ID header on reconnect. The server sets the SSE id: field, so clients can persist that value for recovery.
Reconnect Flow
Section titled “Reconnect Flow”Observations are not retained across WebSocket connections. After disconnect, clients should:
- Request a new WebSocket ticket, or reconnect with a valid access token.
- Re-send
observeResourcefor currently visible views. - Include the locally saved
versionso the server can decide whether to send a snapshot. - For
playback, also include cachedmediaId,playlistId,target, andplaybackClientProfile. - Keep the local cache when
resourceObserved.changed=false; replace it whenresourceChangedarrives.
If the connection has been down for a long time, fetch critical resources through HTTP/gRPC first, then restore observations. This avoids showing stale UI for too long.
Observation Errors
Section titled “Observation Errors”Resource observation errors are returned as ServerMessage.resourceObserveError:
| Scenario | Client action |
|---|---|
Empty observeId or longer than 128 characters | Fix client code. Do not retry the same request |
Missing resource | Fix client code |
| More than 64 observations on one connection | Unobserve unused resources or merge list views |
| Snapshot service unavailable or load failed | Show a temporary error and retry with backoff |
| Permission, missing resource, or invalid input error | Re-fetch room state and navigate back when needed |
If sending fails or the server closes the connection, do not assume observations remain active. Rebuild all observations after reconnecting.
Continue Reading
Section titled “Continue Reading”- Resource fields and subscription examples: Realtime Resource Observation.
- End-to-end client examples: SDK and API Examples.
- Unified error handling: Errors.