Client Integration Guide
Integration Entry
Section titled “Integration Entry”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.
Common Integration Recipes
Section titled “Common Integration Recipes”| Goal | Implementation order | Key page |
|---|---|---|
| Sign in and keep a session | Login, handle MFA, store access/refresh tokens, refresh sessions, handle 401 | This page: “Token Usage” and “Local Login and MFA” |
| Enter room realtime state | Fetch the room, create a WebSocket ticket, connect /ws/rooms/{roomId}, decode protobuf | Realtime API |
| Show synchronized playback | Read current playback state, fetch playback, observe playbackState and playback | Playback Model |
| Add media | Choose Provider, browse or search media, submit playlist item, handle Provider errors | Media Sources |
| Handle URL expiry | Read expires_at, refresh playback before expiry, discard old URLs after media switch | Playback and Proxy Model |
| Reconnect | Request a new ticket, reconnect, observe resources with local versions | Realtime API |
| Build unified errors | Classify by HTTP status, business code, requestId, and Retry-After | Errors |
Implement sign-in, room list, ticket creation, Realtime, and playbacks first. Then add Providers, chat, notifications, and WebRTC.
Choose an Interface
Section titled “Choose an Interface”| Scenario | Interface | Notes |
|---|---|---|
| Normal business operations | HTTP/OpenAPI or public gRPC | Users, rooms, playlists, notifications, providers, and most business APIs can use either style. |
| SDK generation | HTTP/OpenAPI | Generate TypeScript, Kotlin, Swift, Go, or other clients from /api-docs/openapi.json. |
| Strongly typed internal clients | gRPC | Use synctv-proto/proto/client.proto, oauth2.proto, and provider protobuf files. |
| Room realtime state | WebSocket or gRPC stream | Use for playback state, chat, and WebRTC signaling. |
| Operations management | management gRPC or CLI | Do not expose the management endpoint to normal clients. Prefer the CLI. |
| Media playback | Direct provider URL or SyncTV proxy URL | Providers decide headers, proxy policy, and Range behavior. Clients should not infer upstream rules. |
Chat Message API
Section titled “Chat Message API”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:
| Goal | HTTP |
|---|---|
| Send message | POST /api/rooms/{roomId}/chat/messages |
| Create attachment upload session | POST /api/rooms/{roomId}/chat/attachments/upload-session |
| Edit message | PATCH /api/rooms/{roomId}/chat/messages/{messageId} |
| Delete message | DELETE /api/rooms/{roomId}/chat/messages/{messageId} |
| Page history | GET /api/rooms/{roomId}/chat/history |
| Fetch playback-scoped chat | GET /api/rooms/{roomId}/chat/playback-messages |
| Fetch one message | GET /api/rooms/{roomId}/chat/messages/{messageId} |
| Fetch message context | GET /api/rooms/{roomId}/chat/messages/{messageId}/context |
| Mark read | POST /api/rooms/{roomId}/chat/read-state |
| Fetch read state | GET /api/rooms/{roomId}/chat/read-state |
| SSE message events | GET /api/rooms/{roomId}/watch/chat-events |
gRPC methods on synctv.client.RoomService:
| Goal | RPC |
|---|---|
| Send message | SendChatMessage |
| Create attachment upload session | CreateChatAttachmentUploadSession |
| Edit message | EditChatMessage |
| Delete message | DeleteChatMessage |
| Page history | GetChatHistory |
| Fetch one message | GetChatMessage |
| Fetch message context | GetChatMessageContext |
| Fetch playback-scoped chat | GetChatPlaybackMessages |
| Mark read | MarkChatRead |
| Fetch read state | GetChatReadState |
| Server-stream message events | WatchChatEvents |
Client rules:
client_message_idis the send idempotency key.client_operation_idis the edit/delete idempotency key. Generate a stable key for one user operation and reuse it for retries.display_positionanddisplay_colorare 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, andplayback_position_seconds.GetChatPlaybackMessagesreads historical chat for the current playback object and time window. expected_versionenables optimistic locking for edit and delete. On conflict, reload the message or context before submitting again.metadatamust be a JSON object. Empty metadata can be omitted.- User avatars, media covers, room covers, and playlist covers also use the
FileUploadReferencereturned by upload sessions. Update APIs submitavatar_referenceorcover_reference, and onlyidis required;UserAvatar,MediaCover,FileCover, andResourceCoverare server-returned display models for rendering URL, MIME type, dimensions, and metadata. - Files and images use
CreateChatAttachmentUploadSession. The session returnsattachment_reference, and clients pass it inSendChatMessage.attachments. Upload references usekind=UPLOAD, andattachment_reference.idis the client-visible attachment id.upload_tokenis an upload-session transport token used only for object uploads andCompleteChatAttachmentUploadSession. - History, single-message, and context responses return
ChatAttachment.reuse_tokenandreuse_expires_at. To copy, forward, or resend an already visible chat attachment, submitChatAttachmentReference { 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.attachmentsbelow the message.@mentions are inline content entities and must point to a matching@tokenrange incontent. - Create an upload session first with empty
parts; the server returns aFileUploadPlancontainingpart_size_bytesand each part’spart_number,offset_bytes, andsize_bytes. The client hashes every planned part with SHA-256, then calls the same create API again withFileUploadManifestPart[]. The server computescontent_manifest_sha256from 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 returnupload_url; PUT each planned part withContent-Range: bytes start-end/total. S3 backends returnpart_urls; PUT each part to its presigned URL with the signedx-amz-checksum-sha256header. Completion submitsfile_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 againstcontent_manifest_sha256; the S3 path does not download the object. - When
ownership_proof_required=true, read the returnedownership_proof_rangesfrom the local file, compute the SHA-256 hex proof withownership_proof_nonce, and submitfile_id,token, andownership_proofto the complete upload-session API. The proof hash input order issynctv-file-ownership-proof-v1, one0x00byte, nonce UTF-8, one0x00byte, lowercase ASCIIcontent_manifest_sha256,size_bytesas big-endian i64, range count as big-endian u64, then each range’soffsetas big-endian i64,lengthas 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=imageplus optional width and height for previews; other attachments carrykind=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, sendobserveResource.chatEventsto receive laterresourceChanged.chatEventpayloads. WatchChatEventsis the gRPC server-streaming version.- HTTP SSE uses
/watch/chat-events; initial recovery can passafterEventSequence, and browser reconnects useLast-Event-ID. - The event cursor is
ChatMessageEvent.eventId, separate from generic resource versions.
Minimal HTTP Flow
Section titled “Minimal HTTP Flow”The examples show how to compose calls. Exact fields are defined by /api-docs/openapi.json and protobuf.
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.
ACCESS_TOKEN='paste-access-token-here'ROOM_ID='room_1' # Use the room.id returned by create/list room APIs
curl -sS http://localhost:8080/api/tickets \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H 'Content-Type: application/json' \ -d "{\"roomId\":\"${ROOM_ID}\"}"The returned ticket is short-lived, one-time-use, and room-bound.
Browsers cannot reliably set a WebSocket Authorization header, so use a ticket:
const roomId = 'room_1'; // Use the room.id returned by create/list room APIsconst 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); // generated from client.proto console.log(message);};Native clients and server-side SDKs can use:
Authorization: Bearer <accessToken>Token Usage
Section titled “Token Usage”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.
Local Login and MFA
Section titled “Local Login and MFA”LoginResponse always has the same shape. It can return final tokens directly or return mfa.required=true.
Client flow:
- Submit the first factor, such as password, OPAQUE, passkey, or email-code login.
- Read
LoginResponse.mfa.required. - If
required=false, storeaccessTokenandrefreshToken. - If
required=true, store the short-livedmfa.session_idand presentavailable_methods. - If
MFA_METHOD_EMAILis available, callRequestMfaEmailCodewhen the user chooses email. - If
MFA_METHOD_WEBAUTHNis available, callStartMfaPasskeyfor WebAuthn options, thenFinishMfaPasskey. - 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
Section titled “Email Codes”Email codes can be used for login, MFA, email verification, and password reset. Keep those flows separate in client state.
Standalone email login request:
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
Section titled “OAuth2”OAuth2/OIDC is frontend-driven:
- Call
GET /api/oauth2/providersto discover provider instances,signupEnabled, andsignupNeedReview. - Call
GET /api/oauth2/{provider}/authorize?redirectUrl=<callback>to get the provider authorization URL and state. - Redirect the user to the provider.
- The provider redirects back to the client URL with
codeandstate. - Call
POST /api/oauth2/{provider}/exchange. - If the response has
registrationReviewRequired=true, show pending-review state and keepregistrationReviewId; do not treat it as logged in. - If the response contains tokens, store the SyncTV access and refresh tokens.
Minimal exchange:
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.
gRPC Examples
Section titled “gRPC Examples”Public gRPC uses the same main port. With reflection:
grpcurl -plaintext localhost:8080 list synctv.client.AuthServiceThe public AuthService local password flow uses OPAQUE. Generate the credential request in your client and call StartOpaqueLogin, then finish with FinishOpaqueLogin:
grpcurl -plaintext \ -d '{"username":"root","credentialRequest":"<base64-opaque-request>"}' \ localhost:8080 synctv.client.AuthService/StartOpaqueLoginAuthService/Login is reserved for passwordless email-token login.
Room-scoped RPCs need both user identity and room context:
grpcurl -plaintext \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "x-room-id: ${ROOM_ID}" \ -d '{}' \ localhost:8080 synctv.client.RoomService/GetPlaybackDo 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
Section titled “WebSocket”WebSocket URL:
wss://<host>/ws/rooms/<roomId>Authentication options:
| Method | Clients | Notes |
|---|---|---|
Authorization: Bearer <token> | Native clients, CLI, server-side SDKs | The token is not placed in the URL. |
?ticket=<ticket> | Browsers and clients that cannot set headers | First call POST /api/tickets to obtain a short-lived one-time ticket. |
Browser-style flow:
- Call
POST /api/ticketswith a normal Bearer token and targetroomId. - Receive
ticketand its expiration. - Connect to
wss://<host>/ws/rooms/<roomId>?ticket=<ticket>. - 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
Section titled “Realtime Resource Observation”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 Bootstrap
Section titled “WebRTC Bootstrap”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-serversAuthorization: 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.
Media URLs and Headers
Section titled “Media URLs and Headers”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, orRange. - 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.
Error Handling
Section titled “Error Handling”HTTP error body:
{ "error": "Invalid username or password", "status": 401, "code": null, "requestId": "..."}Handle errors by category, not by exact text:
| Status | Meaning | Client action |
|---|---|---|
400 | Invalid request shape or fields | Fix input and show field errors when useful |
401 | Expired token, invalid ticket, or unauthenticated | Start login or refresh |
403 | Authenticated but not authorized | Show permission error and do not retry endlessly |
404 | Resource not found | Refresh local lists or navigate back |
409 | State conflict | Re-fetch the resource before submitting again |
429 | Rate-limited | Back off using Retry-After or server guidance |
5xx | Server or dependency failure | Show retry and collect requestId, time, and path |
Development Entry Points
Section titled “Development Entry Points”- OpenAPI Access: Swagger UI and
/api-docs/openapi.json. - gRPC Debugging: reflection, grpcurl, and protobuf file locations.
- SDK and API Examples: login, refresh, rooms, tickets, Realtime, Providers, and errors.
- Realtime API: WebSocket protobuf, resource observation, version synchronization, and reconnect handling.
- Errors: HTTP/gRPC/Realtime/Provider error structures and codes.
- Authentication and Security Model: 2FA, OAuth2, token context, and provider header boundaries.
- Rooms, Permissions, and Preferences: roles, permission bits, room settings, and user preferences.
- Troubleshooting: CORS, WebSocket, login, provider, and Range diagnostics.