Skip to content

Media server accounts

HTTP routes and behavior for media server accounts.

GET /api/media-servers # user: granted Jellyfin/Emby/Plex/Audiobookshelf instances + own account state
# [{ instance_id, service_type, name, kind: account | invite, public_address, listening_apps?: { ios, android },
# account: null | { username, disabled, pending, administrator, verified, manage_access, access_sync_pending },
# auto_link_suppressed, existing_account }] (true only when the server confirmed an unlinked account named like the
# user while they have none there: the guide then leads with signing in to link it; false says
# nothing about absence, since an unreachable server reads the same)
GET /api/media-servers/watch # user: where a title can be watched ?media_type=movie|tv&tmdb_id=&tvdb_id=&year=&title=
# -> [{ instance_id, service_type, name, state: found | missing | unreachable | unverified, url?, fallback_url? }]
# granted servers with a sign-in address; Jellyfin/Emby require a linked account, Plex also offers a generic fallback
# lookup uses the linked account's live access and provider IDs; url only when found (Plex: hosted Plex Web)
# fallback_url is Plex's sign-in address when no exact link is verified; [] when no server was eligible
# 400 for bad media_type or no id; kids: 404 for a blocked title, 503 when content limits cannot be checked; no-store
GET /api/media-servers/listen # user: available audiobook links ?instance_id=&foreign_book_id=
# explicit authorized Chaptarr instance + native book identity; identifiers resolved server-side from current audio records
# -> [{ instance_id, service_type: audiobookshelf, name, state: found | unverified | unreachable,
# items: [{ id, title, library_name, narrators, url }], fallback_url?, listening_apps: { ios, android } }]
# exact ISBN/ASIN matches under live linked-account library/tag/explicit-content access; separate item IDs remain separate
# fallback_url is the configured public address when no exact match is verified, including unlinked accounts
# [] when no Audiobookshelf server is eligible or no audio files are available in Chaptarr
# 400 invalid parameters, 403 missing source access, 503 source unavailable/changed; no-store
GET /api/me/video-apps # own service-keyed { plex: { ios }, jellyfin: { ios }, emby: { ios } } preferences; no-store
PUT /api/me/video-apps # replace own video preferences; omitted services/empty values inherit instance defaults
GET /api/me/listening-apps # authenticated caller's personal { ios, android } listening preferences (admins too); no-store
PUT /api/me/listening-apps # replace own { ios, android }; empty inherits the instance default, browser overrides it
# ios: "" | browser | audiobookshelf | shelfplayer; android: "" | browser | audiobookshelf | theshelf
# 400 unsupported values or malformed body, 503 storage unavailable; no target user ID
POST /api/media-servers/{instanceID}/account # user: create own account { password } (Jellyfin, Emby, Audiobookshelf) or ask for the invite { email } (Plex)
# -> 201 { username, public_address, pending, administrator, manage_access }
# 403 (one body for unknown and ungranted ids), 409 { code: account_exists | name_taken },
# 400 { code: invalid_name | invalid_email | wrong_kind }, 502 upstream failure (the half-created account is rolled back)
POST /api/media-servers/{instanceID}/account/link # user: link an account that is already theirs { username, password } (Jellyfin, Emby, Audiobookshelf); the server
# checks them with the media server once and keeps neither -> 201 { username, public_address, administrator, manage_access: false }
# 400 { code: bad_credentials | account_refused | wrong_kind } (never a 401, which the app reads as its own session being revoked),
# 409 { code: account_exists | remote_already_linked }, 403 as above, 502 upstream failure
POST /api/media-servers/plex/sign-in/begin # user: start a plex.tv sign-in with their own Plex account -> { pin_id, code, url }
POST /api/media-servers/plex/sign-in/check # user: poll it { pin_id } -> { linked: false } until approved, then
# { linked: true, username, email, invite_state: sent | adopted | failed | claimed | unlinked | "" } (the same answer on every later poll;
# claimed = the account belongs to another user's row here);
# 404 { code: pin_expired } for an unknown, expired, or someone else's pin
GET /api/admin/instances/{id}/media-access # admin: ABS default libraries, granted user IDs and per-user library policies
PUT /api/admin/instances/{id}/media-access # admin: atomically save ABS defaults, grants and explicitly edited policies
GET /api/admin/media-servers/accounts # admin: every linked account (user, instance name/type, remote id + name, created_by_cantinarr, manage_access, granted, access_sync_pending, disabled, administrator, verified, pending; live username when verified)
GET /api/admin/media-servers/{instanceID}/users # admin: accounts on that server ({ id, name, is_administrator, is_disabled, pending }) for the link picker;
# on Plex the rows are shares and pending invites, keyed by email, plus the server's owner as an administrator
PUT /api/admin/users/{userID}/media-servers/{instanceID}/account # admin: link an existing account { remote_user_id, manage_access?: false }; administrators link too, and are never changed
POST /api/admin/media-servers/{instanceID}/import # admin: turn picked accounts into Cantinarr users { remote_user_ids, server_url, manage_access?: false }
# -> { results: [{ remote_user_id, remote_username, user_id, username, created, linked, link, origin_source, error }] }
# per account: a user of that exact name (found, or created with a connect link built on the External Address, else server_url),
# the instance grant, and the account linked (created_by_cantinarr=false); a Plex share also records its email on a user
# who has none. Rows are independent; error is not_found | already_linked | user_failed | user_has_account | link_failed.
# 400 empty list, more than 200 ids, no address; 404 unknown instance; 502 the server could not list its accounts
PATCH /api/admin/users/{userID}/media-servers/{instanceID}/account/management # admin (users:manage): { manage_access: bool }, required; returns account row
# true verifies the live account, rejects administrators (409), then applies the current grant; 502 if verification fails
# false stops management and cancels pending changes without provider traffic; 404 when no link exists
DELETE /api/admin/users/{userID}/media-servers/{instanceID}/account # admin: unlink (Cantinarr's row only; the server account and the grant stay)

Audiobookshelf’s admin media-access endpoint accepts default_library_ids, user_ids, and policies keyed by Cantinarr user ID. Each edited policy is { "mode": "default|all|selected", "library_ids": [] }; selected requires at least one ID, while default and all require an empty list. Omitted policies retain their choices, including on revocation. GET also reports sync_pending and manages_libraries for each saved policy. Defaults, choices and grants commit together before account creation can consume a new grant. Library choices live in user_media_library_policies, keyed by user and instance independently of account creation. Pending writes bind to the remote account ID, retry through account maintenance, and are canceled when management stops or the account is unlinked. Existing linked accounts keep their ABS library permissions until an explicit library edit on a managed account; remote administrators are protected. A default change updates only accounts following that default. All exact Listen links still check the linked person’s live ABS permissions.

The user routes are self-scoped and rate-limited like /api/auth/plex-email, except the sign-in poll: the app polls it every few seconds, and a pin can only be polled by the user who began it. The instance grant (instance-grants / grant-users) controls Cantinarr eligibility. New accounts and Plex shares created by Cantinarr are managed; existing admin links, imports, self-links, and adopted Plex shares (including Plex-login signup) default to linked only. Optional admin manage_access: true adopts access management without changing existing library selections. Managed accounts follow grants: revocation disables Jellyfin/Emby/Audiobookshelf accounts or removes Plex shares; regrant enables accounts or recreates Plex shares with the instance’s current library selection. Linked-only accounts are untouched by grants, library updates, email changes, and user deletion. Stopping management preserves the link and remote state and clears pending work. Unlinking preserves the grant and remote account and writes per-user/per-instance suppression, preventing automatic Plex sign-in, email hooks, and maintenance from recreating the link; explicit per-server linking/inviting clears it. Administrator and owner accounts are always protected. Upgrade backfills existing mappings as managed once, preserving their established behavior; new mappings default to unmanaged.

/api/config advertises media_account_management: true. New apps hide management controls and use legacy behavior descriptions when this capability is absent; older apps omit the optional link/import field and receive the safer default. Admin account rows separate the local granted value, manage_access mode, access_sync_pending intent, and live remote disabled/administrator/verified state. The switch uses granted, not a remote disable stamp. verified is a live read with a short timeout, so an unreachable server answers with Cantinarr’s stored row and verified: false rather than pretending the account is gone; a server that confirms the account no longer exists answers account: null; on Plex pending marks an invite nobody has accepted yet; administrator marks an account Cantinarr records and never changes (a server administrator, or on Plex the server’s owner). /api/config carries plex_access_requestable: true whenever a Plex instance exists, so a user with no Plex grant can still share their email from the guide and have the admins told.

View the maintained source for this page.