# Configuration & API Reference
> [Back to Docs](README.md)
Complete reference for all configuration options and REST API endpoints.
> [!IMPORTANT]
> This page is the source of truth for configuration precedence, settings, and REST API behavior.
> For installation and setup flows, use [Getting Started](getting-started.md).
> For operations, webhooks, and troubleshooting, use [Guides & Troubleshooting](guides.md).
## Contents
- [Configuration Priority](#configuration-priority)
- [Media Servers](#media-servers)
- [Processing Options](#processing-options)
- [Environment Variables](#environment-variables)
- [Web Interface Settings](#web-interface-settings)
- [Webhook Settings](#webhook-settings)
- [Path Mappings](#path-mappings)
- [REST API](#rest-api)
- [WebSocket Events](#websocket-events)
- [Rate Limiting](#rate-limiting)
## Related Docs
- [Getting Started](getting-started.md)
- [Guides & Troubleshooting](guides.md)
- [FAQ](faq.md)
- [Main README](../README.md)
---
## Configuration Priority
**settings.json** (at `/config/settings.json`) is the sole source of truth for application configuration. On first start, environment variables are migrated into settings.json as seed values. After that, all configuration is managed via the **Web UI** (Setup Wizard and Settings page).
**Infrastructure environment variables** remain active and are not migrated (see [Infrastructure Variables](#infrastructure-variables)).
---
## Media Servers
Every server the app talks to — any number of Plex, Emby, and Jellyfin entries
— is stored as an array under `media_servers` in `settings.json`. Managed from
the **Servers** page (or via the REST API below). Each entry has the shape:
```json
{
"id": "plex-household",
"type": "plex",
"name": "Household Plex",
"url": "http://192.168.1.100:32400",
"enabled": true,
"auth": {
"method": "token",
"token": "..."
},
"libraries": [
{"id": "1", "name": "Movies", "enabled": true},
{"id": "2", "name": "TV Shows", "enabled": true}
],
"path_mappings": [
{"remote_prefix": "/data", "local_prefix": "/media", "webhook_prefixes": []}
],
"plex_config_folder": "/plex"
}
```
Per-vendor notes:
| Vendor | `auth.method` values | Extra fields |
|---|---|---|
| Plex | `token` (OAuth is the *acquisition* flow — the result is stored as `auth.token`) | `plex_config_folder` (where BIF bundles are written), `server_identity` (Plex's `clientIdentifier`, used to disambiguate webhooks) |
| Emby | `password`, `api_key` | `auth.user_id`, `auth.access_token` |
| Jellyfin | `password`, `quick_connect`, `api_key` | `auth.user_id`, `auth.access_token` |
> **Runtime state, not persisted.** Jellyfin's Media Preview Bridge plugin
> presence is probed live via `JellyfinServer.check_plugin_installed()` and
> surfaced in the `/previews-readiness` payload — it isn't stored on the
> server entry.
**Legacy flat keys.** Older single-Plex installs had top-level `plex_url`,
`plex_token`, `plex_config_folder`, and `selected_libraries`. These are
migrated into the first enabled Plex entry of `media_servers[]` on first
boot. Reads still work via a compatibility shim, so existing scripts that
query `GET /api/settings` and look at `plex_url` keep working — but new
writes should use `media_servers[]` via `/api/servers`.
> [!TIP]
> Use the Setup Wizard to sign in. Plex OAuth, Jellyfin Quick Connect, and
> Emby username/password exchange all happen through the wizard without you
> pasting tokens by hand. Exactly one "first server" is configured via the
> wizard; add more from the Servers page.
---
## Processing Options
### Per-GPU Configuration (gpu_config)
GPU settings are configured per-GPU in **Settings** → **Processing Options**. Each entry in `gpu_config` has:
| Field | Type | Description |
|-------|------|-------------|
| `device` | string | GPU device identifier (e.g. `/dev/dri/renderD128`) |
| `name` | string | Display name (e.g. "Intel UHD Graphics 630") |
| `type` | string | `nvidia`, `intel`, `amd`, `apple` |
| `enabled` | boolean | Whether this GPU is used for processing |
| `workers` | int | Number of worker threads for this GPU (0–32) |
| `ffmpeg_threads` | int | CPU threads per FFmpeg job on this GPU (0–32, 0 = no limit). Recommended: 2 |
### Other Processing Settings
| Setting | Web UI | Default | Description |
|---------|--------|---------|-------------|
| `cpu_threads` | Yes | `1` | Number of CPU worker threads (0–32) |
| `scan_workers` | Yes | `0` (Auto) | Full-scan only: how many files are checked **in parallel** for an existing preview, independent of the FFmpeg-generation cap (GPU + CPU workers). Checking is light disk I/O and does NOT add FFmpeg/GPU load. `0` = Auto (`max(32, generators)`); an explicit value is bounded to 1–256. Raise it to speed the "skip already-done files" sweep on large libraries; lower it on a single spinning HDD. |
| `thumbnail_quality` | Yes | `4` | Preview quality 1-10 (2=highest) |
| `thumbnail_interval` | Yes | `10` | Interval between preview images (1–60 s). Matches Plex/BIF community convention (see sidecar `-{width}-10.bif` files). |
| `selected_libraries` | Yes | All | Library IDs to process |
| `sort_by` (per-run) | Yes | `newest` | Order items are processed: `newest`, `oldest`, `random`, or empty for Plex's natural order. Set per manual run (New Job modal) or per schedule — not a global setting. |
### Frame Reuse Cache (frame_reuse)
When the same canonical file fires multiple webhooks within the cache TTL (e.g. Sonarr fires immediately, Plex's library.new follows 30 min later), this cache reuses the FFmpeg-extracted frames across siblings instead of re-running FFmpeg. Tuned per-server under **Settings → Performance**:
| Field | Default | Description |
|-------|---------|-------------|
| `enabled` | `true` | Master toggle for cross-server frame reuse |
| `ttl_minutes` | `60` | How long to keep extracted frames in the cache |
| `max_cache_disk_mb` | `2048` | Disk cap for the cache (oldest entries evicted first) |
> [!TIP]
> **Multi-disk libraries (unraid shfs, mergerfs, JBOD):** pick **Random** as the Processing Order on the New Job modal or on a scheduled full-library scan. With alphabetical order, parallel workers tend to read sequential files from the same physical disk; shuffling spreads reads across disks so disk I/O stops being the bottleneck. Webhook jobs and Recently Added scans are unaffected — they touch too few files for ordering to matter.
> [!NOTE]
> When a GPU worker can't process a file (unsupported codec,
> hardware-accelerator error, driver crash), the same worker retries
> on CPU in-place and the UI shows a warning badge with the reason.
> No separate fallback pool is needed — increase `cpu_threads` if you
> want more dedicated CPU concurrency for files that never hit the GPU.
---
## Environment Variables
### Infrastructure Variables (always active)
These are not migrated to settings.json and remain in effect:
| Variable | Default | Description |
|----------|---------|-------------|
| `CONFIG_DIR` | `/config` | Directory for settings.json, auth, schedules |
| `WEB_PORT` | `8080` | Web server port |
| `PUID` | `1000` | User ID (Unraid: `99`) |
| `PGID` | `1000` | Group ID (Unraid: `100`) |
| `TZ` | Host | Timezone (e.g. `America/New_York`) |
| `CORS_ORIGINS` | `*` | Allowed CORS origins (comma-separated) |
| `HTTPS` | `false` | Enable HTTPS for cookies |
| `DEV_RELOAD` | `false` | Enable Flask auto-reload (development) |
| `WEB_AUTH_TOKEN` | Auto-generated | Fixed authentication token (overrides wizard-set token) |
| `AUTH_METHOD` | `internal` | Set to `external` to disable built-in auth when using a reverse proxy or VPN (see below) |
| `FLASK_SECRET_KEY` | Auto-generated | Override the Flask session signing key. Auto-generated and persisted to `/config/flask_secret.key` if not set. Set this only when you need a fixed key across rebuilds. |
| `LOG_FORMAT` | `pretty` | Log output format. Set to `json` to emit one JSON object per log line — useful when shipping logs to Loki / Datadog / similar aggregators. |
| `PLEX_DATA_ROOT` | `/` | Restricts where Plex data paths can be validated to. Defaults to the whole filesystem; tighten to e.g. `/plex` if you want the path validator to refuse anything outside that root. |
| `MEDIA_ROOT` | `/` | Same as `PLEX_DATA_ROOT` but for media paths. |
| `RATELIMIT_STORAGE_URL` | `memory://` | Backend for rate-limit counters. The default in-memory store is fine for a single-container deploy; set to `redis://host:port/0` if you run behind a load balancer with multiple replicas. |
### External Authentication (AUTH_METHOD)
If you secure access via a reverse proxy (Authelia, Authentik, Caddy Security, nginx basic auth, etc.) or a VPN (Tailscale, WireGuard), you can disable the built-in login screen:
```yaml
environment:
- AUTH_METHOD=external
```
When set to `external`:
- The login page is bypassed; all browser and API requests are treated as authenticated.
- Webhook authentication (`webhook_secret` / Bearer token) is **not** affected — external services like Radarr and Sonarr still need their shared secret.
- The setup wizard still runs on first boot.
- Removing the variable (or setting it back to `internal`) instantly re-enables built-in auth.
> [!CAUTION]
> Only use `AUTH_METHOD=external` when you are certain that network-level access control is in place. Without it, anyone who can reach the web UI has full access.
### Deprecated (no longer used)
These env vars are deprecated and silently ignored at startup with a warning logged. Configure via **Settings** instead:
| Variable | Replacement |
|----------|--------------|
| `GPU_SELECTION` | Per-GPU enable/disable in Settings → Processing Options |
| `GPU_THREADS` | Per-GPU workers in `gpu_config` |
| `FFMPEG_THREADS` | Per-GPU `ffmpeg_threads` in `gpu_config` |
| `PLEX_LIBRARIES` | Per-server library toggles (Settings → Media Servers → Libraries) |
| `REGENERATE_THUMBNAILS` | Tick "Regenerate" when starting a job from the UI |
| `SORT_BY` | Pick sort order when starting a job |
| `NICE_LEVEL` | Removed — process priority is no longer configurable |
| `FALLBACK_CPU_THREADS` | Removed in v3.x — CPU retry now happens in-place inside the GPU worker |
### One-time seed values (migrated on first start)
On first run, these env vars are migrated into settings.json. After that, settings.json is the source of truth:
- `PLEX_URL`, `PLEX_TOKEN`, `PLEX_CONFIG_FOLDER`, `PLEX_VERIFY_SSL`, `PLEX_TIMEOUT`
- `PLEX_BIF_FRAME_INTERVAL` / `THUMBNAIL_INTERVAL` (alias), `THUMBNAIL_QUALITY`, `TONEMAP_ALGORITHM`, `CPU_THREADS`, `SCAN_WORKERS`
- `MEDIA_PATH`, `TMP_FOLDER`, `LOG_LEVEL`
---
## Web Interface Settings
The web UI is served by [gunicorn](https://gunicorn.org/) (a Python web server) using thread workers — Docker handles launching it, you don't need to know about this unless you're running outside Docker. Listening port and related knobs live in [Infrastructure Variables](#infrastructure-variables) — `WEB_PORT`, `CORS_ORIGINS`, `HTTPS`, and `DEV_RELOAD`.
---
## Webhook Settings
Settings for automatic preview generation when media is imported via Radarr or Sonarr.
| Setting | Default | Web UI | Description |
|---------|---------|--------|-------------|
| `webhook_enabled` | `true` | Yes | Master enable/disable for webhook processing |
| `webhook_delay` | `60` | Yes | Delay before processing (10–300 s). Incoming webhooks are queued per source; a batch runs only after this many seconds with no new imports, so every file gets at least this long for Plex to add it before we process. |
| `webhook_secret` | *(empty)* | Yes | Dedicated secret for webhook auth (falls back to API token) |
| `plex_webhook_enabled` | `false` | Yes | Enable the Plex direct webhook (`/api/webhooks/plex`). Requires Plex Pass on the server-owner account. |
| `plex_webhook_public_url` | *(empty)* | Yes | URL Plex Media Server should POST to. Defaults to the URL you registered through. Override for reverse-proxy / split-network setups. |
Webhook processing respects `selected_libraries`; paths outside unchecked libraries are ignored.
The **Recently Added Scanner** is not configured via settings keys any more — it's a first-class schedule type (see [Schedules Endpoints](#schedules-endpoints) below). Create one through the Automation page (Triggers tab) "Create default scanner" shortcut, or through the Schedules tab modal with **Scan mode → Recently added only**.
> [!IMPORTANT]
> The Plex direct webhook and Recently Added schedules trigger only on **new** library items (new `ratingKey`s). They do **not** detect in-place file upgrades — Plex keeps the same item when Sonarr/Radarr replaces a file. Use the existing Sonarr/Radarr webhooks (which fire on `On Upgrade`) for that case.
> [!TIP]
> Configure webhooks on the **Automation** page (`/automation`, Triggers tab) in the web UI. See [Webhook Integration](guides.md#webhook-integration) for setup instructions. The legacy `/webhooks` and `/schedules` URLs still work — they 302-redirect to the relevant tab.
---
## Path Mappings
> [!IMPORTANT]
> Essential for Docker deployments where your media server sees files at
> different paths than this container does. Path mappings are stored
> **per-server** — each Plex / Emby / Jellyfin entry in `media_servers[]`
> carries its own list — because different servers can mount the same media
> at different paths.
### Why Path Mappings?
| Component | Sees files at |
|-----------|---------------|
| Media server (Plex / Emby / Jellyfin) | `/data/media/Movies/film.mkv` |
| This Container | `/media/Movies/film.mkv` |
Without mapping, you'll see "Skipping as file not found" errors.
### Configuration (Web UI)
Open **Servers → Edit** on the server that needs mapping, and add rows in the
Path Mappings section. Each row has:
- **Path on server** — The folder path the media server reports for the file
(e.g. `/data`). Called `remote_prefix` in the API.
- **Path in this app** — The folder path this app uses for the same files
(e.g. `/mnt/data`). Called `local_prefix` in the API.
- **Webhook path (if different)** — Only needed when Sonarr, Radarr, Tdarr,
etc. use a different path than the media server (e.g. they use `/data`
while Plex uses `/data_disk1`). Leave blank if they match. Called
`webhook_prefixes` in the API.
Add as many rows as you need (e.g. one per disk when the server has multiple
roots). Each server manages its own list independently.
### Legacy env (semicolon pair)
| Variable | Description |
|----------|-------------|
| `PLEX_VIDEOS_PATH_MAPPING` | Path(s) as Plex sees them; semicolon-separated for multiple roots (seed value, first-boot only) |
| `PLEX_LOCAL_VIDEOS_PATH_MAPPING` | Path as this app sees them (seed value, first-boot only) |
The saved `path_mappings` on each `media_servers[]` entry take precedence.
Existing semicolon-based values are converted into mapping rows on the first
enabled Plex entry at migration time.
### When a server uses multiple roots (e.g. mergerfs)
If a server has several roots (e.g. `/data_disk1`, `/data_disk2`) but
Sonarr/Radarr see one path (`/data`):
- Add one row per server root, each with the same **Path in this app** (e.g. `/data`).
- In **Webhook path**, enter `/data` on one of the rows so imports from
Sonarr/Radarr still match.
### Examples
| Situation | Path on server | Path in this app | Webhook path |
|-----------|----------------|------------------|--------------|
| Different paths in Docker | `/data` | `/mnt/data` | *(blank)* |
| Multiple disks, Sonarr sees one path | `/data_disk1` | `/data` | `/data` |
| Same (second disk) | `/data_disk2` | `/data` | *(blank)* |
### How to Find Your Paths
1. **Plex path**: Plex Web → Settings → Libraries → Edit → Folders.
2. **Emby path**: Emby Dashboard → Libraries → Edit → Folders.
3. **Jellyfin path**: Jellyfin Dashboard → Libraries → Edit → Folders.
4. **Container path**: Check your `-v` volume mount.
### No Mapping Needed
If both Plex and this container see files at the same path (e.g., both use `/media`), skip this configuration.
### Exclude Paths
Under the same **Media path mapping** settings you can add **Exclude paths**: paths or folders to skip for preview generation. These are applied to the **local** path (as this app sees the file after path mapping).
- **Path prefix** — Any file under this folder is skipped (e.g. `/mnt/media/archive` skips everything under that path).
- **Regex** — The full local path is matched against the pattern (e.g. `.*\.iso$` to skip ISO files).
Add one row per path or pattern. Excluded items are not queued for full-library runs and are skipped for webhook-triggered runs.
---
## REST API
All API endpoints (except `/api/health` and `/api/setup/status`) require authentication.
### Authentication
Include the authentication token in requests using one of these methods:
```bash
# X-Auth-Token header
curl -H "X-Auth-Token: YOUR_TOKEN" http://localhost:8080/api/jobs
# Authorization Bearer header
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8080/api/jobs
```
Get your token from [Authentication Token](getting-started.md#authentication-token), or set a fixed token with `WEB_AUTH_TOKEN`.
### Setup & Settings Endpoints
#### GET /api/setup/status
Check if setup is complete. **No authentication required.**
```json
{
"configured": true,
"setup_complete": true,
"current_step": 0,
"plex_authenticated": true
}
```
#### GET /api/setup/state
Get current setup wizard state.
```json
{
"step": 2,
"data": {
"server_name": "My Plex Server"
}
}
```
#### POST /api/setup/state
Save setup wizard progress.
**Request:**
```json
{
"step": 2,
"data": {
"server_name": "My Plex Server"
}
}
```
#### POST /api/setup/complete
Mark setup as complete. Returns `{"success": true, "redirect": "/"}`.
#### GET /api/setup/token-info
Get information about the current authentication token (used by Step 5 of the setup wizard).
```json
{
"env_controlled": false,
"token": "abc123xyz...",
"token_length": 43,
"source": "config"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `env_controlled` | boolean | Whether token is set via `WEB_AUTH_TOKEN` env var |
| `token` | string | The current authentication token |
| `token_length` | number | Length of the token |
| `source` | string | Either `"environment"` or `"config"` |
#### POST /api/setup/set-token
Set a custom authentication token during setup.
**Request:**
```json
{
"token": "my-custom-password",
"confirm_token": "my-custom-password"
}
```
Returns `{"success": true}` on success, or `{"success": false, "error": "..."}` with details:
- `"Tokens do not match."`
- `"Token must be at least 8 characters long."`
- `"Token is controlled by WEB_AUTH_TOKEN environment variable and cannot be changed."`
#### GET /api/settings
Get current settings.
```json
{
"plex_url": "http://192.168.1.100:32400",
"plex_token": "****",
"plex_name": "My Server",
"plex_config_folder": "/plex",
"selected_libraries": ["1", "2"],
"media_path": "/media",
"plex_videos_path_mapping": "",
"plex_local_videos_path_mapping": "",
"path_mappings": [
{"remote_prefix": "/data", "local_prefix": "/mnt/data", "webhook_prefixes": []}
],
"gpu_config": [
{"device": "/dev/dri/renderD128", "name": "Intel UHD 630", "type": "intel", "enabled": true, "workers": 4, "ffmpeg_threads": 2}
],
"cpu_threads": 2,
"thumbnail_interval": 10,
"thumbnail_quality": 4
}
```
> **path_mappings keys**: `remote_prefix` is the canonical key as of the multi-server refactor (works for Plex, Emby, and Jellyfin). The legacy `plex_prefix` is still accepted as an alias on read; new writes should use `remote_prefix`.
#### POST /api/settings
Update settings. Send only the fields to change.
```json
{
"gpu_config": [{"device": "/dev/dri/renderD128", "enabled": true, "workers": 4, "ffmpeg_threads": 2}],
"cpu_threads": 2,
"thumbnail_interval": 10,
"plex_url": "http://192.168.1.100:32400"
}
```
### Plex OAuth Endpoints
#### POST /api/plex/auth/pin
Create a new Plex OAuth PIN.
```json
{
"id": 12345,
"code": "ABCD1234",
"auth_url": "https://app.plex.tv/auth#?clientID=...&code=ABCD1234"
}
```
#### GET /api/plex/auth/pin/{id}
Check if PIN has been authenticated. Returns `{"authenticated": true, "auth_token": "..."}` or `{"authenticated": false, "auth_token": null}`.
#### GET /api/plex/servers
Get list of user's Plex servers.
```json
{
"servers": [
{
"name": "My Server",
"machine_id": "abc123",
"host": "192.168.1.100",
"port": 32400,
"ssl": false,
"owned": true,
"local": true
}
]
}
```
#### GET /api/plex/libraries
Get libraries from connected Plex server. Optional query parameters: `url`, `token`.
```json
{
"libraries": [
{ "id": "1", "name": "Movies", "type": "movie" },
{ "id": "2", "name": "TV Shows", "type": "show" }
]
}
```
#### POST /api/plex/test
Test Plex connection. Request: `{"url": "...", "token": "..."}`. Returns `{"success": true, "server_name": "...", "version": "..."}`.
### Processing state (global pause)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/processing/state` | Get global processing pause state |
| POST | `/api/processing/pause` | Set global pause (no new jobs start; active job stops dispatch after current tasks) |
| POST | `/api/processing/resume` | Clear global pause |
**GET /api/processing/state** — Response: `{"paused": true}` or `{"paused": false}`. State is persisted and survives restarts.
**POST /api/processing/pause** — Response: `{"paused": true}`.
**POST /api/processing/resume** — Response: `{"paused": false}`.
### Jobs Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/jobs` | List all jobs |
| POST | `/api/jobs` | Create new job |
| GET | `/api/jobs/{id}` | Get job details |
| POST | `/api/jobs/{id}/cancel` | Cancel job |
| POST | `/api/jobs/{id}/pause` | Global pause (delegates to `/api/processing/pause`) |
| POST | `/api/jobs/{id}/resume` | Global resume (delegates to `/api/processing/resume`) |
| DELETE | `/api/jobs/{id}` | Delete job |
#### GET /api/jobs
```json
{
"jobs": [
{
"id": "job-123",
"status": "running",
"library_id": "1",
"library_name": "Movies",
"progress": 45,
"total_items": 100,
"completed_items": 45,
"created_at": "2024-01-15T10:30:00Z",
"started_at": "2024-01-15T10:30:05Z"
}
]
}
```
#### POST /api/jobs
**Request:** `{"library_id": "1", "library_name": "Movies"}`
**Response:** `{"id": "job-123", "status": "pending", "message": "Job created successfully"}`
#### GET /api/jobs/{id}
```json
{
"id": "job-123",
"status": "running",
"library_id": "1",
"library_name": "Movies",
"progress": 45,
"total_items": 100,
"completed_items": 45,
"failed_items": 0,
"created_at": "2024-01-15T10:30:00Z",
"started_at": "2024-01-15T10:30:05Z",
"workers": [
{
"id": 0,
"type": "gpu",
"status": "working",
"current_item": "Movie Title"
}
]
}
```
### Schedules Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/schedules` | List schedules |
| POST | `/api/schedules` | Create schedule |
| PUT | `/api/schedules/{id}` | Update schedule |
| DELETE | `/api/schedules/{id}` | Delete schedule |
| POST | `/api/schedules/{id}/run` | Run now |
#### POST /api/schedules
**Cron request — full library scan (default):**
```json
{
"name": "Nightly Movies",
"library_id": "1",
"cron_expression": "0 2 * * *"
}
```
**Interval request — full library scan:**
```json
{
"name": "Every 4 Hours",
"library_id": "1",
"interval_minutes": 240
}
```
**Recently Added scanner schedule:**
```json
{
"name": "Recently Added Scanner",
"library_id": null,
"interval_minutes": 15,
"enabled": true,
"config": {
"job_type": "recently_added",
"lookback_hours": 1
}
}
```
`config.job_type` accepts:
- `"full_library"` *(default — optional, omit to get the same behaviour)* — schedule runs a full library scan via the standard job pipeline, processing every item in `library_id` that's missing previews.
- `"recently_added"` — schedule runs a Recently Added scan instead. Requires `config.lookback_hours` (float, clamped to 0.25–720). Scans only items added within the lookback window (Plex `addedAt`, Emby/Jellyfin `DateCreated`), queuing each through the webhook job pipeline. When `library_id` is `null`, the scan falls back to the globally selected libraries in Settings (or every supported library when no global filter is set); when set, only that section is scanned. Works for Plex, Emby, and Jellyfin — each vendor's processor implements `scan_recently_added` against its native API.
### System Endpoints
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/api/health` | No | Health check |
| GET | `/api/system/status` | Yes | System status (GPUs, workers, job counts) |
| GET | `/api/system/config` | Yes | Current configuration |
| GET | `/api/libraries` | Yes | Aggregated library list across every configured server |
### Multi-Media-Server Endpoints
For full design and per-vendor details see [Multi-Media-Server](multi-server.md).
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/servers` | List configured servers (auth redacted) |
| POST | `/api/servers` | Add a new server (auto-generates id) |
| GET | `/api/servers/` | Fetch one server (auth redacted) |
| PUT/PATCH | `/api/servers/` | Update; redacted auth values are kept |
| DELETE | `/api/servers/` | Remove a server |
| POST | `/api/servers/test-connection` | Test a candidate config without saving |
| POST | `/api/servers//refresh-libraries` | Re-fetch the server's library list |
| GET | `/api/servers/owners?path=...` | Diagnose which servers own a given path |
| GET | `/api/servers//output-status?path=...&item_id=...` | Whether publisher output files exist for a path on this server. `item_id` is required for **Plex** servers (the bundle hash is keyed by item id); optional for Emby and Jellyfin. Plex requests without `item_id` return `{"needs_item_id": true}`. |
| POST | `/api/servers/auth/emby/password` | Username+password → Emby token |
| POST | `/api/servers/auth/jellyfin/password` | Username+password → Jellyfin token |
| POST | `/api/servers/auth/jellyfin/quick-connect/initiate` | Begin Quick Connect ceremony |
| POST | `/api/servers/auth/jellyfin/quick-connect/poll` | Poll for approval |
| POST | `/api/servers/auth/jellyfin/quick-connect/exchange` | Exchange approved secret for token |
| GET | `/api/servers//health-check` | Per-server settings audit. Returns `{vendor, issues, issue_count, fixable_count}`; `issues[]` carries `{flag, label, severity, current, recommended, rationale, library_id, library_name, fixable}`. Works for Plex (server-wide prefs via `/:/prefs`), Emby and Jellyfin (per-library `LibraryOptions`). Replaces the older Jellyfin-only `/jellyfin/trickplay-status` route. |
| POST | `/api/servers//health-check/apply` | Apply settings to one or more flags. Three body shapes (all backwards-compatible): `{}` = fix every issue at recommended value; `{"flags": ["FlagName", ...]}` = fix only named flags toward recommended; `{"set": [{"flag": "X", "value": true\|false, "library_ids": ["id"]\|null}]}` = set each flag to the EXPLICIT value (enables disable-direction toggles on the Previews readiness card). Returns `{ok, results}` keyed `:` (or `:` for server-wide prefs). |
| GET | `/api/servers//previews-readiness` | Unified readiness payload for every vendor. Returns `{vendor, overall_ok, sections: [{id, title, docs_anchor, ok, severity, checks: [{id, label, docs_anchor, tooltip, ok, severity, current, recommended, actions: {enable?, disable?}, reason, meta}]}]}`. Drives the unified Previews readiness card on the Edit Server modal. See the [Previews readiness guide](guides/previews-readiness.md). |
| POST | `/api/servers//install-plugin` | Jellyfin only. Adds the Media Preview Bridge manifest URL to Jellyfin's plugin repos, queues the package install, and restarts Jellyfin. Returns `{ok, steps: [{step, ok, detail}], error}`. |
| POST | `/api/servers//uninstall-plugin` | Jellyfin only. Removes the Media Preview Bridge plugin (`DELETE /Packages/{GUID}`; 404 treated as success — already gone) and restarts Jellyfin. Repo URL stays in place for possible re-install. Same response shape as `/install-plugin`. |
| GET | `/api/bif/servers//search?q=` | Multi-server BIF Viewer search; returns `preview_kind` (`bif` or `trickplay`) per result so the viewer renders the right format |
| GET | `/api/bif/trickplay/info?server_id=...&path=...` | Parse a Jellyfin trickplay manifest + report sheet metadata |
| GET | `/api/bif/trickplay/frame?server_id=...&sheets_dir=...&index=N&tile_width=10&tile_height=10` | Slice and serve a single thumbnail JPEG from a trickplay tile sheet |
### Webhook Endpoints
Inbound webhook endpoints for Radarr/Sonarr/Custom integration. Webhook endpoints accept `X-Auth-Token`, `Authorization: Bearer`, or a configured `webhook_secret`.
> [!TIP]
> The new **universal webhook URL** at `POST /api/webhooks/incoming` auto-detects the vendor (Plex / Emby / Jellyfin / Sonarr / Radarr / templated path) so you only need one URL across every server. Falls back to per-server URLs at `POST /api/webhooks/server/` for ambiguous setups (rare). See [Multi-Media-Server — Webhook configuration](multi-server.md#webhook-configuration-per-vendor) for details.
#### POST /api/webhooks/incoming
Universal webhook router. Inspect the request body, classify it as Plex / Emby / Jellyfin / Sonarr / Radarr / generic-`{path: ...}`, and dispatch to every server that owns the resolved canonical path. Works alongside the per-vendor URLs below — you can keep using those, or replace them all with this one.
Returns 200 with the dispatch result (`status`, `kind`, `canonical_path`, `publishers[]`, `frame_count`) on success, 202 with `status: "ignored"` for noise events the router intentionally drops (e.g. Jellyfin `PlaybackStart`), 400 for unrecognised payloads, 401 for bad auth, 413 for payloads above the 1 MiB cap.
#### POST /api/webhooks/server/{server_id}
Same as `/api/webhooks/incoming` but pins dispatch to one configured server. Useful when two configured servers (e.g. Plex + Jellyfin) own the same path and the source can't tell them apart — the URL itself carries the disambiguation. Returns 404 when the server id isn't configured.
#### POST /api/webhooks/radarr
Receive a Radarr webhook payload.
**Download event request:**
```json
{
"eventType": "Download",
"movie": {
"title": "Inception",
"folderPath": "/movies/Inception (2010)"
}
}
```
**Response (202):** `{"success": true, "message": "Processing queued for 'Inception'"}`
**Test event:** `{"eventType": "Test"}` → **Response (200):** `{"success": true, "message": "Radarr webhook configured successfully"}`
#### POST /api/webhooks/sonarr
Same authentication and response patterns as Radarr.
**Download event request:**
```json
{
"eventType": "Download",
"series": { "title": "Breaking Bad" },
"episodeFile": { "relativePath": "Season 01/S01E01.mkv" }
}
```
#### POST /api/webhooks/custom
Receive a custom webhook payload from any external tool (Tdarr, scripts, etc.). Accepts one or more file paths to process.
**Single file request:**
```json
{
"file_path": "/media/movies/Movie (2024)/Movie.mkv"
}
```
**Multiple files request:**
```json
{
"file_paths": [
"/media/tv/Show/Season 01/S01E01.mkv",
"/media/tv/Show/Season 01/S01E02.mkv"
],
"title": "Optional display label"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file_path` | string | One of `file_path` / `file_paths` | Single absolute file path |
| `file_paths` | array of strings | One of `file_path` / `file_paths` | Multiple absolute file paths |
| `title` | string | No | Display label for history/jobs |
| `eventType` | string | No | Set to `"Test"` to verify connectivity |
**Response (202):** `{"success": true, "message": "Processing queued for 1 file"}`
**Test event:** `{"eventType": "Test"}` → **Response (200):** `{"success": true, "message": "Custom webhook configured successfully"}`
**Error (400):** `{"success": false, "error": "Payload must include 'file_path' (string) or 'file_paths' (array of strings)"}`
#### POST /api/webhooks/plex
Receive a native Plex webhook (Plex Pass feature). Plex POSTs `multipart/form-data` with a `payload` part containing the JSON event body. Only `library.new` events trigger work; other events (`media.play`, `media.rate`, `library.on.deck`, etc.) are acknowledged with 200 and ignored.
The endpoint also accepts a synthetic `test.ping` event used by the **Test reachability** button on the Automation page (Triggers tab).
**`library.new` payload (excerpt):**
```json
{
"event": "library.new",
"owner": true,
"Metadata": {
"ratingKey": "153037",
"type": "movie",
"title": "Some Movie",
"Media": [{ "Part": [{ "file": "/data/movies/Some Movie/Some Movie.mkv" }] }]
}
}
```
When `Media[].Part[].file` is missing from the payload (Plex doesn't always include it), the app fetches the item by `ratingKey` via the Plex API to recover the file paths.
**Authentication:** same as the other webhook endpoints — `X-Auth-Token` header, `Authorization: Bearer`, or HTTP Basic password.
> [!IMPORTANT]
> Plex's `library.new` webhook is wired through the same code path as mobile push notifications. If push notifications are disabled on your Plex server, library events are silently dropped — enable them under Plex Web → Settings → General (toggle *Enable mobile push notifications*). See the [Auto-trigger from Plex guide](guides.md#auto-trigger-from-plex-no-sonarrradarr) for full details.
#### POST /api/settings/plex_webhook/register
Register the Plex direct webhook (`/api/webhooks/plex`) with the user's plex.tv account, using the configured Plex token.
**Request body:**
```json
{ "public_url": "http://your-host:8080/api/webhooks/plex" }
```
`public_url` is optional — when omitted the server uses `:///api/webhooks/plex`.
**Response (200):** `{"success": true, "registered_in_plex": true, "public_url": "..."}`
**Errors:**
- `400` — token missing
- `403` — Plex Pass required (`reason: "plex_pass_required"`)
- `502` — registration call to plex.tv failed
#### POST /api/settings/plex_webhook/unregister
Remove the Plex direct webhook from the user's plex.tv account and turn off the local toggle. Returns `{"success": true, "registered_in_plex": false}`.
#### GET /api/settings/plex_webhook/status
Probe live state. Returns the configured public URL, whether it is currently registered with Plex, and Plex Pass detection.
```json
{
"enabled_in_settings": true,
"registered_in_plex": true,
"public_url": "http://your-host:8080/api/webhooks/plex",
"default_url": "http://your-host:8080/api/webhooks/plex",
"has_plex_pass": true,
"error": null,
"error_reason": null
}
```
#### POST /api/settings/plex_webhook/test
Self-POST a synthetic `test.ping` payload to the configured public URL to verify reachability. The receiving endpoint records a "test" history entry. Returns `{"success": true, "status_code": 200, ...}` on success.
To run a Recently Added scan immediately, call `POST /api/schedules//run` on the scanner schedule — it's a standard user schedule now, not a dedicated settings endpoint.
#### GET /api/webhooks/history
Get recent webhook events (newest first, max 100). For events with `status: "triggered"` (a debounced batch that was processed), the response may include `job_id`, `path_count`, and `files_preview` (up to 20 basenames) so the UI can show which files were in the batch. File lists are also available on the Dashboard job queue (expand with the chevron next to "Sonarr: N files" / "Radarr: N files" / "Custom: N files") and on the Automation page (Triggers tab) Activity Log (expand triggered rows).
```json
{
"events": [
{
"timestamp": "2026-02-12T10:30:00+00:00",
"source": "sonarr",
"event_type": "Download",
"title": "sonarr",
"status": "triggered",
"job_id": "abc-123",
"path_count": 3,
"files_preview": ["S01E01.mkv", "S01E02.mkv", "S01E03.mkv"]
}
]
}
```
#### DELETE /api/webhooks/history
Clear all webhook history. Returns `{"success": true}`.
### Error Responses
All errors follow this format:
```json
{
"error": "Error message",
"code": "ERROR_CODE"
}
```
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `UNAUTHORIZED` | 401 | Missing or invalid authentication token |
| `NOT_FOUND` | 404 | Resource not found |
| `VALIDATION_ERROR` | 400 | Invalid request data |
| `SERVER_ERROR` | 500 | Internal server error |
---
## WebSocket Events
The dashboard uses Flask-SocketIO with WebSocket for real-time updates. The client connects to the `/jobs` namespace.
```javascript
const socket = io('/jobs', {
transports: ['websocket', 'polling'],
reconnection: true
});
```
| Event | Description |
|-------|-------------|
| `job_progress` | Job progress update |
| `job_complete` | Job finished |
| `job_error` | Job failed |
| `worker_update` | Worker status change |
Example payload:
```json
{
"event": "job_progress",
"data": {
"job_id": "job-123",
"progress": 50,
"completed": 50,
"total": 100,
"current_item": "Movie Title"
}
}
```
---
## Complete Endpoint Index
The sections above cover the endpoints most integrations need. This index
catalogues the remaining routes — mostly internal APIs the web UI calls, but
documented here so you can drive them from scripts if you want. All require
the same `X-Auth-Token` / `Authorization: Bearer` auth as the rest of the API
unless noted.
### Auth & token management
| Method | Endpoint | Description |
|---|---|---|
| GET | `/api/auth/status` | Session auth state — used by the UI on page load |
| POST | `/api/auth/login` · `/api/auth/logout` | Session login/logout (cookie-based) |
| POST | `/api/token/regenerate` | Rotate the stored API token (disabled when `WEB_AUTH_TOKEN` is set) |
| POST | `/api/token/set` | Set a custom token — min 8 chars; returns `{success: false, error: ...}` on validation failure |
### Jobs (beyond the basics in [Jobs Endpoints](#jobs-endpoints))
| Method | Endpoint | Description |
|---|---|---|
| POST | `/api/jobs/manual` | Submit one or more absolute paths — `{"file_paths": ["/a.mkv", "/tv/Show"], "force_regenerate": false, "priority": 2, "server_id": "..."}`. Directories are expanded to the video files inside; bypasses library scan. |
| GET | `/api/media/search` | Backs the Manual Generation typeahead. `?q=` (min 2 chars), optional `?server_id=` to scope to one server. Fans across enabled servers and returns `{results: [{kind: "show"\|"movie"\|"episode", title, year, paths: [local container paths], child_count, servers: [{id, name, type}]}]}`. Shows resolve to their folder(s); the same item reported by several servers is merged into one row (union of paths + servers). |
| POST | `/api/jobs/{id}/priority` | Change a pending/running job's priority (`{"priority": 1\|2\|3}`; 1 = high) |
| POST | `/api/jobs/{id}/reprocess` | Re-run a finished job with the same config |
| POST | `/api/jobs/{id}/retry-now` | Skip the retry back-off on a chain-head job whose next attempt is currently in the back-off countdown. Returns 200 + `{"fired": true, ...}` on success, 409 when no retry is pending, 400 if the job isn't a chain head. |
| POST | `/api/jobs/{id}/fire-webhook-now` | Skip the debounce window on a webhook-batch job that's still waiting to dispatch. Looks up the in-memory batch by `job_id` and cancels its threading timer, then dispatches the same callback synchronously. 202 on success, 404 when the job has no live pending batch (already fired, never had one, or container restart cleared the in-memory dict). |
| GET | `/api/jobs/{id}/logs` | Paginated log stream — `?offset=&limit=` (limit capped at 5000); or legacy `?last=N` for the tail |
| GET | `/api/jobs/{id}/files` | Per-file outcomes — paginated `?page=&per_page=` (per_page capped at 500), plus optional `?outcome=` and `?search=` filters. The underlying per-job JSONL is itself soft-capped at 5000 rows; past that, a `truncated` marker row appears and aggregate counts remain in `progress.outcome`. |
| POST | `/api/jobs/clear` | Delete completed/failed jobs from the queue |
| GET | `/api/jobs/stats` | Totals grouped by status |
| GET | `/api/jobs/workers` | Current worker-pool snapshot (type, state, current item) |
| POST | `/api/workers/add` · `/api/workers/remove` | Spawn/shutdown pool workers live (`{"type": "gpu"\|"cpu", "count": N}`) |
| POST | `/api/jobs/{id}/workers/add` · `/api/jobs/{id}/workers/remove` | Per-job worker adjustment while the job runs |
### Schedules
| Method | Endpoint | Description |
|---|---|---|
| POST | `/api/schedules/{id}/enable` · `/api/schedules/{id}/disable` | Toggle a schedule without deleting it |
| GET · POST | `/api/quiet-hours` | Read / write the multi-window quiet-hours policy (days of week + start/stop times) |
### Settings & setup
| Method | Endpoint | Description |
|---|---|---|
| PUT | `/api/settings/log-level` | Change runtime log verbosity (`{"level": "DEBUG"\|"INFO"\|...}`) |
| POST | `/api/settings/validate-local-path` | Pre-flight a mount/volume path before saving (exists + readable) |
| POST | `/api/settings/validate-plex-config-folder` | Pre-flight a Plex config folder (looks for `Cache/Media/Metadata`) |
| GET | `/api/settings/backups` | List rolling settings.json backup snapshots |
| POST | `/api/settings/backups/restore` | Restore a prior settings.json snapshot |
| POST | `/api/setup/skip` | Skip the setup wizard (advanced — saves `setup_complete=true` with minimal state) |
| POST | `/api/setup/validate-paths` | Pre-flight wizard path fields in bulk |
### Servers (beyond the basics in [Multi-Media-Server Endpoints](#multi-media-server-endpoints))
| Method | Endpoint | Description |
|---|---|---|
| POST | `/api/servers/{id}/test-connection` | Re-test a saved server's live connection |
| PATCH | `/api/servers/{id}/enabled` | Enable/disable a server entry without deleting (`{"enabled": true\|false}`) |
| POST | `/api/servers/{id}/vendor-extraction` | Toggle vendor-side preview generation (Plex `enableBIFGeneration`, Emby/Jellyfin trickplay extraction) |
| GET | `/api/servers/{id}/vendor-extraction/status` | Current aggregate state (e.g. "stopped on 3/5 libraries") |
| GET | `/api/servers/{id}/trickplay-readiness` | *(Jellyfin)* Legacy audit endpoint — kept for scripts. New integrations should use [`/previews-readiness`](#multi-media-server-endpoints). |
| POST | `/api/servers/{id}/trickplay-fix-all` | *(Jellyfin)* Apply all recommended trickplay flags |
### Webhooks (beyond the basics in [Webhook Endpoints](#webhook-endpoints))
| Method | Endpoint | Description |
|---|---|---|
| POST | `/api/webhooks/sportarr` | Sonarr-compatible feed for [Sportarr](https://github.com/Sportarr/Sportarr) (falls back to flat `filePath`) |
| GET | `/api/webhooks/pending` | Batches currently debouncing — per-source key, countdown, and queued paths |
| POST | `/api/webhooks/pending/{debounce_key}/fire-now` | Skip the debounce timer and dispatch the batch immediately |
### System & diagnostics
| Method | Endpoint | Description |
|---|---|---|
| GET | `/api/system/timezone` | Container TZ + source (env var vs `/etc/localtime`) |
| GET | `/api/system/media-servers` | Multi-server aggregate health (per-vendor connection + library counts) |
| GET | `/api/system/vulkan` | Vulkan ICD probe result (device, driver version, ICD files loaded) |
| GET | `/api/system/vulkan/debug` | Plain-text diagnostic bundle for attaching to GitHub issues (DV Profile 5 troubleshooting) |
| POST | `/api/system/rescan-gpus` | Re-probe all GPUs (refreshes `gpu_config` candidate list) |
| GET | `/api/system/version` | App version + commit SHA + build date |
| GET | `/api/system/browse` | Folder picker: lists sub-directories of `?path=` (default `/`). `?include_files=1` also returns video files (each entry has `is_dir`); `?show_hidden=1` includes dot-entries. System dirs (`/proc`, `/sys`, …) are denied. |
| GET | `/api/system/notifications` | In-app notification list (health checks, deprecations, warnings) |
| POST | `/api/system/notifications/{id}/dismiss` | Session-only dismiss |
| POST | `/api/system/notifications/{id}/dismiss-permanent` | Persistent dismiss (stored in settings) |
| POST | `/api/system/notifications/reset-dismissed` | Clear all permanent dismissals |
| GET | `/api/system/whats-new` | Release-notes viewer payload (version + changes since last-seen) |
| POST | `/api/system/whats-new/dismiss` | Mark the current version's notes as seen |
| GET | `/api/system/browse` | Safe filesystem browser, scoped to `MEDIA_ROOT` / `PLEX_DATA_ROOT` (used by path pickers) |
| GET | `/api/logs/history` | Persisted log history — `?limit=` (default 500, max 2000), `?level=` (minimum level filter), `?before=` (ISO-8601 timestamp cursor for older-than paging) |
---
## Rate Limiting
| Endpoint | Limit |
|----------|-------|
| `POST /login` | 5 per minute |
| `POST /api/auth/login` | 10 per minute |
| Default | 200 per day, 50 per hour |
Rate limit headers are included in responses:
- `X-RateLimit-Limit`
- `X-RateLimit-Remaining`
- `X-RateLimit-Reset`
---
## Next Steps
- Complete install and setup in [Getting Started](getting-started.md)
- Use operational workflows in [Guides & Troubleshooting](guides.md)
---
[Back to Docs](README.md) | [Main README](../README.md)