Skip to content

API routes reference

This is the endpoint-by-endpoint shape of the /api/v1 surface. For auth, tokens, scopes, and the device flow, see API & tokens; for full per-endpoint parameters and schemas, use the interactive API reference.

Paths below are relative to /api/v1, except where stated. These are public route groups, not a list of every browser or operator endpoint. Use your instance’s /api-docs?format=yaml for exact fields and operation stability; the hosted API reference is a vendored snapshot and can lag new features.

user means a valid user credential plus any required scopes, feature flags, object ownership and library/age access. It does not mean every token can call every method. Device JWTs never become administrative tokens. An admin-looking path or a listed scope does not grant access by itself. Operations labeled beta or experimental retain that status even when their neighboring routes are stable.

The catalog, sessions, watch state, profile and sync foundations are stable. Watch Together, device remote commands, requests, task controls, webhooks and many music discovery operations are beta. HLS/audio/image bytes follow their protocol; consume returned hrefs unchanged. Check stability per operation rather than assuming the whole group has one label.

MethodPathAuthStabilityDescription
GET/mediauserstableList library movies/series (type, libraryGroupId, genre, letter, tag, keyword, year, sort, order, paging); include=facets adds counts
GET/media/{id}userstablePolymorphic detail: movie, series, season, episode
DELETE/media/{id}media:adminstablePermanently remove an item; privileged operation
GET/media/{id}?include=useruserstableSame + per-user watch state
GET/media/{id}/ratinguserstableCurrent rating for the item
PUT/DELETE/media/{id}/ratinguserstableSet a 0–10 rating or remove it with DELETE; PUT 0 also removes it
GET/media/{id}/images/{role}publicstableFetch an image by role (see Images)
GET/series/{id}userstableSeries detail with traversal links
GET/series/{id}/seasonsuserstableList seasons
GET/series/{id}/seasons/{n}userstableSeason detail (/seasons/{id} takes a season handle)
GET/series/{id}/seasons/{n}/episodes/{e}userstableOne episode, or 404
GET/series/{id}/watch-markeruserstableResume target: episode, positionMs and state (?from= adds relation)
GET/series/{id}/episodesuserstableEvery episode, paged (limit, offset; season, episode, absolute and specials filters)
GET/series/{id}/next-episodeuserstableNext unwatched episode; ?afterPlayback=<episode UUID> advances past that episode’s whole default file (autoplay)
GET/series/{id}/watch-statususerstableAggregate watch status
GET/seasons/{id}, /episodes/{id}userstableDirect season/episode detail
GET/media/{id}/context, /media/{id}/similaruserstableContext and similar titles
POST/media/batchuserbetaBatch lookup (beta)
GET/people/{id}userstablePerson detail
GET/people/{id}/images/{role}userstablePerson image (TMDB size vocabulary, see Images)

Library listings use /media; /series/{id} is the series detail route. Search and discovery return the item schemas documented for each operation. Keep returned id, type, provider ids and image hrefs instead of deriving handles from titles.

GET /api/v1/{entity}/{id}/images/{role}
GET /api/v1/{entity}/{id}/images/{role}/{index}

entity ∈ media | people | users | libraries role depends on the entity: media artwork includes poster, backdrop, logo, thumb, banner and still; people use profile, users use avatar. index: for entities with multiple backdrops or logos. Descriptor-list routes (GET /media/{id}/images, /people/{id}/images, /users/{id}/images) return available roles. Indexed image routes use the same /{role}/{index} pattern. Item descriptors also use the primary role (square art: music, audiobooks, people, users, stations, libraries, playlists). People have only profile; users have only avatar.

Media images and user avatars are reachable without a bearer credential. People and library images need a signed-in user. Payloads embed image hrefs directly and browsers load them without attaching an Authorization header. Per-item library/age gating still applies whenever a session is present; an ?sig= HMAC query parameter on the same canonical path serves as the alternate credential for gated content requested without one (there is no separate /image/signed/* tree).

Sizing and cache efficiency. ?size= is the canonical, cache-friendly way to request a derivative:

?size=Pixel widthWhere offered
thumb250px (library mosaics 256, avatars 48)poster, thumb, primary, still
medium350pxaccepted, but not offered in descriptor variants
card480pxmost roles
hd1280pxbackdrop
wide1920pxbackdrop
fulloriginal (no resize)all

A response is immutable only when the URL carries v=<revision> and a bounded size. Everything else gets max-age=3600 with an ETag. full is never immutable.

  • ?width=N snaps onto the smallest preset that is at least as wide and within ±20% of it (covering, not nearest), so a request never comes back with fewer pixels than it asked for (e.g. ?width=460 → card, ?width=300 → medium). Widths outside every ±20% band render as custom, non-immutable derivatives, clamped to [1, 3840].
  • ?quality=Q snaps to the nearest canonical tier (50, 70, 85, 95), within ±20%.
  • ?exact=true bypasses both snapping rules; reserve for debugging, not production traffic.
  • ?format= can usually be omitted: the server negotiates AVIF or WebP. All image responses vary on Vary: Accept, Save-Data.

Two entity types don’t follow the preset system: /people/{id}/images/profile accepts thumb, card, full and TMDB sizes (w45, w185, h632, original), and /libraries/{id}/images/{role} takes only ?size=thumb|card|hd|full (256, 512 and 1280 pixels wide, or the generated original).

MethodPathAuthStabilityDescription
GET/searchuserstableCross-type search (q and the documented type filters)
GET/trendinguserstableTrending titles (?window=day|week|month&type=movie|series|all)
GET/recommendationsuserstablePersonalised recs multiplexer (?type=for-you|ai|mood|fulfilled-requests)
POST/recommendations/refreshuserstableExplicit synchronous refresh; send the same filters in a JSON body; returns the first page
POST/recommendations/dismissuserbetaDismiss a recommended item
GET/continue-watchinguserstableIn-progress titles
GET/feeduserstableSDUI home feed (sections wrapping Tier-2 rec endpoints); section titles localized per Accept-Language, see Language negotiation
GET/upcominguserstableUpcoming releases (?view=alerts|episodes|calendar)
POST/upcoming/refreshuserstableExplicit synchronous refresh; send view and filters in a JSON body
GET/keywordsuserbetaKeyword browse (?mode=you-like|explore|curated)
GET/browseuserbetaBrowse destination (beta); library sorting uses /media
GET/browse/genres, /browse/librariesuserstableAvailable genre and library filters
GET/browse/tagsuserbetaTag filters (beta)
GET/search/suggestuserstableSearch suggestions
GET/watchlistuserstableSaved titles
POST/DELETE/watchlistuserstableAdd or remove a title; use the operation’s request body
DELETE/continue-watching/{id}userstableRemove an entry from continue watching

/feed is a thin composition over the same services as the Tier-2 endpoints. Pin to the Tier-2 resource endpoints (/trending, /recommendations, etc.) for stable automation; feed section structure may change between releases. section.id is the stable part of the contract to filter/cache/branch on; section.title is localized display text and may change wording between releases.

MethodPathAuthStabilityDescription
POST/media/{id}/playabilityuserstableProbe before you start: versions, play method per version for your capabilities, tracks, resume position, recommended tracks. Endpoint requires media:read; use a write-capable token because read-only credentials are blocked from POST; creates no session or grant
GET/playback/{id}/fileuserstableInline direct playback (?grant=)
GET/playback/{id}/hlsuserprotocolHLS master playlist (?grant=)
GET/playback/{id}/hls/{path}userprotocolHLS segment (?grant=)
GET/playback/{id}/timelineuserstableChapters, skip segments and trickplay variants for the selected file (ms)
GET/playback/{id}/probe-statususerbetaTranscode probe status
GET/playback/{id}/trickplay/{width}userprotocolTrickplay sprite
GET/playback/{id}/trickplay/{width}/manifestuserstableTrickplay manifest (JSON, sheet hrefs)
GET/playback/{id}/subtitles/{index}userprotocolEmbedded subtitle by absolute stream index
GET/playback/{id}/subtitles/external/{subtitleId}userstableExternal subtitle by id
GET/playback/{id}/subtitles/{index}/rendereduserprotocolBitmap subtitle manifest; image hrefs use /rendered/{image}
GET/playback/{id}/chapter-image/{index}userprotocolChapter artwork; follow the timeline href
GET/playback/grants/{id}user (owner)stableGrant state by its opaque id
POST/playback/grants/{id}/renewuser (owner)stableBody { token }; returns the grant with the extended expiry (never shorter)
DELETE/playback/grants/{id}user (owner)stableRevoke a grant
POST/playback/telemetryuserstableClient telemetry / quality metrics
POST/playback/previewsuserstableEphemeral hover-preview transcode session
POST/playback/previews/{id}/keepaliveuser (owner)stableBodyless preview heartbeat; use the returned reporting descriptor
DELETE/playback/previews/{id}user (owner)stableRelease a preview (idempotent, including unknown or foreign ids)

The removed GET and POST /playback/{id}/info routes answer 410 route_retired for older clients. Use POST /media/{id}/playability to probe and POST /play-sessions to start.

File, manifest, subtitle and image routes return bytes; timeline, probe-status and grant management return JSON. Stream URLs are opaque: take them from the start response (delivery.url and the hrefs inside delivery) and send them back unchanged. Never build, parse or rewrite an HLS URL yourself.

Start requests take library mediaId and stable client deviceId. Probe with the same capabilities you will send to start. Optional capabilities use server defaults; advertise only formats the client can decode. Video bitrate ceilings use maxVideoBitrateBps (bits/second), audio uses bitrateKbps (kilobits/second), and playback/report positions use milliseconds. Use the returned reporting descriptor for method, href and interval.

For music only, purpose: "prefetch" prepares the next track without consuming a stream slot. It needs an admitted ACTIVE/PAUSED session for the same user/device; otherwise 422 prefetch_requires_current_session means use purpose: "play". One unpromoted allocation per device is allowed. It expires after ten minutes or when its current session ends, and real playback promotes it under the usual admission policies.

MethodPathAuthStabilityDescription
GET/media/{id}/downloadmedia:readstableStable original-file attachment; fileId chooses a complete physical file; Range supported
GET/POST/media/{id}/renditionsuserstableStable rendition discovery / preparation, subject to download feature flags
GET/renditions/{renditionId}userstableStable preparation status
GET/renditions/{renditionId}/fileuserprotocolProtocol bytes; use the returned href
GET/music/downloads/candidates, /series/downloads/candidatesuserbetaBeta suggested offline items
POST/music/downloads/rescore, /series/downloads/rescoreuserbetaBeta priority rescoring
POST/offline-playback-sessionsuserexperimentalExperimental batched offline playback records
GET/network/speed-testuserbetaBeta byte payload for client timing
POST/network/speed-test/resultsuserbetaBeta speed-test result upload

Download-only transfers must not create a listening start. Music downloads use Bearer GET /music/tracks/{id}/stream?download=1; audiobook transfer preparation uses the start contract’s audio.download. See Offline downloads for local playback reporting and the feature-specific access rules.

MethodPathAuthStabilityDescription
GET/play-sessionsuser / adminstablePlaying now (?scope=mine|all&itemId=; scope=all requires admin)
POST/play-sessionsuserstableStart playback for video, music and audiobooks (media:write): 201 with { playbackSessionId, mediaId, kind, delivery, grant, reporting }; see Play media from your own client
PUT/play-sessions/{id}user (owner)stableReport progress (heartbeat). status: COMPLETED ends the session (204)
POST/play-sessions/{id}/beaconuser (owner)stablesendBeacon alias of PUT; same report body
GET/play-sessions/{id}/eventsuser (owner)stableSSE events for a session
DELETE/play-sessions/{id}owner or adminstableTerminate a session and free its stream slot. Anyone else gets 404
POST/play-sessions/replayuserbetaReplay finished plays that had no live session (offline music)
GET/me/resume-handoffuserstableCheck whether another device continued this user’s playback; call while paused, with mediaId, positionMs, deviceId, and idleMs
GET/watch-state/{id}userstable{ watched, positionMs, resumeAnchor, favorite, rating }
PATCH/watch-state/{id}userstableSet watched / favorite (partial); rate through /media/{id}/rating

positionMs uses milliseconds, and so does /playback/{id}/timeline (chapters, skip segments, trickplay). Follow the schema for each time field.

MethodPathAuthStabilityDescription
GET/sync/libraryuserstableStable library snapshot bootstrap
GET/sync/changesuserstableStable incremental library changes; keep opaque cursors
GET/sync/user-stateuserstableStable user-state snapshot/deltas
POST/libraries/ingestadmin PAT/service token with tasks:runbetaBeta delta ingest; send Idempotency-Key
GET/libraries/{id}/images, /libraries/{id}/images/{role}, /libraries/{id}/images/{role}/{index}userstableLibrary image descriptors and bytes

See Library sync for cursor lifecycles. Library-path configuration, full scan triggers, queue control and provider configuration belong to operator tooling; their internal routes are not public stable integration promises.

MethodPathAuthStabilityDescription
GET/taskstasks:readbetaList scheduled task definitions
GET/tasks/{taskId}tasks:readbetaOne task definition
POST/tasks/{taskId}/runtasks:runbetaEnqueue a run
GET/PATCH/tasks/{taskId}/statetasks:adminbetaPause/resume schedule, manage circuit breaker (partial update)
GET/tasks/{taskId}/backlogtasks:readbetaBacklog item listing
GET/PUT/tasks/{taskId}/settingstasks:adminbetaPer-task settings
GET/PUT/DELETE/tasks/{taskId}/triggerstasks:adminbetaInterval/daily/weekly/startup triggers (PUT body { triggers: [...] }, 1–8, replaces all; DELETE resets to the task’s default schedule)
GET/tasks/{taskId}/failurestasks:readbetaPer-item failure log
POST/tasks/{taskId}/failures/{itemId}/retrytasks:runbetaRetry one failed item
GET/runsruns:readbetaRun history ({items,page})
GET/runs/{id}runs:readbetaOne run + result
GET/runs/{id}/logsruns:readbetaLog lines for one run
POST/runs/{id}/canceltasks:runbetaCancel one run: graceful, { "force": true } hard-kills. 404 unknown run, 409 already finished, alreadyCancelling: true when a graceful cancel was already in progress
POST/runs/cancel?byTask={id}tasks:runbetaCancel all runs for a task: graceful unless { "force": true }. Returns cancelled, orphaned (already finished or gone) and escalated (hard-kills scheduled)
GET/tasks/{taskId}/failures/{itemId}/historytasks:readbetaFailure-attempt history

These published task/run controls are beta operator APIs. The public schema does not include queue/scheduler control or a live-run SSE operation. Cancellation here uses POST /runs/{id}/cancel with { "force": true } for escalation; playback cancellation uses DELETE /play-sessions/{id}.

Scope note: task/ops routes (/tasks, /runs, /queues) accept only pat_/svc_ or an admin session, not device-flow JWTs. See Scope tree: the coarse read alias does not expand to tasks:read.

MethodPathAuthStabilityDescription
GET/integrations/homepageintegrations:read service token or adminbetaBeta dashboard feed
GET/integrations/upcomingintegrations:read service tokenbetaBeta upcoming feed
GET/integrations/watch-historyintegrations:read service tokenbetaBeta watch-history feed

Watch-history and upcoming reject other credential kinds, including admin PATs and admin service tokens; Homepage also accepts an admin principal. The exclusive integrations:read service grant reads only these feeds and requires its owner to remain an administrator. Keep the token server-side. Provider setup and integration-key management are operator tooling, not additional public routes under these prefixes.

MethodPathAuthStabilityDescription
POST/diagnostics/eventsuserstableBatch of client diagnostics: crash | hang | js_error | api_failures | playback_incident
PATCH/diagnostics/events/resolveuserstableMark a playback attempt recovered
POST/diagnostics/bundlesuserstableUpload a gzipped client log bundle

See Client diagnostics for what each kind contains, redaction, retention, and how to review or disable it.

MethodPathAuthStabilityDescription
GET/eventsuserstableGlobal per-user SSE channel
GET/notificationsuserstableDurable notification inbox
GET/notifications/{id}userstableOne owned notification
GET/notifications/unread-countuserstableFast unread badge count
POST/notifications/read-alluserstableMark all as read
POST/notifications/{id}/readuserstableMark one notification as read
DELETE/notifications/{id}userstableDelete one notification
GET/me/series-followsuserstableSeries you follow for new-episode notifications
PUT/me/series-follows/{seriesId}userstableFollow a series (idempotent)
DELETE/me/series-follows/{seriesId}userstableUnfollow a series (idempotent)

/me/series-follows is a separate, explicit per-series subscription for third-party clients. It’s distinct from the end-user “New episode” toggle in the web UI’s notification settings, which controls the same event for the account as a whole.

Notification forwarding is beta: GET/PUT /me/notification-forwarding and GET/POST/DELETE /me/notification-webhook. Forwarded/push artwork and localized payloads use issued links (GET /notifications/{id}/image, /push-image, /push-content/{locale}); their endpoint-specific signed credentials do not authorize resource API calls.

MethodPathAuthStabilityDescription
GET/serverpublic / userstableDiscovery: name, release, sign-in methods. With a bearer token it adds capabilities, build and caller (id, isAdmin, permissions)
GET/server/noticeuserstableThe active maintenance notice, or { "notice": null }
MethodPathAuthStabilityDescription
GET/watch-togetheruserbetaCurrent user’s { party } wrapper; party may be null
GET/watch-together/partiesuserbetaList host-listed parties; admins may include unlisted parties
POST/watch-together/partiesuserbetaCreate a watch party (beta)
GET/watch-together/{code}userbetaGet a party by code
POST/watch-together/{code}/eventsuserbetaSend an event (join, seek, reaction, etc.)
GET/watch-together/{code}/eventsuserbetaSSE downstream (beta)

End a party by sending the disband event; guests leave with leave. There is no DELETE /watch-together/{code} route.

Events use a discriminated { clientId, type, payload } envelope on both the POST upstream and the SSE downstream. The closed type enum falls into three groups:

  • Broadcasts (fire-and-forget, no server confirmation): seek, sync, position, reaction, effect
  • Authoritative (server-confirmed state changes): join, leave, navigate, version, visibility, transfer-host, disband, activity
  • Barriers (playback sync primitives): ready, stall, stall-ready, heartbeat
MethodPathAuthStabilityDescription
GET/devicesuserstableList user devices (?scope=mine)
POST/devices/{id}/heartbeatdevice JWTstableSelf-device presence keepalive; PAT/service tokens have no device-session claim
PATCH/DELETE/devices/{id}user (owner)stableRename or revoke a device
POST/devices/revoke-alluserstableRevoke own devices
PUT/DELETE/devices/{id}/push-tokendevice JWTstableRegister/remove push destination for this device
GET/POST/devices/{id}/commandsuserbetaRead pending commands / send a command (beta)
POST/devices/{id}/commands/{commandId}/resulttarget devicebetaAcknowledge a command result (beta)
GET/devices/{id}/commands/{commandId}userbetaRead a command and its status
DELETE/devices/{id}/commands/{commandId}userbetaCancel a command that is not applied yet
GET/PUT/devices/{id}/stateuserbetaWhat the device plays now and what it can do
GET/POST/devices/grantsuserbetaList or grant household remote-control access (beta)
DELETE/devices/grants/{username}userbetaRemove access by username (beta)
GET/devices/controllableuserbetaDevices others allowed you to control

The commands and their arguments are described in Device remote control.

MethodPathAuthStabilityDescription
GET/PATCH/me/privacyuserstableYour privacy preferences (hideFromOthers, shareListeningActivity, shareAudiobookActivity, read-only listeningCircleConfigured); device tokens and PATs accepted. PATCH is partial: omitted fields keep their stored values; an empty update answers 422 validation_failed
GET/meuserstableOwn profile: {id, username, isAdmin, isAgeRestricted, permissions, groups}
GET/PATCH/me/playback-preferencesuserstableClient playback settings; partial update. metadataLanguage/metadataTitleLanguage override Accept-Language, see Language negotiation
GET/POST/me/patsuserstableOwn PATs; DELETE /me/pats/{id} revokes (204)
GET/me/linksuserstableLinked third-party accounts
GET/DELETE/me/links/{provider}userstableOne linked account: status, and unlink
POST/me/links/{provider}/authorizeuserstableInitiate OAuth account linking (returns an authUrl)
GET/users/{id}users:adminstableRead another user’s profile
GET/users/{id}/images/avatarpublicstableUser avatar (shown in social contexts without a session); sizes thumb, card, full = 48, 96, 200 px
PUT/DELETE/users/{id}/images/avataruser (self or admin)stableUpload / remove an avatar (multipart)

The public schema also exposes user/access administration. These are privileged operations, not capabilities of a device token:

MethodPathAuthStabilityDescription
GET/POST/usersusers:adminstableList/create users; method-specific scope and admin ceiling
PATCH/users/{id}users:adminstableUpdate account state; admin ceiling for privilege changes
GET/DELETE/users/{id}/identitiesusers:adminstableInspect/remove federated identities
POST/users/{id}/passwordusers:adminstableReset password
GET/POST/users/{id}/patstokens:adminstableList/mint for a user; minting ceiling applies
DELETE/users/{id}/pats/{tokenId}tokens:adminstableRevoke an authorized PAT
GET/users/{id}/devicesusers:adminstableList devices
DELETE/users/{id}/devices/{deviceSessionId}users:adminstableRevoke a device
POST/users/{id}/devices/revoke-allusers:adminstableRevoke all devices
GET/POST/access/groupsaccess:adminbetaBeta list/create access groups
GET/PATCH/DELETE/access/groups/{id}access:adminbetaBeta read/update/delete group
GET/access/groups/{id}/membersaccess:adminbetaBeta membership listing
PUT/DELETE/access/groups/{id}/members/{userId}access:adminbetaBeta bodyless membership change

The beta debug-report family (GET /admin/debug-report, /health, /runs/{runId}, /cluster/nodes/{instanceId} and /client-diagnostics/{fingerprint} below that prefix) requires admin or the exclusive, admin-minted diagnostics:read service grant. It exposes operator data; keep such a token on a trusted server. Other /admin routes are internal and intentionally omitted.

GET /server (caller.permissions) and GET /me (permissions) return the same object:

FieldMeaning
canRequestSubmit media requests
canReviewWrite community reviews
canAnnotateMediaPlayer annotations
canWatchLiveTvLive TV. Without it every Live TV endpoint answers 403 forbidden
canSeeAllPlaybackThe web app’s Active streams view shows everyone’s playback. GET /play-sessions?scope=all still requires an administrator

Playback also requires the appropriate token scope, an accessible source and admission under stream/IP/transcode policies. /server capabilities describe server features; caller.permissions describes account permissions, not the full token grant.

MethodPathAuthStabilityDescription
GET/POST/playlistsuserstableCollection: list or create
GET/PATCH/DELETE/playlists/{id}userstableOne playlist
GET/POST/playlists/{id}/itemsuserstableList or add playlist items
DELETE/playlists/{id}/items/{entryId}userstableRemove one item
PUT/playlists/{id}/items/{entryId}/watcheduserstableUpdate an item’s watched status
POST/playlists/{id}/items/bulk-deleteuserstableBulk removal of authorized entries
PUT/playlists/{id}/items/orderuserstableReorder playlist items
GET/POST/playlists/{id}/membersuserstableCollaboration members
PUT/DELETE/playlists/{id}/members/{userId}userstableManage one member (by user id)
GET/listsuserstableCurated lists
GET/lists/{id}userstableOne curated list

Playlist membership paths use stable user UUIDs. Follow each resource’s path schema: remote-control /devices/grants/{username} is an explicit username-based exception.

MethodPathAuthStabilityDescription
GET/requestsuserbetaOwn requests (?scope=mine|all&status=)
POST/requestsuserbetaSubmit a request
GET/requests/{id}userbetaOne request
GET/requests/quotauserbetaPer-user quota
POST/requests/{id}/dismissaluserbetaDismiss a request notification (204)
POST/requests/dismissalsuserbetaDismiss a title that has no request: { externalIds, mediaType }

Quota is per-user, per-rolling-period, with separate movie and series buckets. Exceeding quota → 422 quota_exceeded with the bucket in details.

POST /requests/seasons and POST /requests/status provide beta requestability/status lookups. Submission still depends on the caller’s canRequest account permission.

MethodPathAuthStabilityDescription
GET/me/statsuserbetaOwn watch stats (?period=7d|30d|90d|year:YYYY|all)
MethodPathAuthStabilityDescription
GET/livetv/stationsuserstableStable stations with number, images, shown feedId and nowNext
GET/livetv/stations/{stationId}userstableOne station with nowNext (current and next airing)
GET/livetv/stations/{stationId}/feedsuserbetaFeed choices; pass a feed’s id as feedId when starting playback
GET/livetv/stations/{stationId}/programsuserstableAirings for a window (required UTC from / to, at most 24 hours) and nowNext
GET/livetv/stations/{stationId}/current-programuserstablenowNext and asOf
GET/livetv/guideuserstableRequired from / to (at most 24 hours); stationIds (max 200) or limit + cursor paging; stations[{stationId, airings, nowNext}]
GET/livetv/programs/searchuserbetaProgram search; channels[].id identifies a station
GET/livetv/availabilityuserstableFeed-keyed availability; join using the station’s feedId
GET/PUT/livetv/favoritesuserstableFavorites use stationIds; PUT { stationId, favorite }
PUT/livetv/favorites/orderuserstableReorder favorites with { stationIds }
GET/PATCH/livetv/simple-modeuserbetaViewer settings use stationIds, startStationId, clearStartStation
POST/livetv/playback-sessionsuserstableStart: { stationId, feedId?, quality?, maxBitrateBps?, hevc? } → 201 with delivery.url, quality details, reporting keepalive descriptor, and grant
GET/livetv/playback-sessions/{playbackSessionId}user (owner)stablestate (active / ended), endReason, and failure (code, retryable, retryAfterMs, at) when the channel failed
POST/livetv/playback-sessions/{playbackSessionId}/keepaliveuser (owner)stableCall reporting.href every reporting.intervalMs; 204 or 410 with reason: evicted|superseded|expired|left|failed
DELETE/livetv/playback-sessions/{playbackSessionId}user (owner)stableStop the session (204, idempotent). POST .../leave is the same for sendBeacon
POST/livetv/playback-sessions/{playbackSessionId}/leaveuser (owner)stableSame cleanup for sendBeacon
HLSdelivery.urluserprotocolOpaque manifest URL returned by the start response; follow its child URLs and do not construct protocol paths

POST /livetv/simple-mode/verify-pin verifies the viewer setting’s PIN (beta). The HLS protocol path /livetv/channels/{id}/stream/{path} remains a byte endpoint; that does not restore the retired channel-based browsing/start API.

Station and playback-session routes replace the old channel viewer routes without aliases. For the complete lifecycle, feed selection, quality options and eviction handling, see Live TV playback.

MethodPathAuthStabilityDescription
GET/audiobooksuserstableStable book listing
GET/audiobooks/authors, /audiobooks/narrators, /audiobooks/seriesuserstableStable browse collections
GET/audiobooks/authors/{id}, /audiobooks/narrators/{id}, /audiobooks/series/{id}userstableStable browse entity detail
GET/audiobooks/authors/{id}/books, /audiobooks/narrators/{id}/books, /audiobooks/series/{id}/booksuserstableStable filtered book lists
GET/audiobooks/facetsuserbetaBeta browse filters
GET/PUT/media/{id}/listening-sharinguserstableStable per-book sharing preference
GET/audiobooks/{mediaId}/files/{trackIndex}, /audiobooks/{mediaId}/remux/{trackIndex}user/grantprotocolProtocol direct/remux audio; consume granted hrefs
GET/audiobook/{mediaId}/hls/{token}/{sessionId}/{file}user/grantprotocolProtocol HLS; opaque issued URL

The singular /audiobook/{mediaId}/file/{ino} and /remux/{ino} routes are deprecated retirement responses, not alternate playback routes. Start with POST /play-sessions and follow its delivery. Book progress and track offsets use the book-wide clock; see Audiobook playback.

MethodPathAuthStabilityDescription
GET/music/tracks, /music/albums, /music/artists, /music/genresuserstableList endpoints
GET/music/tracks/{id}userstableTrack detail
GET/music/tracks/{id}/lyricsuserstableLyrics, when available
GET/music/tracks/{id}/similaruserbetaSimilar tracks; ?basis=auto|metadata|sonic, { items, source, page }
GET/music/tracks/{id}/streamuserprotocolAudio playback (see Music streaming and transcoding)
PUT/DELETE/me/music/tracks/{id}/feedbackuserbetaExplicit like or dislike ({ "feedback": "liked" } or "disliked"). Skips and completions never change it
POST/music/recommendationsuserbetaWhat to play next: { seed?, context: { currentTrackId?, history?, skipped?, exclude?, upcoming?, crossfade?, startedBy? } } → { next?, alternatives, source }; a mood seed alone is a cold start
GET/music/albums/{id}/similar, /music/artists/{id}/similaruserbetaSonic similarity, requires sonicEmbeddings (/server → capabilities.music.sonicSimilarity)
GET/music/pathuserbetaSonic bridge between two tracks, requires sonicEmbeddings; rate-limited
GET/music/sonic-mixes, /music/sonic-mixes/{id}/tracksuserbetaSonic mixes, requires musicClusters (capabilities.music.sonicMixes)
GET/me/music/taste-stations, /me/music/taste-stations/{id}/tracksuserbetaTaste stations; the id may be stored, 410 once its clustering expired
GET/music/mood-recommendationsuserbetaRequires the sonicQueue feature; rate-limited
GET/POST/me/music/pinned-genresuserbetaPinned genres
DELETE/me/music/pinned-genres/{genre}userbetaUnpin (percent-encode any / in the genre name)
GET/me/music/recently-played, /me/music/top-tracks, /me/music/top-artistsuserbetaListener rows for a music home; { items, page: { limit, truncated } }
GET/music/radio/stationsuserbetaInternet-radio station list
GET/music/radio/stations/{id}/streamuserprotocolRadio stream
GET/music/radio/stations/{id}/now-playinguserbetaCurrent track on a station

Additional music operations:

MethodPathAuthStabilityDescription
GET/musicuserstableStable server-built music destination
GET/music/albums/{id}, /music/artists/{id}userstableStable album/artist detail
GET/music/lyrics-search, /music/text-searchuserstableStable semantic discovery; requires the corresponding server features
PUT/music/tracks/{id}/lyricsadminstableStable lyric upload/replacement
GET/music/alpha-index, /music/moodsuserbetaBeta collection index / selectable moods
POST/music/quick-picksuserbetaBeta quick picks
GET/music/sections/because-you-listened, /music/sections/genre/{name}, /music/sections/mood-shelf, /music/sections/new-for-you, /music/sections/smart-mixesuserbetaBeta music-home sections
GET/music/sonic-fingerprint, /music/sonic-mixes/indexuserbetaBeta sonic discovery
GET/music/sonic-mixes/{id}/shuffleuserbetaProtocol shuffled audio
GET/music/images/collage, /music/radio/stations/{id}/images/{role}userbetaBeta artwork

GET /music/tracks/{id}/stream negotiates direct play or audio conversion. Query format accepts mp3 | opus | aac | m4a | raw. bitrateKbps explicitly requests a re-encode at a supported quality rung; maxBitrateKbps is a ceiling and converts only when needed. startPositionSeconds is a stream seek in seconds, unlike the millisecond positions used by session start/reporting. A different codec/container, unsupported capability, channel limit, explicit bitrate or stream seek can require conversion. It must be enabled server-side.

format=raw requests the original bytes. It rejects conflicting seeks, bitrate or capability requirements; it does not silently bypass them. For normal listening, start with POST /play-sessions and follow the returned audio URL, rather than rewriting its query. See Music playback and Codec support for format and quality details. Capacity failures can return 503; honour Retry-After when supplied.

MethodPathAuthStabilityDescription
GET/POST/media/{id}/comments, /people/{id}/comments, /playlists/{id}/commentsuserbetaRead or add comments; account permissions and ownership apply
DELETE/media/{id}/comments/{commentId}, /people/{id}/comments/{commentId}, /playlists/{id}/comments/{commentId}userbetaDelete an authorized comment
GET/PUT/DELETE/media/{id}/reactions, /people/{id}/reactions, /playlists/{id}/reactionsuserbetaRead, set or remove a reaction
GET/media/{id}/viewersuserbetaPrivacy-filtered viewers
POST/playlists/{id}/inviteuserbetaAccept or decline an invitation
GET/playlists/{id}/playback-context, /playlists/{id}/progress, /playlists/{id}/suggestionsuserbetaCollaborative playback context, progress and suggestions
PUT/playlists/{id}/view-stateuserbetaCaller’s collapsed series/season groups
POST/DELETE/for-you/dismissuserbetaDismiss or restore a home suggestion
POST/for-you/less-like-thisuserbetaFeedback on suggestions
MethodPathAuthStabilityDescription
GET/webhookswebhooks:writebetaList webhooks
POST/webhookswebhooks:writebetaCreate webhook
PUT/DELETE/webhooks/{id}webhooks:writebetaManage one webhook
POST/webhooks/{id}/testwebhooks:writebetaSend a test payload
GET/webhooks/eventswebhooks:writebetaAvailable webhook event kinds
MethodPathAuthStabilityDescription
POST/auth/logoutdevice JWTstableRevoke current device session and refresh credentials
GET/POST/service-tokenstokens:adminbetaList/create service tokens (beta; creation also enforces the minting ceiling)
GET/DELETE/service-tokens/{id}tokens:adminbetaRead/revoke service tokens (beta)

Internal config, plugin-management and forward-auth diagnostics paths are omitted from the public contract. Use the operator UI for these tasks.

Experimental Top Shelf previews use GET /media/{id}/topshelf-preview and its issued /file/{variant} URL. They are separate from ordinary hover-preview sessions.

These sit outside the /api/v1 resource tree and carry no version prefix. They’re an operational ABI (load balancers, Kubernetes, Prometheus pin literal paths) and are exempt from the problem+json and version-prefix conventions used elsewhere.

MethodPathAuthStabilityDescription
GET/api/healthpublicoperationalLiveness: always 200, no dependency checks
GET/api/health/readypublic / adminoperationalReadiness: 200/503; per-component verdict for an admin caller
GET/metricspublic (opt-in) or metrics:readoperationalPrometheus text exposition; also gated by LUMASTRA_METRICS_BEARER_TOKEN

GET /api/v1/events is the one global per-user push channel. Connect with a standard EventSource:

const es = new EventSource('/api/v1/events', { withCredentials: true });
// Clients that cannot send a session cookie read the stream with fetch() and an
// Authorization header. Query-string tokens are not accepted.

On connect, the server immediately sends a connected bootstrap frame:

event: connected
data: {"serverTime":1766258109000}

Hub frames carry a numeric SSE id:. On reconnect, pass the last SSE ID in Last-Event-ID (not the notification UUID). Replay comes from a shared 500-entry, one-hour Valkey ring, filtered per recipient. It is bounded catch-up, not a durable sync log. Use /notifications and sync endpoints to recover after a longer gap. Hub data: is an envelope { targetUserId, targetDeviceSessionId?, replay?, kind, data, notificationId? }; the inner data is the event payload. connected, admin and watch-state signals carry their payload directly and have no replay ID.

Signal and notification SSE frame names are advertised at GET /server in capabilities.events.kinds[].name; connected is the additional bootstrap frame:

Event typeTrigger
connectedBootstrap frame with server time
adminOperator message or maintenance state; discriminate on data.type
device.commandA command for this device (sent to the target device only)
device.command_cancelledA pending command for this target device was cancelled
device.command_statusA command you sent changed status
device.stateA device of your account reported new state
session_changedSession/watch-state signal
watch_state_set_changedWatch-state-set signal
library.new_episodeNew episode for a followed series
request.availableRequested title is available
admin.broadcastDurable operator notification

The admin payload types are admin_message, maintenance_notice and maintenance_cleared. These signals are live-only. The last three notification types also write to the durable inbox (/notifications).

Device command/cancel/state/status frames are live-only. Reconnect a receiver by reading /devices/{id}/commands for pending commands. Ignore unknown event names. Handle 429 connection limits and 503 capacity failures before opening the stream; retry with backoff and the advertised reconnectMaxMs. Native clients should use a Bearer-authenticated streaming request. Browser EventSource cannot set that header.

For admin_message, honour optional playSessionId/deviceId targeting on the client; an unknown severity should display as info. The server re-sends current maintenance state on connect; /server/notice also recovers that state.

The scoped SSE channels (watch-together/{code}/events, play-sessions/{id}/events) remain separate; they have a resource lifecycle the global channel does not.

GET /api/v1/server (also HEAD) is the one discovery call, with Cache-Control: private, no-cache and an ETag. Without a credential it answers the anonymous tier: id, name, release, apiVersions, branding, auth (deviceFlow, passwordLogin) and links. With a valid user bearer token it adds build, caller and capabilities. There is no separate /server/capabilities route. Invalid credentials receive the anonymous tier; a service token also receives the anonymous tier. Use a user JWT/PAT to discover caller permissions. Treat absent optional capabilities as unsupported.

Selected fields from an authenticated response (feature values vary per server):

{
"id": "550e8400-e29b-41d4-a716-446655440000", "name": "Home", "release": "0.1.0", "apiVersions": ["v1"],
"caller": { "id": "…", "isAdmin": false, "permissions": { "canRequest": true, "canReview": true,
"canAnnotateMedia": true, "canWatchLiveTv": true, "canSeeAllPlayback": false } },
"capabilities": {
"playback": { "direct": true, "remux": true, "transcode": true,
"transcoder": { "engine": "ffmpeg", "videoCodecs": ["h264", "hevc"],
"audioCodecs": ["aac", "ac3"], "containers": ["mp4", "ts"],
"maxResolution": "4K", "hdrPassthrough": true,
"dolbyVisionPassthrough": false },
"hlsVariants": ["720p", "1080p"], "maxBitrateKbps": null },
"liveTv": { "available": true },
"music": { "available": true, "clustering": false, "sonicSimilarity": false, "sonicMixes": false },
"audiobooks": { "available": false },
"features": { "watchTogether": true, "videoPreviews": true, "topShelfPreviews": false,
"mediaDownload": false, "downloadRenditions": false },
"events": { "href": "/api/v1/events", "reconnectMaxMs": 30000,
"kinds": [ { "name": "admin", "class": "signal", "durable": false, "available": true } ] },
"metadata": { "primary": "en", "additional": ["de"] }
}
}

capabilities.features is the engine-can-do matrix (booleans in camelCase), distinct from the playback transcoder capabilities. music.available is true when the server has at least one enabled music library; music.clustering is a separate recommendation feature and says nothing about whether music exists. metadata.primary and .additional list the languages enabled for metadata translation, see Language negotiation below. GET /api/v1/server/notice returns the active maintenance notice or { "notice": null }; call it on launch and on foreground.

/api/v1 clients negotiate language per request with the standard Accept-Language header. There is no ?language= query parameter and no Content-Language response header.

Two negotiations run independently, against different candidate sets:

Candidate setApplies to
MetadataAdmin-enabled set: the server’s primary language plus any additional languages enabledtitle / overview / tagline on media, wherever the translation overlay runs
UI chromeThe UI’s supported locales: en, de only/feed section titles

Metadata precedence is a chain of signals, each consulted only if the one above it produced no signal:

  1. Stored preference: metadataLanguage / metadataTitleLanguage fields on PATCH /me/playback-preferences
  2. The language-switcher cookie set by the web UI
  3. The Accept-Language request header
  4. The server’s primary language

A stored preference or cookie that is present but unsatisfiable (names a language the server doesn’t have enabled) ends the chain at the primary language. It does not fall through to the next signal. For example, the cookie is set to de, only fr is enabled server-side, and the request’s Accept-Language says fr. The response is still the primary language, not French, because the cookie already supplied (and exhausted) a signal.

UI chrome (server-built section titles) resolves independently: explicit PARAGLIDE_LOCALE cookie first, then Accept-Language against en/de, then English. An unsupported but present cookie stops at English. Stored metadata preferences do not affect this UI-language choice.

Use GET /server to discover which languages are enabled before assuming a preference will resolve (see the metadata block above).

Send Accept-Language with the device language on every request, for example Accept-Language: de-DE, de;q=0.9, en;q=0.5. To let a user pick a metadata language that differs from the device, store it once with PATCH /api/v1/me/playback-preferences (metadataLanguage for overviews and taglines, metadataTitleLanguage for titles); the stored preference then wins over the header on every client. Operations whose responses carry translated titles or overviews list the Accept-Language header parameter in the API reference.

Full library scans and library-path configuration are operator actions. Their internal routes are not part of the public schema. Use the server’s administration UI to configure paths and scan them. The public beta integration below accepts known deltas with an admin PAT or service token holding tasks:run; a device JWT is rejected with 403.

Push a known set of file changes without walking the library. Useful when your storage layer can emit change events:

Terminal window
curl -X POST https://lumastra.example.com/api/v1/libraries/ingest \
-H "Authorization: Bearer $ADMIN_PAT" \
-H "Idempotency-Key: $EVENT_ID" \
-H "Content-Type: application/json" \
-d '{
"libraryPathId": "550e8400-e29b-41d4-a716-446655440000",
"changes": [
{ "path": "movies/Nosferatu (1922)/Nosferatu.mkv", "op": "upsert" },
{ "path": "movies/OldMovie/old.mkv", "op": "remove" }
]
}'

Send an Idempotency-Key header and reuse it after a timeout or retryable failure. The key deduplicates run scheduling. Delta payloads are queued before scheduling, so a retry can enqueue the changes again; do not treat the key as an exactly-once guarantee.

FieldTypeDescription
libraryPathIdstring (UUID, optional)Required when entries use path; omit when entries use sourceUri.
changesarray (1–5000)The delta. Max 5 000 entries per request.
changes[].pathstringPath relative to the library root. Mutually exclusive with sourceUri.
changes[].sourceUristringFull object-store URI. Mutually exclusive with path. Requires source_prefix on the target path.
changes[].op"upsert" | "remove"upsert = added/modified; remove = deleted.

The response includes accepted counts:

{
"runId": "run_example",
"deduped": false,
"statusUrl": "/api/v1/runs/run_example",
"accepted": { "upsert": 1, "remove": 1 }
}

Obtain the configured library-path UUID from the operator UI. Do not substitute a media UUID. Ingest replies 202 when enqueued or 200 when deduplicated; accepted counts may be absent on a deduplicated reply.

  • Ingest is async: poll statusUrl. GET /api/v1/runs/{runId} returns the terminal state and per-file result. The run read requires runs:read; an admin token covers both gates. Fire-and-forget is fine when you don’t need outcomes.
  • Check the result as well as the run state. File ingest can report partial outcomes. Inspect the returned result and errors rather than assuming that a terminal run means every requested file succeeded.
  • Object-storage timing. An upsert reads the file on the worker. If you fire the delta the instant you upload, the object may not be visible yet → soft error. Either tolerate it and let the periodic scan backstop catch it, or add a short delay. remove does not need to read the removed file.
  • Chunking. Split deltas larger than 5 000 entries across multiple requests. Keep a rename’s upsert and remove in the same request so they are processed in the same ingest batch; this is not a transactional filesystem rename.
  • Backpressure. Under a burst the queue can fill → 503 dependency_unavailable with Retry-After. Retry with the same Idempotency-Key.
  • One library per request. All entries must resolve to one libraryPathId; mixed entries are rejected with validation_failed (current handler uses 422).
  • /api/v1/* is stateless at the web tier: no sticky sessions needed.