-
Notifications
You must be signed in to change notification settings - Fork 2
API Reference
🌐 English · Deutsch
There are two API layers:
-
External REST API (
/api/v1/...) — for scripts, dashboards (e.g. Homepage/Homarr) and third-party systems. Authenticated via API key, no login needed. -
Internal API (
/api/...) — used by the WebUI, session-based (login required, partly admin). It is not a stable contract, but the most important endpoints are documented below anyway.
Send the API key in the X-Api-Key header. The key is generated automatically on first start and can be viewed (and regenerated) under Settings → API.
curl -H "X-Api-Key: <key>" http://localhost:8080/api/v1/statusMissing/wrong key → 401 Unauthorized.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/status |
Overall status: version, pause state, queue counters by status, currently running download incl. live progress (episode_progress: percent, phase, speed, bandwidth; overall_progress_percent) |
| GET | /api/v1/queue |
All queue entries; filter: `?status=queued |
| GET | /api/v1/queue/<id> |
Single queue entry (404 if unknown) |
| GET | /api/v1/library |
Full library (all paths, titles, seasons, episodes, sizes) |
| GET | /api/v1/library/series |
Series only |
| GET | /api/v1/library/movies |
Movies only |
| GET | /api/v1/stats |
Overall download statistics |
| GET | /api/v1/autosync |
Auto-Sync jobs — every job of every user (the internal /api/autosync is filtered to the caller) |
| GET | /api/v1/history |
Download history — every user; filters: ?status=, ?limit=, ?offset=
|
| GET | /api/v1/upscale |
Upscale queue: items, badge count and the running job's progress |
| GET | /api/v1/mediascan |
MediaScan (Plex/Jellyfin import): run status and cached title count |
| GET | /api/v1/uptime |
UpTime monitor: current status, uptime % and average response time per tracked source |
| GET | /api/v1/update-status |
Self-update: progress of a running update, or the idle state |
{
"version": "2.5.3",
"paused": false,
"queue": {"total": 12, "queued": 2, "running": 1, "completed": 8, "failed": 1, "cancelled": 0},
"currently_running": {
"id": 42, "title": "…", "current_episode": 3, "total_episodes": 12,
"episode_progress": {"percent": 57, "phase": "download", "speed": "2.1x", "bandwidth": "8.4 MB/s", "active": true},
"overall_progress_percent": 29
}
}All internal endpoints require a logged-in session; POST/PUT/DELETE require Content-Type: application/json. Entries marked (admin) additionally require the admin role.
| Method | Path | Description |
|---|---|---|
| GET | /api/search/sources |
Every content source a keyword search can currently reach: built-ins plus the ones installed modules registered (search.register_search_source), each with {id, label, adult, thirdparty, enabled, css_class, media_types}, plus the user's saved order and hide_disabled_in_search. This is the list the WebUI fans a search out to, chips under the search field, and lists in Settings → Sources -- so a module source needs no frontend change. 18+ sources are omitted entirely for an age-limited session. media_types is a list of "movies"/"series"/"adult" -- what a module declared on register_search_source(..., media_types=…), or web/source_policy.py's BUILTIN_SOURCE_MEDIA_TYPES for a built-in, defaulting to both films and series. A lookup that already knows what it wants (the Seerr page's "find streams" search) uses it to skip sources that cannot have it. |
| POST | /api/search |
{keyword, site} -- site is any id from /api/search/sources (built-in: aniworld, sto, filmpalast, megakino, hanime; or a module's own). An 18+ source answers 403 age_limited for a limited session. |
| GET |
/api/series / /api/seasons / /api/episodes
|
Series/season/episode data for a URL. /api/episodes additionally answers with locations: where this season's already-downloaded files sit, as {root_label, folder, episodes} per folder, biggest first. root_label is the custom path's name (empty = the global download folder); the absolute path is only included for admins. |
| GET | /api/providers |
Available hosts/languages of an episode, as {language_label: [hoster, ...]}. Built from the episode's provider_data: an (Audio, Subtitles) tuple key is translated into a label through the language maps, a plain string key is taken as the label it already is — so a module may write {"German Dub": {"VOE": url}} without touching the enums. |
| GET |
/api/new-animes, /api/popular-animes, /api/new-series, /api/popular-series, /api/new-movies, /api/random
|
Browse lists |
| GET | /api/home-feed?adult=0|1&limit=30&pool=0|1 |
The whole new home page in one answer: rows (new/popular/movies, deduplicated across all three), the source list with its enabled/error state, and which sources failed upstream. Includes sources registered by modules. With pool=1 the server builds a reserve pool of up to limit*4 cards (hard ceiling 150) instead of exactly limit, and the answer carries a top-level limit field saying how many of them the client should show. Without it the behaviour is unchanged — the Start Page settings preview relies on that. |
| GET | /api/home-feed/personal |
The personal home rows for the current user: continue, watchlist, library, upcoming. Every one degrades to an empty list instead of an error. |
| GET | /api/home-panels |
The home page's button bar: one entry per panel the current account may see, each with a count badge. Admin-only panels are omitted for non-admins. Includes panels registered by modules. |
| GET | /api/home-panel/<id> |
The body of one panel (stats, list, link). 403 for an admin-only panel, 404 for an unknown one; a panel that fails answers 200 with error: true. |
| GET | /api/home-feed/sources |
Every source the feed knows (built-in + module-registered) and the effective row layout, without fetching anything. Feeds the Start Page settings. |
| GET | /api/home-feed/row/<row>?adult=0|1&limit=30&pool=0|1 |
One discovery row (new, popular, movies) on its own. What the home page actually uses: one slow source can no longer hold up the whole page, and rows below the fold are only fetched once they are scrolled to. Cross-row deduplication moves to the client. Takes the same pool=1 as /api/home-feed, and the home page passes it: deselecting a source in the Sources dropdown then no longer shortens the row — the cards behind the cut move up. The home page also resolves the CineInfo/TMDB metadata for the held-back cards in background batches, so a filter change promotes finished cards rather than blank ones. |
| GET | /api/mediaplayer/users |
The configured Jellyfin/Plex server's user list, for the profile picker. Names only. {configured, server, users, linked}. |
| GET | /api/mediaplayer/image?path=… |
Artwork proxy for the media server, so the browser never needs its address or token. Server-relative paths only. |
| GET | /api/home/wrapped?period=YYYY-MM |
The monthly recap card: watched (from the linked Jellyfin/Plex user, absent when none is linked) and downloaded (always MediaForge's own history). |
| GET | /api/home/onboarding |
What this instance still needs (sources, TMDB key, library scan, modules, media-server link). Admin-only steps are omitted for a normal account. |
| GET | /api/home/suggest?q=… |
Instant search suggestions, grouped into library and watchlist. Local lookups only — no provider scrape per keystroke. |
| POST | /api/home/mode |
Switch the active home mode: {mode, max_fsk, pin}. Lowering the age ceiling is always allowed; raising it needs the kids-mode PIN when one is set. The ceiling itself is enforced server-side in the feed, never in the browser. |
| Method | Path | Description |
|---|---|---|
| POST | /api/download |
Create a download job |
| GET | /api/queue |
Queue incl. progress |
| POST |
/api/queue/pause / /api/queue/resume
|
Global pause |
| POST |
/api/queue/<id>/cancel / restart / skip-episode / retry-episode / move
|
Actions |
| DELETE |
/api/queue/<id> / /api/queue/completed
|
Delete an entry / all finished entries |
| POST | /api/autosync/<id>/dry-run |
What the job would do right now: episodes online/local, what would be queued per language, what was skipped. Writes nothing — not the queue, not the job's own bookkeeping, not even on failure |
| POST | /api/queue/bulk |
One action (cancel/remove/retry/top/bottom) on up to 500 ids; every id is reported individually in succeeded/failed
|
| Method | Path | Description |
|---|---|---|
| GET/POST | /api/autosync |
List / create jobs (create: admin); body incl. language, provider, custom_path_id, episode_filter (JSON), movie_custom_path_id
|
| POST | /api/autosync/site-search |
Resolve a title to candidate series on AniWorld/serienstream.to (used by the Library "Add to Auto-Sync"); body {title}, returns results[] with site, site_label, title, url, score
|
| PUT/DELETE | /api/autosync/<id> |
Update (incl. episode_filter, movie_custom_path_id, group_name) / delete (admin)
|
| POST |
/api/autosync/<id>/sync, /api/autosync/sync-all
|
Sync immediately |
| GET/POST |
/api/autosync/export, /api/autosync/import, /api/autosync/batch
|
Backup / restore / bulk add. batch actions: enable, disable, set_path, set_group (+group_name), remove_group, delete
|
| POST | /api/autosync/group/rename |
Rename a group; body {old, new} — sets group_name of all own jobs (admin: all) from old to new
|
| Method | Path | Description |
|---|---|---|
| GET | /api/calendar |
Aggregated calendar events for the user (AutoSync + optional Seerr/Library overlays). Returns events[], a watcher status block (active, is_scanning, last_sync) and meta (seerr_active, seerr_count). Requires the calendar to be enabled + a TMDB key. |
| GET | /api/history |
Download history; query: ?search=, ?status=all|completed|failed|cancelled|skipped, ?source=all|manual|autosync|seerr, ?range=all|1d|7d|30d, ?limit= (max 200), ?offset=. Users see their own entries, admins all. Returns entries[] + total. |
| GET | /api/history/<id> |
Single history entry (403 if not yours and not admin) |
| GET | /api/history/export |
Export the filtered view; ?format=csv|json plus the same filters as /api/history
|
| POST | /api/history/<id>/retry |
Re-queue a failed/cancelled episode |
| DELETE | /api/history/<id> |
Delete a single entry |
| POST | /api/history/delete |
Delete multiple entries (body {ids: [...]}) |
| POST | /api/history/clear |
Clear the history; body may include status/source/range/search to delete only the filtered subset (otherwise all; own entries, admins all) |
| Method | Path | Description |
|---|---|---|
| GET/PUT | /api/settings |
General settings. Includes default_ui_toggles: the instance default for the eight design toggles, a comma-separated list of the ui_* preference keys that should start on (ui_glow_effect, ui_header_color, ui_header_color_help, ui_skeleton_loader, ui_choose_border, ui_active_download_glow, ui_click_effect, ui_icon_move). Edited under Settings → Design → "Default appearance for new accounts" → Advanced Settings and surfaced to templates as appearance_defaults.ui. Absent (null) is not the same as empty: null means no admin ever configured this and keeps the old per-browser localStorage fallback, an empty string means "explicitly all off". |
| GET/PUT |
/api/settings/sso, /api/settings/cineinfo, /api/settings/mediaplayer, /api/settings/mediascan, /api/settings/seerr, /api/settings/crunchyroll, /api/settings/dns
|
Sub-areas |
| GET/POST |
/api/settings/api-key, /api/settings/api-key/regenerate
|
External API key |
| GET/POST |
/api/encoding/settings, /api/encoding/detect-hw
|
Encoding |
| GET/POST |
/api/upscale/settings, … |
Upscaling (reading settings is allowed for everyone) |
| Method | Path | Description |
|---|---|---|
| GET/PUT | /api/settings/crunchyroll |
Load/save settings (password is write-only; clear_password: true forgets it) |
| POST | /api/settings/crunchyroll/test |
Validate login → {ok, mode, profile, premium}
|
| GET | /api/settings/crunchyroll/profiles |
List account profiles for the selector |
| GET | /api/crunchyroll/availability?title=… |
Whether a title is on Crunchyroll (powers the provider pill; cached 24 h) |
Backed by web/crunchyroll_service.py — a thread-safe client cache (6 h re-login TTL, encrypted session-token file, failure-tolerant) on top of the vendored vendor/crunchyroll_api.py client. Anonymous mode needs no account; watchlist/profile and premium checks require a login. The calendar uses get_simulcast_titles() / get_watchlist_titles() (Crunchyroll knows which anime; TMDB supplies the air dates). Video streams are DRM-protected and never downloaded.
| Method | Path | Description |
|---|---|---|
| GET |
/api/library / /api/library/status / /api/library/watcher
|
Inventory & scan status |
| POST |
/api/library/refresh (admin), /api/library/delete (admin), /api/library/rename (admin), /api/library/move (admin), /api/library/media_info
|
Management |
| POST |
/api/stream/start / /api/stream/stop
|
Start/stop an HLS transcode session (library files) |
| GET |
/api/stream/<token>/index.m3u8, /api/stream/<token>/segNNN.ts, /api/stream/<token>/status
|
HLS delivery |
| POST | /api/stream/start-source |
Stream an episode directly from the provider via transcode; body {episode_url, provider, language, start_pos}
|
| POST |
/api/stream/start-proxy / /api/stream/close-proxy
|
Start/stop a passthrough proxy session for the provider's native HLS (no FFmpeg); returns {token, playlist_url, hls}
|
| GET | /api/proxy/<token>/r/<b64url> |
Proxy a provider playlist (rewritten) or segment/key (streamed); forwards Range. SSRF-guarded (http/https only, private/loopback hosts blocked) |
| POST/GET |
/api/progress/save, /api/progress/get, /api/progress/bulk
|
Watch progress (per user) |
| GET | /api/downloaded-folders |
The folder names the library holds, for the "already downloaded" badges. Additionally carries aliases: a {loose_title_key: folder} map built from web/library_aliases.py, so the browser recognises a folder whose name shares nothing with the card's title. Empty until the background resolver has caught up (and always empty without a TMDB key) — the client then falls back to the plain string comparison. |
| Method | Path | Description |
|---|---|---|
| GET | /api/syncplay/config |
Whether a Syncplay server is configured |
| POST |
/api/syncplay/join / /api/syncplay/leave
|
Join/leave a room (server opens a real Syncplay client); body {room, username?}
|
| POST |
/api/syncplay/control / /api/syncplay/report
|
Send play/pause/seek; report current position (heartbeat) |
| GET | /api/syncplay/poll |
Poll authoritative state events + room user list (?session_id=&after=) |
| GET |
/api/console (admin)
|
Read-only live console output buffer; ?after=<seq> for incremental polling (only when the Web Console is enabled) |
| Method | Path | Description |
|---|---|---|
| GET |
/api/captcha/<queue_id>/screenshot / status; POST /api/captcha/<queue_id>/click
|
Interactive captcha solving |
| GET/POST |
/api/notif/settings, /api/notif/user-settings, /api/notif/admin-settings, /api/notif/test
|
Notifications |
| GET |
/api/seerr/requests; POST …/approve, …/decline, …/hide
|
Seerr requests |
| GET/POST | /api/update-check |
Update check |
| GET/POST/DELETE | /api/favourites |
Favourites |
| GET/POST | /api/user/preferences |
Per-account appearance (theme_pack, theme_mode, accent, plus the eight ui_* design toggles) — keys whitelisted server-side, extendable by modules via db.register_ui_pref_key(). The toggles now cascade over three levels like theme and accent do: account (user_ui_prefs ui_*) → instance default (default_ui_toggles) → localStorage mirror |
| GET |
/api/themes; PUT /api/themes/active (admin)
|
Installed theme packs / instance default |
| GET | /api/stats |
Statistics — accepts ?days= (1–365, default 30) for the trend series |
| GET | /api/stats/trends |
Chart series only for a given ?days= window (no library scan) |
| GET | /api/media/duplicates |
One page of duplicate groups: ?page=&per_page= (1–100, default 20) &q= + an aggregate summary |
| GET | /api/history/summary |
Aggregate figures + 30-day series for the active history filters |
| GET | /api/history/facets |
Providers and languages present in the history, for the filter dropdowns |
| GET | /api/media/incomplete |
One page of incomplete or ignored series: ?view=incomplete|ignored&page=&per_page=&q=
|
| GET |
/api/stats/queue, /api/stats/sync, /api/stats/general
|
Individual statistics sections |
| Method | Path | Description |
|---|---|---|
| GET | /healthz |
Liveness. {"status": "ok"} while the process is up |
| GET | /readyz |
Readiness. 200 when the database answers, 503 when it does not |
Both are deliberately login-exempt (Docker HEALTHCHECK, k8s probes, external monitors have no session) and return a single status string and nothing else — no version, no worker names, no error text. The detailed view is admin-gated at /api/ops/workers.
Registered by web/routes/ops.py. Every endpoint below is listed in that module's ADMIN_ONLY_OPS_ENDPOINTS, which app.py folds into _admin_only — importing the set instead of re-typing it is what stops a new endpoint from being registered but unprotected (asserted by tests/test_ops.py).
| Method | Path | Description |
|---|---|---|
| GET | /api/ops/audit |
Audit entries; ?q=&category=&severity=&since=&until=&limit=&offset=
|
| GET | /api/ops/audit/stats |
Totals, per-category counts, available categories/severities |
| GET | /api/ops/audit/verify |
Recompute the hash chain; reports the first mismatching row |
| GET | /api/ops/audit/export |
CSV of the filtered view |
| GET/POST | /api/ops/groups |
List / create groups |
| PUT/DELETE | /api/ops/groups/<id> |
Update / delete a group (built-ins refuse both) |
| PUT | /admin/api/users/<id>/groups |
Set a user's custom group membership |
| GET | /api/ops/schema |
Migration status + snapshot list |
| POST | /api/ops/snapshots |
Create a manual snapshot |
| GET | /api/ops/snapshots/<id>/verify |
Open read-only and prove it is restorable |
| POST | /api/ops/snapshots/<id>/restore |
Replace the live database (restart required) |
| DELETE | /api/ops/snapshots/<id> |
Delete a snapshot |
| GET | /api/ops/workers |
Worker heartbeats + health roll-up |
| GET/POST | /api/ops/maintenance |
List (incl. currently active limits) / create a window |
| PUT/DELETE | /api/ops/maintenance/<id> |
Update / delete a window |
| GET | /api/ops/diagnostics |
Scrubbed diagnostic bundle (ZIP download) |
| GET | /api/ops/profile/export |
Settings profile as plain JSON (secret-free, allowlisted) |
| POST | /api/ops/profile/preview |
Validate an uploaded profile and report what it would change |
| POST | /api/ops/profile/import |
Apply a profile, optionally limited to keys
|
| GET/POST | /api/ops/rules |
List (incl. field/operator/action catalogue) / create a rule |
| PUT/DELETE | /api/ops/rules/<id> |
Update / delete a rule |
| POST | /api/ops/rules/test |
Evaluate the saved rules against a hypothetical context |
| GET/POST | /api/ops/language-profiles |
List (incl. bindings) / create a profile |
| PUT/DELETE | /api/ops/language-profiles/<id> |
Update / delete a profile |
| POST | /api/ops/language-profiles/bind |
Bind or unbind a title (profile_id: null unbinds) |
See Operations, Audit Log and Rules & Languages for what these do.
GET /api/v1/openapi.json describes every endpoint, its path parameters and the scope it requires. It is built from the routes that are actually registered, so it cannot drift from the code — a specification maintained separately from the implementation is a specification that is wrong.
It is readable without a key, on purpose: a client cannot know which scopes to ask for until it can read the spec, and the document describes shapes rather than data.
A v1 endpoint is never removed and never changes shape in a breaking way. Anything that has to change appears under a new path. Anything on its way out answers with a Deprecation header for at least one minor release before it stops being documented.
The original external_api_key is all-or-nothing: whatever can read /api/v1/status can also read the whole library and the download history. A Home Assistant dashboard and a script that manages downloads got identical credentials, and revoking one meant revoking both.
A scoped key (Settings → API) carries only the scopes you pick:
| Scope | Opens |
|---|---|
status:read |
/api/v1/status |
queue:read |
queue, single queue item, upscale queue |
library:read |
library, series, movies, mediascan |
history:read |
download history |
stats:read |
statistics |
autosync:read |
Auto-Sync jobs |
uptime:read |
uptime monitor |
update:read |
update status |
Three properties matter:
- Hashed. Only a SHA-256 is stored. The plaintext is shown once, at creation, and cannot be recovered — a stolen database must not hand over working credentials. This is why the old key could not simply grow a scopes column: it is stored in clear text by design, because the settings page displays it.
- Individually revocable and expirable. An unparseable expiry counts as expired rather than being ignored; the alternative lets a malformed date make a key eternal.
- Unknown scopes are dropped on save. A scope nothing checks looks granted and does nothing.
An authenticated key without the required scope gets 403, not 401. The credential was fine, the permission was not, and conflating the two sends people to regenerate a key that was never the problem.
The legacy key keeps working and grants every scope. Breaking every existing integration to introduce scopes would be a poor trade, so scoped keys are an upgrade, not an imposition.
Fixed along the way: the login-exemption list named seven of the thirteen v1 endpoints by hand. The other six — autosync, uptime, update-status, mediascan, upscale, history — were wrapped in
login_required, which answers/api/paths with a bare 401. A caller with a perfectly valid API key was told its key was wrong, and the obvious next step (regenerating it) changed nothing. The list is now derived from the endpoint→scope map, so a new v1 endpoint cannot be born broken the same way.
🇬🇧 English
Users
- Installation
- Getting Started
- Migration from AniWorld
- Configuration
- Web UI
- Download System
- Download History
- AutoSync
- Calendar
- Library
- Authentication
- Notifications
- Integrations
- SyncPlay
- Anime4K Upscaling
- Encoding
- Modules
- Theme Packs
- Backup
- Operations
- Audit Log
- Rules & Languages
- Docker
- Supported Sites
Developers
🇩🇪 Deutsch
Benutzer
- Installation
- Erste Schritte
- Umzug von AniWorld
- Konfiguration
- Web-UI
- Download-System
- Download-Verlauf
- AutoSync
- Kalender
- Bibliothek
- Authentifizierung
- Benachrichtigungen
- Integrationen
- SyncPlay
- Anime4K-Upscaling
- Encoding
- Module
- Theme-Pakete
- Backup
- Betrieb
- Audit-Log
- Regeln & Sprachen
- Docker
- Unterstützte Seiten
Entwickler