Appearance
plex
38 endpoints at a glance
| Method | Path | Summary |
|---|---|---|
POST | /plex/admin/libraries/{section_id}/sync | Admin Sync One |
POST | /plex/admin/libraries/refresh | Admin Refresh Libraries |
GET | /plex/admin/linked-users | Admin Linked Users |
DELETE | /plex/admin/linked-users/{username} | Admin Unlink User |
GET | /plex/admin/non-flac-albums | Admin Non Flac Albums |
POST | /plex/admin/playlists/sync-all | Admin Sync All Playlists |
GET | /plex/admin/server | Admin Server Info |
GET | /plex/admin/stats | Admin Stats |
POST | /plex/admin/sync-all | Admin Sync All |
GET | /plex/album/{canonical_album_id}/cover | Get Album Cover |
GET | /plex/libraries | List Libraries |
GET | /plex/libraries/{section_id}/albums | List Albums |
GET | /plex/libraries/{section_id}/artists | List Artists |
GET | /plex/libraries/{section_id}/episodes | List Episodes |
GET | /plex/libraries/{section_id}/movies | List Movies |
GET | /plex/libraries/{section_id}/seasons | List Seasons |
GET | /plex/libraries/{section_id}/shows | List Shows |
GET | /plex/libraries/{section_id}/tracks | List Tracks |
GET | /plex/library/search | Library Search |
DELETE | /plex/me | Unlink |
GET | /plex/me | Get My Link |
POST | /plex/me/auth/cancel | Cancel Pin |
POST | /plex/me/auth/finish | Finish Pin |
POST | /plex/me/auth/start | Start Pin |
POST | /plex/me/favorites-playlist/sync | Sync My Favorites Playlist |
GET | /plex/me/home-users | List Home Users |
POST | /plex/me/home-users/{home_user_id}/provision | Provision Home User |
GET | /plex/me/playlists | List My Playlists |
GET | /plex/me/playlists/{playlist_id} | Get My Playlist |
GET | /plex/me/playlists/{playlist_id}/cover | Get My Playlist Cover |
POST | /plex/me/playlists/{playlist_id}/refresh | Refresh My Playlist |
POST | /plex/me/playlists/sync | Sync My Playlists |
GET | /plex/play/album-tracks/{canonical_album_id} | List Plex Playable Tracks |
GET | /plex/play/album-tracks/{canonical_album_id}/details | List Plex Album Track Details |
GET | /plex/play/track/{canonical_id} | Play Plex Track |
POST | /plex/play/track/{canonical_id}/timeline | Post Timeline |
GET | /plex/play/track/{canonical_id}/transcode.mp3 | Transcode Plex Track |
GET | /plex/track/{canonical_id}/details | Get Plex Track Details |
plex
Get My Link
GET
/plex/me
Public link state for the caller.
link is null when not linked. has_pending_pin is true when
a PIN flow is mid-air — the SPA uses it to know whether to resume
polling on settings page refresh.
Parameters
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Unlink
Start Pin
Finish Pin
POST
/plex/me/auth/finish
Poll for completion of the caller's pending PIN.
One poll per call. SPA loops every ~2s until status != 'pending'.
Returns one of:
{status: 'pending'} — keep polling
{status: 'completed', link} — done; the user is linked
{status: 'expired', error} — PIN expired before completion
{status: 'no_pending_pin'} — nothing to poll (start one first)
Parameters
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Cancel Pin
List Home Users
GET
/plex/me/home-users
List the caller's Plex Home users (excluding the caller).
412 when the caller hasn't linked Plex. 401 if the stored token
was revoked plex-side since last use; SPA should suggest a
re-link in that case.
Parameters
Responses
Successful Response
application/json
JSON { "additionalProperties": "string" }
[
]
Provision Home User
POST
/plex/me/home-users/{home_user_id}/provision
Create an app account for a Plex Home user.
Status codes:
- 201 — created. Body is the new user's public profile.
- 400 — Plex returned a 4xx on switch (wrong PIN, no such Home
user). Body'sdetailcarries the plex.tv message. - 401 — owner's stored Plex token was rejected.
- 409 — requested
new_usernamealready exists. - 412 — owner hasn't linked Plex yet.
- 502 — Plex.tv transport / unexpected upstream error.
Parameters
Path Parameters
home_user_id*
Type
Requiredinteger
Request Body
application/json
JSON "new_username": "string", "new_password": "string", "pin": "string"
{
}
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
List My Playlists
GET
/plex/me/playlists
List the caller's synced playlists.
Returns plain catalog rows (no items embedded in the listing — the
detail endpoint serves those). Optional playlist_type=audio
/video /photo filter for the SPA's tab strip.
Parameters
Query Parameters
playlist_type
Responses
Successful Response
application/json
JSON { "additionalProperties": "string" }
[
]
Sync My Playlists
POST
/plex/me/playlists/sync
Fire-and-forget sync of the caller's Plex playlists.
Requires the caller to have linked Plex via the PIN flow first;
returns 412 Precondition Failed otherwise so the SPA can surface
a "Link your Plex account first" CTA.
Parameters
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Sync My Favorites Playlist
POST
/plex/me/favorites-playlist/sync
Reconcile the caller's favorite tracks against their per-user
Plex playlist ("<username> - Favorites").
Single-track add/remove already happens automatically inside the
favorite/unfavorite endpoints. This route is the manual catch-up
path: useful right after enabling the feature (to backfill
existing favorites), and as a "Sync now" button on Settings.
Side-effect of a successful first call: flips
preferences.plex_favorites_sync_enabled on if it isn't
already, since the user actively asked for it. The toggle in
Settings just calls this endpoint.
force_recreate=true deletes any existing playlist and rebuilds
from the current favorites list. Use when the playlist is
corrupted manually on Plex side.
412 if the user hasn't linked Plex.
Parameters
Query Parameters
force_recreate
Type
boolean
Default
falseResponses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Get My Playlist Cover
GET
/plex/me/playlists/{playlist_id}/cover
Stream the playlist's composite cover image from Plex.
The composite is Plex-side artwork (4 album covers stitched
together for audio playlists, item-art collage for video). Its URL
is on the playlist row but it needs an X-Plex-Token to fetch
— we can't put that in the browser (CORS + token leak), so we
proxy the same way the audio streaming endpoint does.
Returns 404 when the playlist doesn't have a composite_url
(empty playlists, some smart-playlist variants). The SPA falls
back to a typed emoji placeholder.
Ownership: caller must own the playlist (same scoping as the
detail endpoint). Cover image goes through unauthenticated by
the browser's image fetch, so this gate matters.
Route ordering matters: declared above
/me/playlists/{playlist_id:path} because that catch-all
would otherwise match playlist_id='<id>/cover'.
Parameters
Path Parameters
playlist_id*
Type
Requiredstring
Responses
Successful Response
application/json
JSON
[
]
Get My Playlist
GET
/plex/me/playlists/{playlist_id}
Single playlist with embedded items + per-item canonical resolution.
For each audio item we look up
track_external_refs._id == 'plex::<server_id>::<rating_key>'
in one $in query and attach canonical_track_id to the
matching items. The SPA uses that to render source pills and wire
inline playback to /plex/play/track/<canonical_id>.
playlist_id is the compound key <server_id>::<rating_key>,
which contains a : — declared as :path so FastAPI doesn't
truncate.
Parameters
Path Parameters
playlist_id*
Type
Requiredstring
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Refresh My Playlist
POST
/plex/me/playlists/{playlist_id}/refresh
Re-fetch a single playlist's items from Plex.
Smart playlists are Plex-side filters; their contents can change
without the playlist row's updatedAt advancing. The detail
view auto-calls this on mount so the user sees current state.
Non-smart playlists also work here (just a refetch + overwrite).
Parameters
Path Parameters
playlist_id*
Type
Requiredstring
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
List Libraries
List Artists
List Albums
List Tracks
Library Search
GET
/plex/library/search
Substring search across the user's synced Plex music library.
Powers the "Add Plex link" modal on canonical detail views.
Active items only (soft-deleted Plex docs are excluded — the
canonical layer wouldn't merge from them anyway).
offset skips that many matches for the picker's "Load more".
Parameters
Query Parameters
q*
Type
Requiredstring
kind
Type
string
Default
"album"limit
Type
integer
Default
10offset
Type
integer
Default
0Responses
Successful Response
application/json
JSON { "additionalProperties": "string" }
[
]
List Movies
List Shows
List Seasons
List Episodes
Get Plex Track Details
GET
/plex/track/{canonical_id}/details
Project the Plex-side metadata for a canonical track for display.
Used by TrackDetailView to render the "🎵 Plex (full) · FLAC 16/44.1
· 23.4 MB" one-liner + the expandable "Plex details" block (plays,
last played, added, star rating).
Returns 404 if the canonical doesn't have a (live) Plex link. The SPA
suppresses the details block in that case. Resolves the freshest live
plex_tracks row (see :func:_resolve_live_plex_track_doc) so a
track whose file was replaced shows the new file's details rather
than a stale, soft-deleted row.
Parameters
Path Parameters
canonical_id*
Type
Requiredstring
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
List Plex Album Track Details
GET
/plex/play/album-tracks/{canonical_album_id}/details
Return {canonical_track_id: PlexTrackDetails} for every track
on this album that's Plex-linked.
Batch version of /plex/track/{canonical_id}/details — avoids
the N+1 problem when the SPA needs the quality one-liner for
every row of an album tracklist. Three queries total regardless
of tracklist size:
media_trackswithalbum_id+sources.plex— gets
the canonical-track set.track_external_refswith$inon those canonical_ids- source=plex — gets the compound Plex external_ids.
plex_trackswith$inon those external_ids — fetches
the raw payloads.
Parameters
Path Parameters
canonical_album_id*
Type
Requiredstring
Responses
Successful Response
application/json
JSON "additionalProperties": { "additionalProperties": "string" }
{
}
List Plex Playable Tracks
GET
/plex/play/album-tracks/{canonical_album_id}
Return Plex-playable canonical tracks on this album, keyed three ways.
The album-detail tracklist iterates Deezer track rows (that's where
titles + 30s previews come from), but Plex play needs the canonical
ULID. Three lookup paths, tried in order by the SPA, so a row can
still resolve when the easier matchers don't apply:
by_deezer_id—{deezer_track_id: canonical_track_id}.
The fast path: stamped via the denormaliseddeezer_idfield
on the canonical track. Only fires for canonicals that already
have a Deezer ref.by_title_folded—{title_folded: canonical_track_id}.
Fallback for canonicals minted from a non-Deezer source (Plex /
MB / LastFM) wheredeezer_idis empty. The SPA folds the
Deezer row's title with the same rules backend uses to compute
title_foldedand looks it up here.by_title_match—{match_key: canonical_track_id}. Like
by_title_foldedbut with trailing source annotations removed
("(Live)", "- 2011 Remaster", "(feat. …)"), which is what a
Deezer tracklist carries and a Plex file tag usually does not.
Keys claimed by two canonicals are dropped rather than guessed —
see :func:api.canonical.match_key.build_match_map.by_position—{"<disc>::<track>": canonical_track_id}.
Last-ditch fallback for manually-linked Plex tracks whose
merged title diverges from Deezer's (Plex pulls titles from
the audio file's tag, which often differs in capitalisation,
bracketed annotations, etc.). On an album, disc + track number
uniquely identify a song — so if the canonical and the Deezer
row agree on those, that's a confident match. Disc defaults
to 1 when missing (single-disc albums never set the field).
Single indexed query for all three maps; the SPA picks whichever
key resolves per row.
Parameters
Path Parameters
canonical_album_id*
Type
Requiredstring
Responses
Successful Response
application/json
JSON "additionalProperties": { "additionalProperties": "string" }
{
}
Get Album Cover
GET
/plex/album/{canonical_album_id}/cover
Stream a Plex-album's thumb so the SPA can render it directly.
Plex stores album thumbs at /library/metadata/<ratingKey>/thumb
behind a token-authenticated GET. The path is relative to the Plex
server's host:port and never directly usable from a browser. The
album merger therefore omits Plex thumbs from the canonical's
cover_url field — but for libraries where Plex is the only
source, that leaves the SPA cover-less.
This proxy fills the gap: SPA hits /plex/album/<canonical_id> /cover whenever track.album.cover_url is null and
track.album.sources.plex is true. We resolve the canonical
album → Plex external_ref → ratingKey, then stream the bytes
back. Admin token is fine (same auth model as the audio + playlist
proxies); per-user token isn't needed for thumb reads.
404 when the album isn't Plex-linked; 503 if the server isn't
configured. Failures are explicit so the SPA can fall back to a
placeholder cleanly.
Parameters
Path Parameters
canonical_album_id*
Type
Requiredstring
Responses
Successful Response
application/json
JSON
[
]
Play Plex Track
GET
/plex/play/track/{canonical_id}
Direct-play proxy for a canonical track linked to Plex.
Returns the bytes Plex sends back from /library/parts/<partId>.
The original Range header is forwarded so the browser can seek.
Parameters
Path Parameters
canonical_id*
Type
Requiredstring
Responses
Successful Response
application/json
JSON
[
]
Post Timeline
POST
/plex/play/track/{canonical_id}/timeline
Report playback state for a Plex track on behalf of the user.
This is how Plex's LastFM scrobbler actually gets triggered.
The bytes-only streaming proxy doesn't generate any play state —
so Plex's server-side LastFM agent (and play history, last
played at, play count) never updates for SPA listens. The
/:/scrobble toggle bumps playcount but doesn't reliably
trigger the LastFM agent; the timeline events real Plex clients
send do.
Sequence the SPA fires per track:
- On
playing(first audio frame):state=playing, time_ms=0 - Every ~10 s while playing:
state=playing, time_ms=current - On
pause/ navigation away:state=paused, time_ms=current - On
endedor user stop:state=stopped, time_ms=current
Plex's LastFM agent consumes those state transitions exactly the
way the actual LastFM scrobbler does — fires when the track was
played >50% or for 4+ minutes, whichever first.
Per-user token required: scrobbles are tied to the Plex account
that owns the token, so the server token wouldn't reach the
caller's LastFM. 412 if the user hasn't completed the Plex
OAuth pin flow.
Fire-and-forget at the HTTP boundary — Plex errors are logged
but never bubble to the SPA so playback isn't disrupted.
Parameters
Path Parameters
canonical_id*
Type
Requiredstring
Request Body
application/json
JSON "state": "string", "time_ms": 0, "duration_ms": 0
{
}
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Transcode Plex Track
GET
/plex/play/track/{canonical_id}/transcode.mp3
Transcode-to-MP3 fallback for direct-play codec compat.
Use when the source is FLAC / OGG / something the browser can't
play natively. Plex spins up a transcode session and streams an
MP3. Seek support depends on Plex's transcoder; in practice it's
less reliable than direct play, so the SPA only falls here on a
direct-play error event.
Parameters
Path Parameters
canonical_id*
Type
Requiredstring
Responses
Successful Response
application/json
JSON
[
]
Admin Server Info
GET
/plex/admin/server
Live /identity ping against the global server.
Used by the admin panel's Server card. Returns the unwrapped
MediaContainer (machineIdentifier, friendlyName,
version, platform, etc.) on success.
Parameters
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Admin Stats
GET
/plex/admin/stats
Aggregate Plex usage stats for the admin panel.
entities is the active count (excludes soft-deleted items);
entities_deleted separately surfaces the tombstoned items so
admins can see "your TV library shrank by N episodes since last
sync" at a glance. The historical records remain queryable via the
per-entity endpoints with include_deleted=true.
Parameters
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Admin Non Flac Albums
GET
/plex/admin/non-flac-albums
Plex albums that aren't fully FLAC — replace candidates.
Any album with ≥1 non-FLAC track, worst-affected first. Each row
links to the canonical album page when one exists. Read-only over the
local Plex mirror (no Plex API calls).
Parameters
Query Parameters
limit
Type
integer
Default
1000Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Admin Linked Users
Admin Unlink User
Admin Refresh Libraries
Admin Sync One
POST
/plex/admin/libraries/{section_id}/sync
Fire-and-forget per-library sync.
Music libraries with thousands of tracks routinely exceed the
SPA's 15s HTTP timeout, so the work runs on the event loop and
the SPA polls /admin/task-history/{task_id} for progress.
quick=true (music only): sync artists + albums fully but fetch
tracks only for new/changed albums — seconds on a stable library.
Parameters
Path Parameters
section_id*
Type
Requiredstring
Query Parameters
quick
Type
boolean
Default
falseResponses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Admin Sync All
POST
/plex/admin/sync-all
Fire-and-forget version of the cron job.
Multi-library sync across a non-trivial Plex server takes
minutes. The SPA polls /admin/task-history/{task_id}.
quick=true forwards to each library (music: new/changed-album
tracks only).
Parameters
Query Parameters
quick
Type
boolean
Default
falseResponses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}
Admin Sync All Playlists
POST
/plex/admin/playlists/sync-all
Fire-and-forget sync of every linked user's Plex playlists.
Distinct from /admin/sync-all (which syncs the global library
via the admin token). Playlists are per-user, so this iterates
over every media-user with a stored plex_user_token and runs
sync_user_playlists for each. Per-user failures are isolated.
Parameters
Responses
Successful Response
application/json
JSON "additionalProperties": "string"
{
}