Skip to content

Client Integration Guide

OpenAPI and protobuf define request, response, and message shapes. Clients also need to handle authentication, MFA, Realtime, media headers, and retry behavior. Web, desktop, mobile, CLI, bot, and third-party SDK integrations use these conventions.

For a minimal runnable call chain, see SDK and API Examples. For unified error handling, see Errors.

GoalImplementation orderKey page
Sign in and keep a sessionLogin, handle MFA, store access/refresh tokens, refresh sessions, handle 401This page: “Token Usage” and “Local Login and MFA”
Enter room realtime stateFetch the room, create a WebSocket ticket, connect /ws/rooms/{roomId}, decode protobufRealtime API
Show synchronized playbackRead current playback state, fetch playback, observe playbackState and playbackPlayback Model
Add mediaChoose Provider, browse or search media, submit playlist item, handle Provider errorsMedia Sources
Handle URL expiryRead expires_at, refresh playback before expiry, discard old URLs after media switchPlayback and Proxy Model
ReconnectRequest a new ticket, reconnect, observe resources with local versionsRealtime API
Build unified errorsClassify by HTTP status, business code, requestId, and Retry-AfterErrors

Implement sign-in, room list, ticket creation, Realtime, and playbacks first. Then add Providers, chat, notifications, and WebRTC.

ScenarioInterfaceNotes
Normal business operationsHTTP/OpenAPI or public gRPCUsers, rooms, playlists, notifications, providers, and most business APIs can use either style.
SDK generationHTTP/OpenAPIGenerate TypeScript, Kotlin, Swift, Go, or other clients from /api-docs/openapi.json.
Strongly typed internal clientsgRPCUse synctv-proto/proto/client.proto, oauth2.proto, and provider protobuf files.
Room realtime stateWebSocket or gRPC streamUse for playback state, chat, and WebRTC signaling.
Operations managementmanagement gRPC or CLIDo not expose the management endpoint to normal clients. Prefer the CLI.
Media playbackDirect provider URL or SyncTV proxy URLProviders decide headers, proxy policy, and Range behavior. Clients should not infer upstream rules.

Room chat is a durable message system. HTTP and gRPC handle history, single-message lookup, context lookup, send, edit, delete, attachment upload sessions, and read state. Realtime connections deliver message events after explicit subscription.

HTTP endpoints:

GoalHTTP
Send messagePOST /api/rooms/{roomId}/chat/messages
Create attachment upload sessionPOST /api/rooms/{roomId}/chat/attachments/upload-session
Edit messagePATCH /api/rooms/{roomId}/chat/messages/{messageId}
Delete messageDELETE /api/rooms/{roomId}/chat/messages/{messageId}
Page historyGET /api/rooms/{roomId}/chat/history
Fetch playback-scoped chatGET /api/rooms/{roomId}/chat/playback-messages
Fetch one messageGET /api/rooms/{roomId}/chat/messages/{messageId}
Fetch message contextGET /api/rooms/{roomId}/chat/messages/{messageId}/context
Mark readPOST /api/rooms/{roomId}/chat/read-state
Fetch read stateGET /api/rooms/{roomId}/chat/read-state
SSE message eventsGET /api/rooms/{roomId}/watch/chat-events

gRPC methods on synctv.client.RoomService:

GoalRPC
Send messageSendChatMessage
Create attachment upload sessionCreateChatAttachmentUploadSession
Edit messageEditChatMessage
Delete messageDeleteChatMessage
Page historyGetChatHistory
Fetch one messageGetChatMessage
Fetch message contextGetChatMessageContext
Fetch playback-scoped chatGetChatPlaybackMessages
Mark readMarkChatRead
Fetch read stateGetChatReadState
Server-stream message eventsWatchChatEvents

Client rules:

  • client_message_id is the send idempotency key. client_operation_id is the edit/delete idempotency key. Generate a stable key for one user operation and reuse it for retries.
  • display_position and display_color are chat presentation metadata. The server validates, stores, and forwards these fields; clients may use them for any presentation mode.
  • When a message is sent, the server records the current playback context: playback_media_id, playback_playlist_id, playback_target, playback_target_hash, and playback_position_seconds. GetChatPlaybackMessages reads historical chat for the current playback object and time window.
  • expected_version enables optimistic locking for edit and delete. On conflict, reload the message or context before submitting again.
  • metadata must be a JSON object. Empty metadata can be omitted.
  • User avatars, media covers, room covers, and playlist covers also use the FileUploadReference returned by upload sessions. Update APIs submit avatar_reference or cover_reference, and only id is required; UserAvatar, MediaCover, FileCover, and ResourceCover are server-returned display models for rendering URL, MIME type, dimensions, and metadata.
  • Files and images use CreateChatAttachmentUploadSession. The session returns attachment_reference, and clients pass it in SendChatMessage.attachments. Upload references use kind=UPLOAD, and attachment_reference.id is the client-visible attachment id. upload_token is an upload-session transport token used only for object uploads and CompleteChatAttachmentUploadSession.
  • History, single-message, and context responses return ChatAttachment.reuse_token and reuse_expires_at. To copy, forward, or resend an already visible chat attachment, submit ChatAttachmentReference { kind=REUSE, id=reuse_token }. The server rechecks that the current user can still view the source message and attachment, then creates a new business reference for the new message; clients do not download the source file, compute a full-file hash, or upload bytes again.
  • Instant-upload ownership proof is for the flow where a client claims it locally owns bytes matching a content_manifest_sha256. Chat attachment reuse tokens are for the flow where the server already authorized the current user to see an attachment. Both flows still pass through attachment count, MIME, size, and room permission validation during send.
  • Attachments are independent message parts. Message content may be empty and may omit any inline reference to attached files; frontends can render ChatMessageReceive.attachments below the message. @ mentions are inline content entities and must point to a matching @token range in content.
  • Create an upload session first with empty parts; the server returns a FileUploadPlan containing part_size_bytes and each part’s part_number, offset_bytes, and size_bytes. The client hashes every planned part with SHA-256, then calls the same create API again with FileUploadManifestPart[]. The server computes content_manifest_sha256 from the canonical manifest and uses it for instant-upload hits and resumable-session lookup.
  • When upload_required=true, upload according to the session fields. Database backends return upload_url; PUT each planned part with Content-Range: bytes start-end/total. S3 backends return part_urls; PUT each part to its presigned URL with the signed x-amz-checksum-sha256 header. Completion submits file_id=attachment_reference.id, token=session.upload_token, upload_id, ownership proof, and each part’s ETag, size, and SHA-256. The server validates the recorded part manifest against content_manifest_sha256; the S3 path does not download the object.
  • When ownership_proof_required=true, read the returned ownership_proof_ranges from the local file, compute the SHA-256 hex proof with ownership_proof_nonce, and submit file_id, token, and ownership_proof to the complete upload-session API. The proof hash input order is synctv-file-ownership-proof-v1, one 0x00 byte, nonce UTF-8, one 0x00 byte, lowercase ASCII content_manifest_sha256, size_bytes as big-endian i64, range count as big-endian u64, then each range’s offset as big-endian i64, length as big-endian i32, and range bytes.
  • Chat attachments support images, audio, video, common documents, archives, text, CSV, Markdown, JSON, and PDF. Image attachments carry kind=image plus optional width and height for previews; other attachments carry kind=file.
  • Delete is soft delete: history and events keep the message id, version, delete actor, and reason, while content and attachments are hidden in deleted views.

Event subscription:

  • In bidirectional MessageStream, send observeResource.chatEvents to receive later resourceChanged.chatEvent payloads.
  • WatchChatEvents is the gRPC server-streaming version.
  • HTTP SSE uses /watch/chat-events; initial recovery can pass afterEventSequence, and browser reconnects use Last-Event-ID.
  • The event cursor is ChatMessageEvent.eventId, separate from generic resource versions.

The examples show how to compose calls. Exact fields are defined by /api-docs/openapi.json and protobuf.

Terminal window
curl -sS http://localhost:8080/api/auth/opaque/login/start \
-H 'Content-Type: application/json' \
-d '{
"username": "root",
"credentialRequest": [/* bytes from your OPAQUE client */]
}'

Finish the OPAQUE exchange with /api/auth/opaque/login/finish. A typical finish response contains accessToken, refreshToken, user, and mfa. If mfa.required=true, complete MFA before treating the user as logged in. Passwordless email-token login uses /api/auth/email/confirm.

Business APIs use Bearer tokens. Signed-in users send access tokens; guests send guest tokens when reading guest-enabled room data:

Authorization: Bearer <access_token_or_guest_token>

Client guidance:

  • Use access tokens only for short-lived API calls.
  • Use guest tokens only for the public room they were issued for. They can read or participate in guest-enabled room data, but cannot log in, refresh a session, log out, call account APIs, or access media resources.
  • When a room grants guests use_webrtc, guests use the same guest token to fetch ICE servers, connect to Realtime, and send WebRTC signaling.
  • Use refresh tokens only for session refresh. Do not put them in WebSocket query strings, media URLs, or third-party requests.
  • After refresh, replace the locally stored token pair with the server response. Avoid concurrent refresh attempts with stale refresh tokens.
  • When a user enables 2FA, local login must complete MFA before receiving an acceptable token. OAuth2 login is not part of local 2FA, but OAuth2-issued tokens are accepted for that login path.

Production deployments must use TLS. Login, registration, password changes, password reset, and MFA verification must use HTTPS or a trusted encrypted tunnel.

LoginResponse always has the same shape. It can return final tokens directly or return mfa.required=true.

Client flow:

  1. Submit the first factor, such as password, OPAQUE, passkey, or email-code login.
  2. Read LoginResponse.mfa.required.
  3. If required=false, store accessToken and refreshToken.
  4. If required=true, store the short-lived mfa.session_id and present available_methods.
  5. If MFA_METHOD_EMAIL is available, call RequestMfaEmailCode when the user chooses email.
  6. If MFA_METHOD_WEBAUTHN is available, call StartMfaPasskey for WebAuthn options, then FinishMfaPasskey.
  7. After the second factor succeeds, the final response returns usable tokens.

GetUserPreferences returns auth_factors, which lets clients show whether the current user has password, WebAuthn, or verified email factors. Enabling 2FA requires at least two local factors. Password is a first factor completed through OPAQUE or direct password login. OAuth2 does not count as a local 2FA factor.

Email codes can be used for login, MFA, email verification, and password reset. Keep those flows separate in client state.

Standalone email login request:

Terminal window
curl -sS http://localhost:8080/api/auth/email/request \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com"}'

Then call login with email and email_token. Do not mix email-login tokens with MFA email codes; MFA email codes are bound to mfa_session_id.

OAuth2/OIDC is frontend-driven:

  1. Call GET /api/oauth2/providers to discover provider instances, signupEnabled, and signupNeedReview.
  2. Call GET /api/oauth2/{provider}/authorize?redirectUrl=<callback> to get the provider authorization URL and state.
  3. Redirect the user to the provider.
  4. The provider redirects back to the client URL with code and state.
  5. Call POST /api/oauth2/{provider}/exchange.
  6. If the response has registrationReviewRequired=true, show pending-review state and keep registrationReviewId; do not treat it as logged in.
  7. If the response contains tokens, store the SyncTV access and refresh tokens.

Minimal exchange:

Terminal window
curl -sS http://localhost:8080/api/oauth2/github/exchange \
-H 'Content-Type: application/json' \
-d '{
"code": "code-from-callback",
"state": "stateFromAuthorizeResponse123456"
}'

Use GET /api/oauth2/{provider}/bind?redirectUrl=<callback> when binding a provider to an existing authenticated account. The bind authorization request and the later POST /api/oauth2/{provider}/exchange call must both include the current user’s access token; the exchange token user must match the user captured in the OAuth2 state. OAuth2 login is not a local 2FA first or second factor, but users with 2FA enabled can still log in through OAuth2.

Public gRPC uses the same main port. With reflection:

Terminal window
grpcurl -plaintext localhost:8080 list synctv.client.AuthService

The public AuthService local password flow uses OPAQUE. Generate the credential request in your client and call StartOpaqueLogin, then finish with FinishOpaqueLogin:

Terminal window
grpcurl -plaintext \
-d '{"username":"root","credentialRequest":"<base64-opaque-request>"}' \
localhost:8080 synctv.client.AuthService/StartOpaqueLogin

AuthService/Login is reserved for passwordless email-token login.

Room-scoped RPCs need both user identity and room context:

Terminal window
grpcurl -plaintext \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "x-room-id: ${ROOM_ID}" \
-d '{}' \
localhost:8080 synctv.client.RoomService/GetPlayback

Do not put the room id only in the request body and expect the server to infer it. RPCs marked with x-room-id metadata in client.proto should send that metadata explicitly.

WebSocket URL:

wss://<host>/ws/rooms/<roomId>

Authentication options:

MethodClientsNotes
Authorization: Bearer <token>Native clients, CLI, server-side SDKsThe token is not placed in the URL.
?ticket=<ticket>Browsers and clients that cannot set headersFirst call POST /api/tickets to obtain a short-lived one-time ticket.

Browser-style flow:

  1. Call POST /api/tickets with a normal Bearer token and target roomId.
  2. Receive ticket and its expiration.
  3. Connect to wss://<host>/ws/rooms/<roomId>?ticket=<ticket>.
  4. Do not reuse tickets after success or failure. Request a new one instead.

WebSocket only handles binary protobuf frames. Clients send synctv.client.ClientMessage, and the server returns synctv.client.ServerMessage; text frames are ignored. Browser clients should send the Uint8Array produced by generated protobuf code and decode received Blob or ArrayBuffer values into ServerMessage.

Realtime resource observation lets a room WebSocket subscribe to cacheable resources such as playback state, playbacks, room settings, playlist items, and room members. Clients send ClientMessage.observeResource, and the server returns resourceObserved, resourceChanged, or resourceObserveError.

See Realtime API for the full protocol, delivery modes, version synchronization, examples for every resource type, reconnect behavior, and error handling.

WebRTC clients fetch ICE servers before connecting to Realtime for signaling. Signed-in members and authorized guests both use the standard Bearer token path:

GET /api/rooms/<roomId>/webrtc/ice-servers
Authorization: Bearer <access_token_or_guest_token>

Access rules:

  • A signed-in member must belong to the room and have use_webrtc.
  • A guest must use a guest token issued for that room, and the room must allow guests and grant use_webrtc.
  • If the room is closed, banned, password-protected, or guest access is revoked, guests cannot fetch fresh ICE configuration.

The response includes a webrtc status object. Clients can surface a non-secret warning when builtin_stun_state is degraded or when no ICE servers are returned, while still using any external STUN/TURN servers present in servers.

Provider playback results may include URLs, proxy mode, direct mode, and required headers. Client rules:

  • Use the headers returned by the provider. Do not invent User-Agent, Referer, or Range.
  • If a provider returns a direct URL with required headers, the client runtime must be able to set those headers.
  • If the client cannot set required headers, use the provider proxy URL or request proxy mode.
  • Range playback depends on the provider and upstream origin. SyncTV proxy does not automatically forward raw client headers.

See Media Providers and Proxy Slice Cache for provider and proxy behavior.

HTTP error body:

{
"error": "Invalid username or password",
"status": 401,
"code": null,
"requestId": "..."
}

Handle errors by category, not by exact text:

StatusMeaningClient action
400Invalid request shape or fieldsFix input and show field errors when useful
401Expired token, invalid ticket, or unauthenticatedStart login or refresh
403Authenticated but not authorizedShow permission error and do not retry endlessly
404Resource not foundRefresh local lists or navigate back
409State conflictRe-fetch the resource before submitting again
429Rate-limitedBack off using Retry-After or server guidance
5xxServer or dependency failureShow retry and collect requestId, time, and path