Base URL (default): http://<host>:8787
RelayTV serves its HTTP API from the root path. Most endpoints return JSON. HTML, SVG, image, and SSE endpoints are called out explicitly below.
This file is the active endpoint reference for the native Qt runtime. Historical compat-only endpoints are removed from the active tree and are not documented here.
GET /ui: main web UI HTMLGET /ui/events- Server-Sent Events stream for the main web UI
GET /idle: idle dashboard HTMLGET /: redirects to/uiGET /health:{"ok": true}GET /manifest.json: PWA manifestGET /sw.js: service workerGET /thumbs/{filename}: cached thumbnail imageGET /snapshots/{filename}: saved JPEG snapshotGET /snapshotandPOST /snapshot: capture a snapshot of active playbackGET /share- query:
urlorlink, optionalcec - share-target compatible immediate play helper
- query:
Selected SVG/asset helpers also exist for the UI:
GET /qr/connect.svgGET /assets/logo.svgGET /assets/banner.svgGET /assets/banner.pngGET /pwa/brand/logo.svgGET /pwa/brand/banner.svgGET /pwa/brand/banner.pngGET /pwa/weather/{asset_name}GET /pwa/icon.svgGET /pwa/splash.svgGET /pwa/jellyfin.svgGET /pwa/{asset_path:path}GET /favicon.ico
GET /ui/events is the stable browser-state push path for the native UI. It is not a durable event log and does not support replay cursors or sequence resumption. The contract is:
- snapshot events remain authoritative
- hint events trigger targeted refresh/render work
- clients should reconnect on disconnect and keep
/statusas bootstrap/fallback
Current /ui/events event types:
hello- initial connection confirmation
ping- keepalive event when no other data was emitted recently
playback- compact fast snapshot derived from
/playback/state - intended for hot now-playing/progress/volume/session updates
- compact fast snapshot derived from
status- full server-authoritative snapshot equivalent to
/status
- full server-authoritative snapshot equivalent to
queue- queue mutation hint with current queue snapshot and
queue_length - currently emitted for add, remove, move, dedupe, clear, and Jellyfin queue mutations
- queue mutation hint with current queue snapshot and
jellyfin- Jellyfin browse/runtime refresh hint
- currently emitted for connect, disconnect, register, catalog cache clear, play, and queue-only Jellyfin actions
Clients should treat status as the authoritative full-state refresh, use playback for fast-path UI updates, and treat queue / jellyfin as immediate refresh hints rather than a standalone source of truth.
Primary play-family endpoints:
POST /play- body:
{"url", "use_ytdlp"?, "cec"?} - immediate play, clears queue
- body:
POST /smart- body: same as
/play - if already playing, enqueues; otherwise plays immediately
- body: same as
POST /play_now- body:
{"url", "preserve_current"?, "preserve_to"?, "resume_current"?, "reason"?, "title"?, "thumbnail"?} - immediate play with optional preserve-current semantics
- body:
POST /play_temporary- body:
{"url", "resume"?, "resume_mode"?, "timeout_sec"?, "volume_override"?}
- body:
POST /play_temporary/cancelPOST /play_at- body:
{"url", "start_at": epoch_seconds}
- body:
RelayTV supports direct media upload for Android share targets and other local automations that have file bytes instead of a public URL. Upload clients should not send Android content:// URIs to RelayTV and should not weaken the normal URL validators. Send file bytes to the ingest endpoints below, then use the returned RelayTV media URL for playback or queueing.
All ingest endpoints accept multipart/form-data:
file: required uploaded media filetitle: optional display title
Supported uploads are selected by MIME type and/or safe file extension. Current accepted media families include:
- video:
video/mp4,video/webm - audio:
audio/mpeg,audio/mp4,audio/m4a,audio/aac,audio/ogg,audio/opus,audio/wav,audio/flac - Ogg/generic:
application/ogg application/octet-streamwhen the filename has an allowed media extension such as.mp3,.m4a,.aac,.wav,.flac,.ogg,.opus,.mp4,.m4v, or.webm
Upload endpoints:
POST /ingest/media- stores the uploaded file and returns a RelayTV-local media URL
- does not automatically queue or play the item
- use the returned
urlwith existing/enqueueor/play_nowflows
POST /ingest/media/enqueue- stores the uploaded file and appends it to the queue in one call
- response includes
action: "enqueue"andresultfrom queue insertion
POST /ingest/media/play- stores the uploaded file and starts playback in one call
- for eligible
video/mp4andvideo/webm, RelayTV may start playback progressively once enough bytes have arrived and the upload remains healthy - if progressive start is not safe, RelayTV falls back to full-upload-before-play and may show a toast:
Upload still in progress. Waiting for full file for reliable playback.
GET /media/uploads/{upload_id}/{filename}- serves the stored file URL returned by ingest
- returns
410when the upload expired or was removed - returns
404when the filename does not match the stored upload metadata - uses
Cache-Control: private, max-age=60
Typical POST /ingest/media response:
{
"ok": true,
"media_id": "u_0123456789abcdef0123",
"media_path": "/media/uploads/u_0123456789abcdef0123/clip.mp4",
"url": "http://relaytv.local:8787/media/uploads/u_0123456789abcdef0123/clip.mp4",
"item": {
"url": "http://relaytv.local:8787/media/uploads/u_0123456789abcdef0123/clip.mp4",
"provider": "upload",
"title": "Shared Clip",
"mime_type": "video/mp4",
"size_bytes": 123456
},
"cleanup": {
"removed": 0
}
}Typical direct-play response adds:
{
"playback_mode": "progressive",
"fallback_reason": "",
"now_playing": {
"provider": "upload",
"title": "Shared Clip"
}
}Common errors:
400: unsupported media type or empty upload410: returned media URL points to an upload that expired or was removed413: upload exceeds the configured storage size limit500: storage or playback handoff failure
Upload storage defaults:
- root directory:
RELAYTV_UPLOADS_DIR, default/data/uploads - max upload storage size: settings
uploads.max_size_gb, default5 - retention: settings
uploads.retention_hours, default24 - cleanup runs before/after ingest and removes uploads by configured size or retention limit, whichever comes first
Progressive direct-play tuning:
RELAYTV_UPLOAD_PROGRESSIVE_MP4_READY_MB, default24RELAYTV_UPLOAD_PROGRESSIVE_WEBM_READY_MB, default12RELAYTV_UPLOAD_PROGRESSIVE_MAX_STALL_SEC, default2RELAYTV_UPLOAD_PROGRESSIVE_MIN_THROUGHPUT_KBPS, default256
Example upload-only flow:
curl -F "title=Shared Clip" \
-F "file=@clip.mp4;type=video/mp4" \
http://relaytv.local:8787/ingest/mediaExample one-call queue:
curl -F "title=Queued Clip" \
-F "file=@clip.mp4;type=video/mp4" \
http://relaytv.local:8787/ingest/media/enqueueExample one-call play:
curl -F "title=Play Now Clip" \
-F "file=@clip.mp4;type=video/mp4" \
http://relaytv.local:8787/ingest/media/playTransport/control endpoints:
POST /playback/playPOST /playback/togglePOST /pausePOST /resumePOST /toggle_pausePOST /nextPOST /previousPOST /seek- body:
{"sec": number}
- body:
POST /seek_abs- body:
{"sec": number}
- body:
POST /volume- body:
{"set": number}or{"delta": number}
- body:
POST /mute- body: optional
{"set": boolean}
- body: optional
POST /close- close playback but retain resumable session state
POST /stop- stop playback and return to idle visuals while retaining resume metadata
POST /resume_session- resume the retained closed session
POST /resume/clear- clear retained resume state and return to idle
Most playback control responses include compact control-ack fields when available:
request_idack_observedack_reason
Queue endpoints:
POST /enqueuePOST /queue/addPOST /api/queue/addPOST /v1/queue/add- body:
{"url"}
- body:
GET /queuePOST /queue/remove- body:
{"index": int}
- body:
POST /queue/move- body:
{"from_index": int, "to_index": int}
- body:
POST /queue/dedupePOST /clear- clears the queue
History endpoints:
GET /historyPOST /history/play- body:
{"index": int}
- body:
POST /history/clear
Notification entrypoints:
POST /overlayPOST /toastPOST /notify
/toast and /notify are aliases of /overlay.
Overlay request body:
text?duration?position?style?image_url?level?icon?link_url?link_text?
Overlay responses include:
okduration_mspositionstylevisual_runtime_modenotification_strategynotifications_availablenotifications_reasonoverlay_subscribersnotifications_deliverabledelivery_modenative_qt_idle_deprecatednative_qt_idle_statusnative_qt_idle_override_enablednative_qt_toasts_deprecatednative_qt_toasts_statusnative_qt_toasts_override_enabled
POST /overlay returns 503 in headless runtime when notifications are unavailable.
Notification/runtime introspection:
GET /notifications/capabilities- includes native Qt idle/toast deprecation and override-only metadata
Advanced X11 overlay runtime endpoints:
GET /x11/overlayGET /x11/overlay/events- Server-Sent Events stream for X11 overlay clients
POST /x11/overlay/client_state- body:
{"state", "reason"?, "client_event"?, "client_reason"?, "active_toasts"?}
- body:
GET /x11/host_urls
These X11 overlay endpoints remain active for overlay/runtime diagnostics and browser-side overlay clients. They are not the primary native Qt control surface.
GET /status- full server-authoritative runtime state
GET /playback/state- compact fast playback-state endpoint for the web UI
GET /runtime/capabilities- backend/runtime capability snapshot
GET /tv/status- HDMI-CEC / TV control status
GET /devices- discovered device/runtime capability helpers
GET /discovery/status- mDNS discovery status
GET /settings- returns a UI-safe settings view
- secrets are masked; configured-state flags are exposed instead
POST /settings- partial settings update
- request body supports the current
SettingsReqfields, including:device_namevideo_modedrm_connectordrm_modeaudio_devicequality_modequality_capytdlp_formatyoutube_cookies_pathyoutube_use_invidiousyoutube_invidious_basesub_langcec_enabledtv_takeover_enabledtv_pause_on_input_changetv_auto_resume_on_returnvolumeidle_qr_enabledidle_qr_sizeidle_panelsweatheruploadsjellyfin_enabledjellyfin_server_urljellyfin_usernamejellyfin_passwordjellyfin_user_idjellyfin_audio_langjellyfin_sub_langjellyfin_playback_modeapply_now
- supports
apply_nowfor settings that can be applied live - response includes:
okplayingapply_nowapply_performedapply_succeededsettingsnow_playinglive_appliedlive_apply_failedrestart_sensitive_pendingrestart_recommended
Upload settings shape:
{
"uploads": {
"max_size_gb": 5,
"retention_hours": 24
}
}Upload setting bounds:
max_size_gb:0.25to500retention_hours:1to2160
YouTube cookie helpers:
POST /settings/youtube/cookies- body:
{"cookies_text", "filename"?}
- body:
POST /settings/youtube/cookies/clear
Integration status and operator helpers:
GET /integrations/jellyfin/statusPOST /integrations/jellyfin/catalog/cache_clearPOST /integrations/jellyfin/connectPOST /integrations/jellyfin/disconnectPOST /integrations/jellyfin/registerPOST /integrations/jellyfin/commandPOST /integrations/jellyfin/heartbeatGET /integrations/jellyfin/progress_snapshotPOST /integrations/jellyfin/stoppedGET /integrations/jellyfin/stopped_snapshot
Legacy compatibility endpoint:
POST /integrations/jellyfin/push- deprecated legacy Jellyfin plugin ingress
- returns
410 Gone
Native Jellyfin browse/detail endpoints:
GET /jellyfin/homeGET /jellyfin/search- query:
q, optionallimit, optionalrefresh
- query:
GET /jellyfin/moviesGET /jellyfin/tv/seriesGET /jellyfin/tv/series/{series_id}/seasonsGET /jellyfin/tv/series/{series_id}/episodesPOST /jellyfin/tv/series/{series_id}/play_allGET /jellyfin/item/{item_id}GET /jellyfin/item/{item_id}/adjacentGET /jellyfin/audio/optionsPOST /jellyfin/audio/select- body:
{"index": int}
- body:
POST /jellyfin/action- item play command wrapper (
play_now,play_next,play_last,resume)
- item play command wrapper (
- Queue/history/session/settings persistence lives under
/data. - Playback/state endpoints are server-authoritative; the web UI should not invent state locally.
/ui/eventsis the preferred hot-state delivery path for the browser UI, but/statusremains the supported reconnect/bootstrap fallback.- Some control endpoints return
400for invalid user actions such as empty queue or no resumable session. - Some playback-dependent endpoints return
409when active playback is required and unavailable, for example/snapshot. - Existing aliases remain active where noted for backward compatibility.