Repository navigation
API Reference
Every endpoint Moonbase exposes on the server. This page is for developers and for admins debugging an integration. Normal use of Moonfin never needs it.
Paths are absolute and served by both the Jellyfin and Emby plugins unless noted.
Auth column:
- Yes requires any valid Jellyfin or Emby session.
- Admin requires an elevated (administrator) session.
- Session uses a stored Seerr session rather than the platform token.
- No is anonymous (used for web assets, discovery, and the inbound Seerr webhook, which has its own shared secret).
Some areas are gated by config: the Settings and public Themes endpoints return 503 when settings sync is disabled, and the Games endpoints return empty results or 404 when the game library feature is turned off.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Ping |
Yes | Plugin status: version, whether sync is enabled, Seerr/MDBList/TMDB availability, admin defaults, and feature flags so older clients can hide the features they lack. An optional profile query (tv, desktop or mobile) returns defaultSettings with that device's admin defaults layered over the Global ones. messagesSupported is on both servers. recommendationsSupported follows the Use Moonfin Recommends for similar items switch. clientLogSupported is on Emby, Silo and Plex and follows Enable client log upload. socketRelay is Jellyfin only and follows SyncPlay relay for Roku. arrQueueAvailable says whether the download queue can be read: Seerr turned on with its URL set, or on Plex, a Sonarr or Radarr set on the admin page. pinSupported, tmdbThumbsSupported, tmdbDetailsSupported, tmdbPersonLinksSupported, tmdbReviewsSupported and extendedCompleteShowingsSupported are on both servers, incompleteRecordingsSupported follows Detect incomplete recordings, and smartSearchEnabled follows Enable smart search
|
| GET | /Moonfin/Defaults |
Yes | The admin-configured default settings profile |
| GET | /Moonfin/Settings |
Yes | The current user's full settings envelope |
| POST | /Moonfin/Settings |
Yes | Save the current user's settings (merge or replace via body) |
| DELETE | /Moonfin/Settings |
Yes | Delete all of the current user's settings |
| HEAD | /Moonfin/Settings |
Yes | Check whether the current user has stored settings (200 or 404) |
| GET | /Moonfin/Settings/Stream |
Yes | Server-sent events stream of settings changes, theme changes, and admin messages. On Emby it answers { "supported": false, "transport": "websocket" } and the same events ride the session websocket instead. Moonbase for Plex holds the stream open too, and only sends calendar reminders on it |
| GET, POST | /Moonfin/Settings/{userId} |
Admin | Get or save another user's settings |
| GET | /Moonfin/Settings/Resolved/{profile} |
Yes | The resolved flat values for a profile (global, desktop, mobile, tv) |
| POST, DELETE | /Moonfin/Settings/Profile/{profile} |
Yes | Save or delete one device profile's overrides (global can't be deleted) |
| GET, POST |
/Moonfin/Settings/detailsScreenBlur and /detailsScreenBlur/{profile}
|
Yes | Get or save the details-screen backdrop blur |
| GET, POST |
/Moonfin/Settings/detailsScreenOpacity and /detailsScreenOpacity/{profile}
|
Yes | Get or save the details-screen backdrop opacity |
See Settings Sync for the envelope shape and the full list of synced settings.
A user's sign-in PIN, stored on the server so it follows them to every device. A PIN is four digits. Only a salted PBKDF2-SHA256 hash is kept, and the PIN is never sent back. Moonbase for Plex serves these routes too. Moonbase for Silo doesn't, since Silo keeps PINs on its profiles, so they answer 404 there and the apps use Silo's own.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Pin |
Yes | Whether the calling user has a PIN |
| PUT | /Moonfin/Pin |
Yes | Set the calling user's PIN. Body { "pin", "currentPin" }, where currentPin is required when a PIN already exists, and a wrong one is a 403 that counts as a guess |
| DELETE | /Moonfin/Pin |
Yes | Remove the calling user's PIN. Takes the same body, with currentPin
|
| POST | /Moonfin/Pin/Verify |
Yes | Check a PIN. Body { "pin" }. Answers 404 when the user has no PIN |
The first five wrong guesses are free. After that each one makes the user wait 30 seconds longer, up to 15 minutes, and while they wait every guess, even the right one, gets a 429 with Retry-After and retryAfterSeconds. The count survives a restart, and a correct PIN or an admin setting a new one clears it.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /Moonfin/Admin/ApplyDefaults |
Admin | Push admin defaults onto users' stored settings. Body { "overwrite", "userIds", "profiles", "keys" }. userIds null means everyone, profiles empty means all four, keys null means every set default, otherwise only those settings by JSON name. overwrite resets each user to the defaults alone and can't take keys. Returns usersAffected and orphansDeleted
|
| POST | /Moonfin/Admin/CopySettings |
Admin | Copy one user's saved profiles onto others. Body { "fromUserId", "toUserIds", "profiles", "keys", "replace" }. replace makes each profile an exact copy and can't take keys. API keys and hidden items are never copied. Answers 404 when the source has no settings |
| POST | /Moonfin/Admin/PushDefaults |
Admin | Older route. Applies every profile's defaults to all users (overwrite=true resets everyone, otherwise merges) |
| POST | /Moonfin/Admin/PushDefaults/{userId} |
Admin | Older route. Applies every profile's defaults to a single user |
| POST | /Moonfin/Broadcast |
Admin | Send an announcement to all users, over the SSE stream for connected clients and as a push to registered devices |
| GET | /Moonfin/Libraries/CheckWriteAccess |
Admin | Test whether the server can write metadata to each library, and report failing paths |
| GET | /Moonfin/Admin/Pins |
Admin | Every user with whether they have a PIN, administrators first, for the Accounts tab |
| PUT, DELETE | /Moonfin/Users/{userId}/Pin |
Admin | Set or remove one user's PIN. Setting a PIN replaces the old one without asking for it and clears the wrong-guess count, which is how a forgotten PIN is reset |
| GET | /Moonfin/Admin/ConfigBackup |
Admin | Whether the plugin settings look like they were reset while a backup with real settings survives: { "looksReset": bool, "backupPath": "..." }. The config page uses it to offer a restore |
| POST | /Moonfin/Admin/RestoreConfig |
Admin | Copy the backup over the live plugin settings. Returns 404 when there's no backup. Restart the server afterwards, since it only reads the file at startup |
Admin messages are a stored, targeted successor to the one-shot broadcast: a message is saved on the server, filtered to the audience it was meant for, and delivered to connected clients over the SSE stream (as a messagesChanged event) and as a push to registered devices. Visibility is decided on the server, so a message meant for one user is never sent to another.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Messages |
Yes | The messages the calling user should see right now, already filtered to their audience and any start or end date. Returns 503 when settings sync is disabled |
| GET | /Moonfin/Admin/Messages |
Admin | Every stored message, including scheduled and expired ones, for the admin panel |
| POST | /Moonfin/Admin/Messages |
Admin | Create a message, or replace one in place when the ID already exists. Emits a messagesChanged event and, for a new interrupting message, a push |
| POST | /Moonfin/Admin/Messages/Order |
Admin | Reorder the stored messages to match the given list of IDs. The app shows them in this order, so this is what the up and down arrows drive |
| DELETE | /Moonfin/Admin/Messages/{messageId} |
Admin | Delete one message by ID |
A message carries a title, a Markdown body, a colour, a delivery mode (an unread badge on the menu button, or the window opening on its own), an audience (everyone, a set of users, or admins only), optional start and end dates, and an optional link button. Interrupting messages push to devices that don't have the app open, while inbox messages wait for the user to open it. The old /Moonfin/Broadcast still works for a quick one-off announcement that isn't stored.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/MediaBar |
Yes | The resolved media bar items for the current user (profile query, default global) |
| GET | /Moonfin/Genres |
Yes | All genre names and ids across the user's movie and series libraries. Moonbase for Silo and Plex give each genre the id the app itself uses on that server, and answer 502 on a Silo build that can't list library items |
| GET | /Moonfin/Themes |
Yes | List uploaded custom themes |
| GET | /Moonfin/Themes/{themeId} |
Yes | Get one custom theme by id |
| GET | /Moonfin/Admin/Themes |
Admin | Theme metadata index for the admin panel |
| POST | /Moonfin/Admin/Themes |
Admin | Upload or replace a theme payload |
| DELETE | /Moonfin/Admin/Themes/{themeId} |
Admin | Delete a theme |
See Themes for the editor and upload flow.
Jellyfin, Emby and Moonbase for Plex. Emby and Plex can't use Moonfin's scoring for their own similar items, so only Moonfin apps get it there, through this route. On Plex the items come back as Plex's own metadata.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Items/{id}/Similar |
Yes | Similar items for a movie or series, scored by Moonfin on the server. Optional limit (default 20, capped at 200), excludeItemIds, and userId (defaults to the caller). Results are filtered to the libraries the user can see and come back as { "Items": [...], "TotalRecordCount": n }, in the same card shape as the media bar with the user's UserData. Returns 404 for an unknown item, and an empty list when Use Moonfin Recommends for similar items is off |
This endpoint works on every supported Jellyfin version. On Jellyfin 12 Moonbase also plugs into Jellyfin's own similar items system, so the stock /Items/{id}/Similar returns Moonfin's scoring to every client, with Jellyfin's own providers only filling empty slots. Add bypass=moonfin (or engine=jellyfin) to that stock request to get Jellyfin's own results for one call.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Seasonal/Row |
Yes | The seasonal Home row for the calling user: holiday movies they own, plus Seerr suggestions. Optional country is the viewer's ISO alpha-2 code, ZZ for someone who picked Other, or empty to fall back to the server's. Optional allYear=true also answers outside the holiday windows, with one of the user's shown holidays picked for the day, whatever its country. Answers { "holiday", "country", "items", "suggestions" }, with holiday null when no holiday window is open and allYear isn't set. items are the same cards the browse endpoints return, and suggestions use the custom row item shape with a rating attached. On a failure it answers an empty row rather than an error |
Filler, canon and recap markers come from the cache the Moonfin Anime Markers Sync task fills. Subbed and Dubbed are read from the audio tracks in the files.
Moonbase for Plex serves these routes too, with Plex ratingKeys as ids, and runs the sync every night at 4:00. It also has GET /Moonfin/AnimeMarkers/Search (admin), which finds a series by name for the Preview tool, since its admin page can't search Plex.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/AnimeMarkers/Series |
Yes | Markers for every episode of one series (seriesId). Reads the cache only and never goes to the internet, so clients can call it on every episode list. Answers enabled: false when both marker features are off, and pending: true for a matched series that hasn't been fetched yet. Returns 400 for a missing or malformed id and 404 for an unknown series |
| GET | /Moonfin/AnimeMarkers/Items |
Yes | Subbed or Dubbed for up to 200 comma-separated item ids (ids). Series and folders are skipped, since their episodes and seasons come from the Series endpoint. Answers enabled: false when audio markers are off |
| GET | /Moonfin/AnimeMarkers/Diagnostics |
Admin | Match counts for the admin page: how many series matched a show, how many are waiting on a fetch, and which matched nothing |
| GET | /Moonfin/AnimeMarkers/Preview |
Admin | One series' episode list with its markers (seriesId), for the Preview a series tool |
| POST | /Moonfin/AnimeMarkers/Refresh |
Admin | Fetch one series right away instead of waiting for the nightly task (seriesId) |
| GET | /Moonfin/AnimeMarkers/Log |
Admin | The tail of the marker debug log (lines, default 200, up to 2000) |
| POST | /Moonfin/AnimeMarkers/ClearLog |
Admin | Empty the marker debug log |
| POST | /Moonfin/AnimeMarkers/ClearCache |
Admin | Drop every cached show so the next sync fetches from scratch |
A Series response looks like this:
{
"enabled": true,
"matched": true,
"slug": "naruto-shippuden",
"title": "Naruto Shippuden",
"pending": false,
"recapKnown": true,
"episodes": {
"<episodeId>": { "kind": "Filler", "recap": false, "audio": "Subbed" }
},
"seasons": {
"<seasonId>": { "audio": "Dubbed" }
},
"placement": "below"
}kind is Filler, Mixed, MangaCanon, AnimeCanon, or null when there's no marker or that pill is turned off. audio is Subbed, Dubbed, or SubbedAndDubbed when dual audio is shown separately. placement is the admin's pill placement (below, beside, or thumbnail). The Items endpoint answers { "enabled": true, "items": { "<itemId>": { "audio": "Subbed" } }, "placement": "below" }.
On Jellyfin the episode and season keys are item ids without hyphens. On Emby they're Emby's numeric item ids, the Items endpoint keys each answer by the id you sent, and fields with no value are left out instead of being sent as null.
Incomplete recording detection and re-recording. The settings are server-wide and set on the admin page's Live TV tab.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/LiveTv/Recordings/Incomplete |
Live TV access | The incomplete state of each recording listed in itemIds, or of every incomplete recording when none are listed. Recordings that aren't incomplete are left out |
| POST | /Moonfin/LiveTv/Recordings/{itemId}/Dismiss |
Live TV management | Keep as Complete. Clears the recording's incomplete state for everyone and removes its DVRIncompleteRecording tag |
| POST | /Moonfin/LiveTv/CompleteShowing |
Live TV management | Ask for a later showing of a program's episode or movie to be recorded. Repeating the call for the same one joins the same request, so it's safe to retry |
| GET | /Moonfin/LiveTv/CompleteShowing/Options |
Live TV management | For a program that's already airing (programId), whether a later showing can be searched for and the one found: name, description, start, channel and alreadyScheduled. Only channels the caller can see are searched |
| POST | /Moonfin/LiveTv/CompleteShowing/Suggestion |
Live TV management | Record the later showing that Options suggested. Answers 204, or 409 when that airing changed, was canceled or can't be seen |
| GET | /Moonfin/LiveTv/Settings/IncompleteRecordings |
Live TV access | The server-wide settings |
| POST | /Moonfin/LiveTv/Settings/IncompleteRecordings |
Admin | Change the server-wide settings |
Answers 404 while Enable smart search is off. See Smart Search.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/SmartSearch |
Yes | Library items and Seerr titles matching a plain sentence (query), up to limit of each (20 by default). Answers Ready, Items in the same trimmed card shape as the other Moonfin browse endpoints, and Seerr in the shape of Seerr's own search results. Items follows the user's library access and parental limit, and Seerr is empty unless Seerr is turned on. Before the first index is built it answers Ready: false with empty lists and starts the build. debug=true adds each result's score and how the query was read |
These use the calling user's TMDB key, or the server-wide one, and answer 404 without either.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Tmdb/Thumb |
Yes | A 302 to the TMDB backdrop with the title on it for a title the server doesn't hold (type is movie or tv, plus tmdbId, or imdbId when there's no TMDB id). It prefers the backdrop in the server's metadata language, then TMDB's main one. scheme=http asks for a plain http image, for clients that can't load https. Paths are cached for 7 days |
| GET | /Moonfin/Tmdb/Details |
Yes | Overview, rating, genres, runTimeTicks, a textless backdrop, a logo and a YouTube trailer for a title the server doesn't hold (type, tmdbId or imdbId), in the server's metadata language. Shares Thumb's cache |
| GET | /Moonfin/Tmdb/PersonLinks |
Yes | A person's IMDb id, website, and Instagram, X, Facebook, TikTok and YouTube handles (tmdbId). Cached for 24 hours |
| GET | /Moonfin/Tmdb/Reviews |
Yes | TMDB user reviews for a title (tmdbId, type, language), up to 20, with the HTML turned into light Markdown. Asks for English when the requested language has none. Cached for 24 hours per title and language |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/MdbList/Ratings |
Yes | MDBList ratings for a title (type = movie or show, tmdbId), filtered to the user's chosen sources. An optional profile query (desktop, mobile, or tv) resolves the sources against that device profile, anything else falls back to global
|
| GET | /Moonfin/MdbList/KeyInfo |
Admin | Validate an MDBList API key and return the account's plan and remaining quota (backs the Test Key button, optional key query tests an unsaved key, otherwise the server-wide key is used) |
| POST | /Moonfin/MdbList/ClearCache |
Admin | Drop every cached MDBList rating so lookups refetch from the API |
| GET | /Moonfin/MdbList/Lists |
Yes | The cached catalog of MDBList official lists |
| GET | /Moonfin/MdbList/Lists/{slug}/Items |
Yes | Cached items for one official list (optional mediatype). Moonbase for Plex serves both list routes too, fetching each list the first time it's asked for instead of on a schedule |
| GET | /Moonfin/MdbList/WatchingNow |
Yes | What MDBList's users are watching right now, for the Watching on MDBList row, with the user's own MDBList key or the server-wide one (optional profile). One answer is shared by everyone for a few minutes. Without a key it answers an error instead. Served on all four servers |
| GET | /Moonfin/Tmdb/EpisodeRating |
Yes | TMDB rating for one episode (tmdbId, season, episode) |
| GET | /Moonfin/Tmdb/SeasonRatings |
Yes | TMDB ratings for every episode in a season (tmdbId, season) |
| GET | /Moonfin/Tmdb/NextEpisode |
Yes | The next episode to air for a series (tmdbId), so clients can show an air date without Sonarr. Answers { "success", "airDate", "seasonNumber", "episodeNumber", "name" }, or success: false with an error when there's no TMDB key or nothing is scheduled. Uses the user's TMDB key or the server-wide one, and caches each series for 6 hours for all users |
| GET | /Moonfin/Tmdb/ProductionCompanies |
Yes | TMDB studios for a title (tmdbId, type), each flagged for logo availability |
| GET | /Moonfin/Tmdb/StudioImage/{companyId} |
Yes | A cached studio logo PNG (re-downloaded from TMDB if evicted) |
| GET | /Moonfin/Tmdb/StudioLogos |
Yes | Every cached studio and network that has a logo, for the Studios row. A studio item carries no TMDB id, so apps match it to an entry here by name. Jellyfin and Emby |
| GET | /Moonfin/Assets/{fileName} |
No | Bundled rating provider icons and the flags poster labels can use |
| GET | /Moonfin/UserRatings/Mine |
Yes | Every personal rating the calling user has set, in one response, so a client can sort a library by My Rating and filter it by liked or disliked. Neither server has a user rating sort of its own |
| POST, DELETE | /Moonfin/UserRatings/{itemId} |
Yes | Emby only. Set the calling user's rating for an item (rating out of ten, or likes for a thumb) or clear it. Emby's own rating route answers 200 but stores nothing, so Moonfin writes through here |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/CustomRows/Items |
Yes | Resolve an external home row (source, type, params) into items, cached server-side for 24 hours. Items carry officialRating (the TMDB certification), overview, genres and runTimeTicks, and the response is filtered by the calling user's parental limit, where a title with no known rating is left out under a limit. A row fetched with the user's own TMDB or MDBList key is cached for that user only. An optional language (a BCP 47 tag like fr or pt-BR) fetches MDBList, TMDB and Letterboxd rows in that language and caches each language apart. IMDb rows stay in English. Items without an overview get the overview, genres and runtime from TMDB in the server's metadata language. On Jellyfin a resolve keeps running if the client disconnects, and a repeat request joins the one in progress |
| GET | /Moonfin/Collections/{collectionId}/Order |
Yes | The current user's saved item order for a collection |
| POST | /Moonfin/Collections/{collectionId}/Order |
Yes | Save the user's item order for a collection (empty array clears it) |
Moonbase for Plex serves CustomRows/Items too. It doesn't filter outside titles by a managed Plex user's rating limits.
Jellyfin and Emby.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Bookmarks |
Yes | Every bookmark the calling user has, across items |
| GET | /Moonfin/Bookmarks/{itemId} |
Yes | One item's bookmarks and notes for the calling user |
| POST | /Moonfin/Bookmarks/{itemId} |
Yes | Save the item's bookmarks for the calling user |
| POST | /Moonfin/Bookmarks/{itemId}/Notes |
Yes | Save the item's notes for the calling user |
Clients work from local storage first and reconcile with these, so nothing waits on the network to open a book.
All game endpoints require the game library feature to be enabled. See Retro Games for the full guide.
A library backed by RomM answers the same endpoints. Its game ids look like romm-12 and its BIOS ids like romm-fw-3, and the ROM and BIOS streams come from RomM through the server. For a user who linked their RomM account, those calls use that user's own RomM key, and the saves of RomM's games go to their RomM account. Save states are keyed ejs-<core>-romm-<id> or lr-<core>-romm-<id>, and battery saves (kind=sram) use the bare game id.
Moonbase for Plex serves these routes too, for Plex libraries whose folders it reads itself. There, the artwork protocol, arcade compatibility, Core, Cores/Install and Romm/Account answer 404, and the apps fall back to the plain Thumb artwork.
The EmulatorJS player page takes an optional sram URL next to save. It loads that battery save when the game starts and skips it when save is given, since a save state already holds it. window.moonfinGetSram() returns the battery save as base64 for the app to upload on exit.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Games/Libraries |
Yes | Libraries treated as game (ROM) libraries |
| GET | /Moonfin/Games/{libraryId}/Systems |
Yes | Top-level system folders in a game library |
| GET | /Moonfin/Games/{libraryId}/Games |
Yes | Games in a library, optionally filtered by system
|
| GET | /Moonfin/Games/{libraryId}/Games/{gameId} |
Yes | One game's full detail (ROM, core, BIOS) |
| GET | /Moonfin/Games/{libraryId}/Thumb/{gameId} |
Yes | Stream a game's box art or screenshot (type query), downloaded and cached on first request |
| GET, HEAD | /Moonfin/Games/{libraryId}/Rom/{token} |
Yes | Stream a ROM file, auto-extracting archives, with range support |
| GET, HEAD | /Moonfin/Games/{libraryId}/Rom/{token}/{fileName} |
Yes | The same stream with an explicit file name, which some cores need to identify the ROM |
| GET, HEAD | /Moonfin/Games/{libraryId}/Bios/{token} |
Yes | Stream a BIOS file a core needs |
| GET | /Moonfin/Games/Cores/Status |
Yes | Whether the self-hosted emulator cores are installed or downloading |
| POST | /Moonfin/Games/Cores/Install |
Admin | Download and install the cores bundle in the background |
| POST | /Moonfin/Games/Cores/Upload |
Admin | Install cores from an uploaded zip |
| GET | /Moonfin/Games/Saves/{gameId} |
Yes | Download a saved game blob for the current user (kind is state, sram or settings, default state) |
| PUT | /Moonfin/Games/Saves/{gameId} |
Yes | Upload or overwrite a saved game blob (max 32 MB) |
| GET | /Moonfin/Games/Romm/Account |
Yes | The current user's RomM link. While a link waits for approval, each call also checks with RomM. Returns {available, linked, username, pending, error}
|
| POST | /Moonfin/Games/Romm/Account |
Yes | Start linking the current user's RomM account. pending holds userCode, verificationUrl, verificationUrlComplete, expiresInSeconds and intervalSeconds
|
| DELETE | /Moonfin/Games/Romm/Account |
Yes | Unlink the current user's RomM account |
| GET | /Moonfin/EmulatorJS/player.html |
No | The in-browser player shell |
| GET | /Moonfin/EmulatorJS/data/{**path} |
No | Self-hosted EmulatorJS runtime files, with a CDN fallback when not installed |
| GET | /Moonfin/Games/Debug |
Admin | Diagnostics for game library detection |
| POST | /Moonfin/Games/Romm/Test |
Admin | Check a RomM address and token from the admin page before saving them. Body {"url", "token"}
|
| PUT | /Moonfin/Games/{libraryId}/Games/{gameId}/Core |
Yes | Jellyfin and Emby. Pin one game to a specific core |
| PUT | /Moonfin/Games/{libraryId}/Games/{gameId}/Backend |
Yes | Pin one game to the native or EmulatorJS backend |
Jellyfin and Emby.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Games/ArtworkCapabilities |
Yes | Advertises the manifest-driven artwork protocol, so older clients keep the old path |
| GET | /Moonfin/Games/{libraryId}/ArtworkManifest |
Yes | The cataloged artwork state for one system, without scheduling any provider work. Returns 304 when nothing changed |
| POST | /Moonfin/Games/{libraryId}/ArtworkPriority |
Yes | Promotes already cataloged artwork into the order the client actually needs it |
| GET | /Moonfin/Games/{libraryId}/Artwork/{gameId}/{role}/{revision} |
Yes | One versioned artwork file, served locally |
| POST | /Moonfin/Games/{libraryId}/Artwork/{gameId}/Refresh |
Admin | Repair path that reopens and reschedules one artifact |
| POST | /Moonfin/Games/{libraryId}/Artwork/Refresh |
Admin | Library-wide repair, optionally for one system. Checks each system's game database for updates, then reopens artwork that never found an original. Answers 202 and runs in the background, or 409 when a refresh is already running |
Manifest entries carry each image's width and height, and ArtworkCapabilities answers dimensions: true, so a client can shape a system's grid around its box art.
Jellyfin and Emby.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Games/ArcadeCompatibility/Status |
Admin | Whether the FBNeo and MAME data files are installed, and what they cover |
| PUT | /Moonfin/Games/ArcadeCompatibility/{core}/Dat |
Admin | Replace one core's data file with an uploaded XML, up to 512 MB |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Transcodes/Active |
Admin | Active transcode and download jobs with progress and session info (includePlayback adds HLS playback jobs) |
| DELETE | /Moonfin/Transcodes/Active/{jobId} |
Admin | Cancel an active job |
| GET | /Moonfin/Transcodes/Mine |
Yes | Jellyfin and Emby. The caller's own transcode jobs, scoped by the device id on its token, so a client can show progress and an ETA for a download it started |
Emby, and Moonbase for Silo and Plex. Jellyfin has POST /ClientLog/Document of its own and Moonfin uses that there. Emby has no equivalent anywhere in its API, so the plugin provides one, which is what makes the app's Send report to server button work on Emby. Silo and Plex have none either, so Moonbase for them takes reports the same way and writes each one to its own log, since neither server gives it a log folder.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /Moonfin/ClientLog/Document |
Yes | Upload a diagnostic or crash report as text/plain. The report is written into the server's log folder as upload_<client>_<version>_<utc>.txt, matching the .txt Emby writes its own logs as, so it appears under Logs beside them. The response names the file: { "FileName": "..." }
|
Not admin-only on purpose: crash reports come from whichever user hit the bug. The guards are elsewhere. The body is capped at 1 MB (413 past it), each user gets 10 uploads an hour (429 past that), the file name is built entirely on the server from the authenticated session rather than anything the client sends, and the whole endpoint returns 503 when Enable client log upload is off in the plugin settings. Clients redact server addresses, IP addresses and access tokens before sending.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Seerr/Config |
Yes | Seerr config for the client (see the response shape below) |
| POST | /Moonfin/Seerr/Login |
Yes | Sign in to Seerr with Jellyfin/Emby or local credentials and store the session |
| GET | /Moonfin/Seerr/Status |
Yes | Whether Seerr is enabled and the current user's session status |
| GET | /Moonfin/Seerr/Validate |
Yes | Confirm the stored session is still valid |
| DELETE | /Moonfin/Seerr/Logout |
Yes | Log out and clear the stored session |
| GET, POST, PUT, DELETE | /Moonfin/Seerr/Api/{**path} |
Session | Authenticated proxy to the Seerr API using the stored session. For a user with a parental limit, titles above it and unrated ones are left out of discover, search, recommendations, credits and collection results, and opening, requesting or watchlisting one answers 403. The rating comes from TMDB, so without a TMDB key such a user gets nothing |
Moonbase for Plex serves these routes too. Its Login takes {"authType": "plex", "authToken": "<plex.tv token>"} or local credentials, and refuses a Plex sign-in whose Seerr account belongs to someone else. It has no parental filter and no webhook.
See Reverse Proxy and Seerr for proxy and single sign-on notes.
{
"enabled": true,
"url": "https://seerr.example.com",
"displayName": "Seerr",
"variant": "seerr",
"userEnabled": true
}| Field | Type | Description |
|---|---|---|
enabled |
bool | Whether Seerr is enabled by the admin |
url |
string | The Seerr server URL used for proxying |
displayName |
string | The name shown in the client (admin override, defaults to Seerr) |
variant |
string | Always seerr on current versions |
userEnabled |
bool | Whether the user has Seerr enabled in their own settings |
How the feature works for admins is on Calendar. Moonbase for Plex answers the two calendar routes from the Sonarr and Radarr set on its admin page, and serves the reminder routes too. It checks Plex for arrivals every 2 minutes while anyone has a reminder.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Seerr/Radarr/Calendar |
Yes | The Radarr calendar from start to end (yyyy-MM-dd), trimmed to the Days back and Days ahead window, with optional unmonitored=true. The server reads the Radarr connection from Seerr through a stored Seerr admin session and calls Radarr itself, so the API key stays on the server and remote clients still see it. Each movie in one of the caller's libraries gains moonfinItemId and moonfinLibraryIds. One answer from Radarr is reused for Cache minutes. Returns 403 when Who can see the calendar is Admins only and the caller isn't an admin, 503 when Seerr is off or no Seerr admin has signed in through Moonfin, and 404 when Seerr has no Radarr server. It uses Seerr's regular (non-4K) default Radarr, and when Seerr's internal address for it can't be reached it tries the External URL set in Seerr, but only over https or when that address is on the local network |
| GET | /Moonfin/Seerr/Sonarr/Calendar |
Yes | The same for Sonarr, with each episode's series and images included. An episode whose series is in the caller's libraries gains moonfinItemId (the series) and moonfinLibraryIds, plus moonfinEpisodeId when the episode itself is on the server. Clients also use it for a series' next air date and fall back to TMDB |
| GET | /Moonfin/Calendar/Reminders |
Yes | The caller's reminders as {"reminders": [...]}. Each has a key, kind (episode or movie), tvdbId, tmdbId, imdbId, seasonNumber, episodeNumber, title, subtitle and createdAt
|
| POST | /Moonfin/Calendar/Reminders |
Yes | Saves a reminder from those fields and returns it with its key (episode:<tvdb>:<season>:<episode>, movie:<tmdb> or movie:imdb:<id>). Saving the same title again keeps the first createdAt. Returns 400 when the fields can't make a key or the caller already has 500 |
| DELETE | /Moonfin/Calendar/Reminders/{key} |
Yes | Removes one reminder. A reminder is also removed when it fires, which happens as a seerrNotification on the live stream plus a push when its title lands in the library |
The Ping answer carries calendarAvailable (false when Seerr is off, or the caller is turned away by Admins only), calendarDaysBack, calendarDaysAhead and calendarReminders.
Why a Radarr or Sonarr download for a request is stuck, and the admin action behind Get a different release. On Jellyfin and Emby these read the same Radarr and Sonarr as the calendar, through a stored Seerr admin session, and answer 503 while Seerr is off. Moonbase for Plex reads the Sonarr and Radarr set on its admin page, and answers 404 for one that isn't set. See Seerr Notifications.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Seerr/Radarr/Queue |
Yes | A movie's Radarr queue, looked up by tmdbId. Answers service, inArr, canRemove (whether the caller is an admin), monitored, hasFile, isAvailable and items, each with Radarr's own status, trackedDownloadState, trackedDownloadStatus, size, time left, errorMessage and statusMessages. An empty items with inArr true means Radarr has the movie and nothing is downloading. Server paths in the messages are cut down for anyone but an admin, and only admins get outputPath. Returns 400 without a positive id |
| GET | /Moonfin/Seerr/Sonarr/Queue |
Yes | The same for a series in Sonarr, looked up by tvdbId, with each entry's episodeId, seasonNumber and episodeNumber
|
| DELETE | /Moonfin/Seerr/Radarr/Queue/{id} |
Admin | Remove a queue entry from the download client, blocklist its release and let Radarr search again. Answers {"removed": [...]}, or 404 when it's no longer in the queue |
| DELETE | /Moonfin/Seerr/Sonarr/Queue/{id} |
Admin | The same in Sonarr. Every entry that shares the download, such as the episodes of a season pack, goes with it |
How the feature works for admins is on Watch Parties. Moonbase for Plex serves the same routes from the companion, with the Plex server token as X-Plex-Token. The Silo build doesn't serve them.
- Room calls also send
X-Moonfin-Room-Token, thetokenfrom the create or join answer. It belongs to one user on one device, and a token that isn't the caller's answers403. - JSON keys are snake_case, positions are seconds as doubles, and times are ISO 8601 UTC with milliseconds. The room and message shapes follow Silo's watch-together protocol, with a few additions such as member
status,anchor_at,has_pin,invite_urland the chat, reaction and notice messages. - Errors answer
{"code", "message"}. The codes aredisabled,pin_required,wrong_pin,no_access,not_hostandnot_allowed(403),not_found(404),full(409),too_many_attempts(429, withretry_after_secondsand aRetry-Afterheader),rate_limited(429) andbad_request(400). - Every route but
/Configanswers403disabledwhile Turn on Watch Parties is off, so a404on/Configmeans a Moonbase without Watch Parties.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/WatchParty/Config |
Yes |
{"enabled", "chat", "reactions", "alerts", "everyone_control_allowed", "max_members", "version"}, answered whether Watch Parties are on or off |
| GET | /Moonfin/WatchParty/Time |
Yes |
{"server_time"}, the clock players line up against |
| GET | /Moonfin/WatchParty/Rooms |
Yes |
{"rooms": [...]}, the open rooms the caller can watch. A summary has no code, so a listed room is joined by room_id. Rooms whose item the caller can't play are left out, and lobbies with nothing picked are listed |
| POST | /Moonfin/WatchParty/Rooms |
Yes | Create a room. Body {"content_id"?, "pin"?, "guest_control_policy"?}. Answers {"room", "token"}. It closes any other room the caller hosts |
| POST | /Moonfin/WatchParty/Join |
Yes | Join by code (case doesn't matter, spaces and dashes are ignored) or room_id, plus pin when the room has one. Answers {"room", "token"}
|
| POST | /Moonfin/WatchParty/Rooms/{id}/Stage |
Host | Pick the item (content_id) and end voting |
| POST | /Moonfin/WatchParty/Rooms/{id}/Start |
Host | Start from position_seconds (default 0). Everyone loads the item, and once every connected member is ready, or after 10 seconds, a 3 second countdown starts playback |
| POST | /Moonfin/WatchParty/Rooms/{id}/Stop |
Host | Back to the lobby with the same item picked. A run also returns to the lobby on its own when the item ends |
| POST | /Moonfin/WatchParty/Rooms/{id}/Policy |
Host |
guest_control_policy: host_only, guests_can_pause or everyone. everyone answers not_allowed when the admin turned it off |
| POST | /Moonfin/WatchParty/Rooms/{id}/Pin |
Host | Set a room PIN of 4 to 8 digits, or null to remove it |
| POST | /Moonfin/WatchParty/Rooms/{id}/Voting |
Host | Turn the vote on what to watch on or off (voting) |
| GET | /Moonfin/WatchParty/Rooms/{id}/Suggestions |
Member | The titles up for a vote, each {"id", "content_id", "title", "subtitle", "vote_count", "voted_by_me", "created_at"}
|
| POST | /Moonfin/WatchParty/Rooms/{id}/Suggestions |
Host | Add a title to the vote (content_id). Answers 204
|
| POST | /Moonfin/WatchParty/Rooms/{id}/Suggestions/{sid}/Vote |
Member | Vote, or take the vote back, with voted. Answers 204
|
| DELETE | /Moonfin/WatchParty/Rooms/{id}/Suggestions/{sid} |
Host | Take a title out of the vote. Answers 204
|
| POST | /Moonfin/WatchParty/Rooms/{id}/Promote |
Host | Hand the room to another member (member_id) |
| POST | /Moonfin/WatchParty/Rooms/{id}/Remove |
Host | Take a member out (member_id). Answers 204
|
| POST | /Moonfin/WatchParty/Rooms/{id}/Close |
Host | End the room for everyone. Answers 204
|
| POST | /Moonfin/WatchParty/Rooms/{id}/Leave |
Member | Leave. A leaving host hands the room to the earliest member who joined. Answers 204
|
| GET | /Moonfin/WatchParty/Rooms/{id}/Events |
Member | The long poll. cursor is the last cursor seen and wait the seconds to hold, capped at 25. Answers {"cursor", "messages", "reset"?} as soon as there's something newer. The first call (no cursor, or 0) answers at once with a snapshot and the last 50 chat messages. A cursor older than the last 200 messages gets reset: true and a fresh snapshot |
| POST | /Moonfin/WatchParty/Rooms/{id}/Messages |
Member | Send {"messages": [...]}: ping, transport_request, state_report, buffering, attach_session, chat or reaction. Answers {"messages": [...]} with any replies, such as the pong
|
In the Auth column, Member needs the caller's room token, and Host also needs the caller to be the room's host, otherwise it answers not_host. Under everyone, guests may also play, pause and seek through transport_request. Events sends snapshot (rendered for each member), transport_command (with execute_at on the server clock and the name of whoever asked), suggestions_update, chat, chat_history, reaction (love, haha, wow, sad, fire or popcorn), notice (joined, left, host_changed, policy_changed, switched or no_access), error, removed and room_closed. A member is shown as away after 35 seconds without an Events call, and removed after 120. Chat is capped at 500 characters and one message a second with bursts of 5, reactions at 4 a second, and transport requests at 10 a second.
The admin routes, used by the Live rooms list:
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Admin/WatchParty/Rooms |
Admin |
{"rooms": [...]}, every open room with room_id, code, host_name, phase, playback_state, position_seconds, content_title, content_label, has_pin, created_at and its members with their user ids, connected and status
|
| POST | /Moonfin/Admin/WatchParty/Rooms/{id}/Close |
Admin | Close a room for everyone |
| POST | /Moonfin/Admin/WatchParty/Rooms/{id}/Remove |
Admin | Take a member out. Body {"member_id"}
|
The invite page:
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Party/{code} |
No | The invite page, as HTML with Open Graph tags for link previews. It never looks the code up, so it shows no title, host or members, only the server's name and address, the code, and an Open in Moonfin button that opens moonfin://party?server=<server id>&code=<code>&address=<public address>. Anything that can't be a room code answers 404
|
| GET | /Moonfin/Party/preview.png |
No | The 1200x630 link preview image |
Moonbase for Silo keeps what Silo's own Watch Party rooms don't, by Silo room id:
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/WatchParty/{roomId} |
Yes | The room's subtitle choice and its members' avatars, or {} for a room it doesn't know. Rooms are forgotten after 24 hours |
| PUT | /Moonfin/WatchParty/{roomId} |
Yes | Set the room's subtitle choice, {"subtitle": {...}} for a track, {"subtitle": {"off": true}} for none, or {"subtitle": null} to leave it to each member. The first profile to set it owns the room, and anyone else gets 403
|
| PUT | /Moonfin/WatchParty/{roomId}/Avatar |
Yes | Set the caller's own avatar link for the room, {"url"}, an http(s) link or a path on the server |
| DELETE | /Moonfin/WatchParty/{roomId} |
Yes | Forget the room when its party ends, so it stops counting toward the account's 10 stored rooms. The profile that set its subtitles can, or the account that stored it when nobody did. Answers 204
|
Jellyfin only. Roku can't open Jellyfin's websocket on a server reached over https, so Moonbase holds the socket for it and passes SyncPlay messages on through a long poll. The relay is always the caller's own user, device and token. It follows SyncPlay relay for Roku on the Watch Parties tab, and the Ping answer's socketRelay says whether it's on.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /Moonfin/SocketRelay/Open |
Yes | Open the caller's relay, or keep using the one already open. Answers active, created, state and cursor. 429 past 10 relays for one user or 200 in all, and 503 while the relay is off |
| GET | /Moonfin/SocketRelay/Poll |
Yes | The long poll. cursor is the last cursor seen and waitMs how long to hold, up to 25000. Answers cursor, messages, state and reset. 404 when no relay is open, and 410 when it's gone or the relay is off, so the app goes back to Open |
| POST | /Moonfin/SocketRelay/Close |
Yes | Close the caller's relay. Answers 204
|
The top 3 for each of the apps' hidden After Hours games, shared by everyone on the server. Each user keeps one row per game, their best, and a tie goes to whoever got there first. Served on all four servers. The boards are stored in minigames/scores.json, see Data Locations.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/MiniGames/{gameId}/Scores |
Yes | The board, best first, each row {"userName", "score", "at"}. gameId is buffer-run, fast-forward, cable-crawl, case-stack or pixel-purge, and anything else answers 404
|
| POST | /Moonfin/MiniGames/{gameId}/Scores |
Yes | Record a finished run, {"score"}, a whole number from 0 to 9,999,999. Answers {"rank"}, its place on the board, or null when it isn't the user's new best or misses the top 3. An API key without a user gets 401
|
The labels the admin sets up on the Labels tab, which the apps draw over posters. Jellyfin, Emby and Moonbase for Plex. Labels are read from each file's own tracks, so a viewer only gets them for items they can see.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/PosterBadges/Items |
Yes | The labels for up to 200 comma-separated item ids (ids), for the caller's device (profile is tv, desktop or mobile). Answers enabled, version, maxPerCorner and items keyed by the id sent, leaving out items with no label. enabled: false means this viewer gets no labels at all. Without ids it only answers whether they get any |
| POST | /Moonfin/PosterBadges/Test |
Admin | Try the rules the admin page holds, saved or not, on one movie or episode. Body {"itemId", "rules", "defaults"}. Answers which labels show, why the others don't, and the track values the conditions read |
| GET | /Moonfin/PosterBadges/Presets |
Admin | The built-in labels, each with id, label, group, color and description
|
| GET | /Moonfin/PosterBadges/Search |
Admin | Moonbase for Plex only. Finds a movie or episode by name (term) for the Test a file tool, since its admin page can't search Plex |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET, POST | /Moonfin/Notifications/Prefs |
Yes | Get or save the caller's notification preferences, including the new media opt-in (notifyOnNewMedia) |
| POST, DELETE | /Moonfin/Notifications/Register |
Yes | Register or remove a push token for the caller's device |
| GET | /Moonfin/Notifications/WebhookInfo |
Admin | The Seerr webhook URL, secret, supported types, and provisioning status |
| POST | /Moonfin/Notifications/Reprovision |
Admin | Re-register Moonfin's webhook in Seerr immediately |
| POST | /Moonfin/Seerr/Webhook |
No (shared secret) | Inbound Seerr webhook, authenticated by the secret query or X-Moonfin-Webhook-Secret header |
Moonbase for Plex only serves Notifications/Register, for calendar reminder pushes.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /Moonfin/Web/{**path} |
No | Serve the Moonfin web app static files with SPA fallback |
| GET | /Moonfin/Web/config.json |
No | Runtime config for web plugin mode |
| GET | /Moonfin/Web/loader.js |
No | The loader bridge script for the stock Jellyfin header button |
| GET |
/Moonfin/Discovery and /Moonfin/Discovery/discover
|
No | Same-origin server metadata for web discovery |
For server admins
- Installation
- Admin Guide
- Seerr Setup
- Calendar
- Smart Search
- Watch Parties
- Common Problems
- Retro Games
- Themes
Reference
For developers