WatchState HTTP API reference. Examples use the default /v1/api prefix.
- HTTP API Documentation
- Table of Contents
- Authentication
- Global Notes
- Endpoints
- Backends
- GET /v1/api/backends
- POST /v1/api/backends
- GET /v1/api/backends/spec
- GET|POST /v1/api/backends/uuid/{type}
- GET|POST /v1/api/backends/users/{type}
- GET|POST /v1/api/backends/discover/{type}
- POST /v1/api/backends/accesstoken/{type}
- POST /v1/api/backends/validate/token/{type}
- POST /v1/api/backends/plex/generate
- POST /v1/api/backends/plex/check
- Configured Backend Endpoints
- GET /v1/api/backend/{name}
- PUT /v1/api/backend/{name}
- PATCH /v1/api/backend/{name}
- DELETE /v1/api/backend/{name}
- GET /v1/api/backend/{name}/info
- GET /v1/api/backend/{name}/version
- GET /v1/api/backend/{name}/users
- POST /v1/api/backend/{name}/accesstoken
- GET /v1/api/backend/{name}/sessions
- GET /v1/api/backend/{name}/discover
- GET /v1/api/backend/{name}/library
- POST|DELETE /v1/api/backend/{name}/library/{id}
- GET|POST|PATCH|DELETE /v1/api/backend/{name}/option[/{option}]
- GET /v1/api/backend/{name}/search[/{id}]
- GET /v1/api/backend/{name}/unmatched[/{id}]
- GET /v1/api/backend/{name}/mismatched[/{id}]
- GET /v1/api/backend/{name}/stale/{id}
- DELETE /v1/api/backend/{name}/stale/{id}
- GET /v1/api/backend/{name}/ignore
- POST /v1/api/backend/{name}/ignore
- DELETE /v1/api/backend/{name}/ignore
- History
- Ignore Rules
- Logs
- Player Streaming
- System
- GET /v1/api/system/healthcheck
- GET /v1/api/system/version
- GET /v1/api/system/supported
- GET /v1/api/system/auth/test
- GET /v1/api/system/auth/has_user
- GET /v1/api/system/auth/user
- POST /v1/api/system/auth/signup
- POST /v1/api/system/auth/login
- PUT /v1/api/system/auth/change_password
- DELETE /v1/api/system/auth/sessions
- GET /v1/api/system/env
- GET /v1/api/system/env/{key}
- POST|DELETE /v1/api/system/env/{key}
- GET /v1/api/system/guids
- GET /v1/api/system/guids/custom
- PUT /v1/api/system/guids/custom
- DELETE /v1/api/system/guids/custom/{id}
- GET /v1/api/system/guids/custom/{client}
- PUT /v1/api/system/guids/custom/{client}
- DELETE /v1/api/system/guids/custom/{client}/{id}
- GET /v1/api/system/guids/custom/{client}/{index}
- GET /v1/api/system/events
- GET /v1/api/system/events/stats
- POST /v1/api/system/events
- GET /v1/api/system/events/{id}
- PATCH /v1/api/system/events/{id}
- DELETE /v1/api/system/events/{id}
- DELETE /v1/api/system/events
- POST /v1/api/system/command
- GET /v1/api/system/command/{token}
- GET /v1/api/system/scheduler
- POST /v1/api/system/scheduler/restart
- GET /v1/api/system/report
- GET /v1/api/system/report/ini
- POST /v1/api/system/url/check
- POST /v1/api/system/yaml[/{filename}]
- POST /v1/api/system/sign/{id}
- GET /v1/api/system/static/{file}
- GET /v1/api/system/images/{type}
- GET /v1/api/system/backup
- GET|DELETE /v1/api/system/backup/{filename}
- GET /v1/api/system/processes
- DELETE /v1/api/system/processes/{id}
- DELETE /v1/api/system/cache
- DELETE /v1/api/system/reset
- POST /v1/api/system/reset/opcache
- GET /v1/api/system/integrity
- DELETE /v1/api/system/integrity
- GET /v1/api/system/parity
- DELETE /v1/api/system/parity
- GET /v1/api/system/duplicate
- DELETE /v1/api/system/duplicate
- GET /v1/api/system/suppressor
- POST /v1/api/system/suppressor
- GET /v1/api/system/suppressor/{id}
- PUT /v1/api/system/suppressor/{id}
- DELETE /v1/api/system/suppressor/{id}
- Tasks
- Identities
- GET /v1/api/identities
- POST /v1/api/identities
- DELETE /v1/api/identities/{identity}
- GET /v1/api/identities/{identity}
- PUT /v1/api/identities/{identity}
- GET /v1/api/identities/provision
- PUT /v1/api/identities/provision/mapping
- POST /v1/api/identities/provision
- POST /v1/api/identities/provision/sync-backends
- Webhook
- Backends
- Error Responses
Most routes require either an API key or a signed user token.
-
API key in a header:
X-APIKEY: <api-key> -
API key in the query string:
?apikey=<api-key> -
Signed user token in the
Authorizationheader for most authenticated routes:Authorization: Bearer <token>or
Authorization: Token <token> -
Signed user token in the query string:
?ws_token=<token>
-
Content-Type
- Send
Content-Type: application/jsonfor JSON request bodies. - JSON auto-parsing only happens for
application/jsonandapplication/*+json.
- Send
-
Identity Context
- Many endpoints operate on a per-identity config/database.
- Those routes accept
X-User: <name>or?user=<name>. - If omitted, WatchState uses the
mainidentity context.
-
Response Format
- Successful endpoints usually return a JSON object or JSON array.
- Error responses use:
{ "error": { "code": 400, "message": "Description of the problem" } } - Informational success messages use:
{ "info": { "code": 200, "message": "Human readable message" } }
-
Pagination
- Most paginated endpoints use
pageandperpage. - History, parity, duplicate, and events responses include paging metadata.
- Most paginated endpoints use
-
Raw Backend Responses
- Several backend endpoints accept
raw=true. raw=trueexposes backend-specific upstream payloads and can be much larger than the normalized response.
- Several backend endpoints accept
-
Real-time APIs
- Real-time endpoints use Server-Sent Events (SSE), mainly for logs and command execution.
GET /v1/api/backends and POST /v1/api/backends honor X-User or ?user=. Probe routes work without a saved backend.
Lists configured backends for the current user.
Response:
[
{
"name": "plex_main",
"type": "plex",
"url": "https://plex.example.com",
"uuid": "...",
"user": "owner",
"import": {
"enabled": true,
"lastSync": "2026-03-28T12:00:00+00:00"
},
"export": {
"enabled": false,
"lastSync": null
},
"urls": {
"webhook": "/v1/api/webhook?apikey=..."
}
}
]Notes:
- External responses omit the stored
optionsobject except foroptions.IMPORT_METADATA_ONLY. - The generated webhook URL includes
?apikey=...when secure API mode is enabled.
Creates and persists a new backend definition.
Body:
{
"name": "plex_main",
"type": "plex",
"url": "https://plex.example.com",
"token": "secret-token",
"user": "owner",
"uuid": "optional-server-id",
"import": {
"enabled": true
},
"export": {
"enabled": false
},
"options": {
"client": {
"verify_host": true
}
}
}Response:
{
"name": "plex_main",
"type": "plex",
"url": "https://plex.example.com",
"uuid": "...",
"token": "secret-token",
"...": "saved backend fields"
}Errors:
400 Bad Requestiftype,name,url, ortokenis missing or invalid.404 Not Foundif the user does not exist.409 Conflictif the backend name already exists.
Notes:
- Backend names must use lowercase letters, numbers, and underscores only.
- If
uuidis omitted, WatchState tries to fetch it from the remote backend. - Only option keys defined in
config/servers.spec.phpare stored.
Returns the backend option specification.
Response:
[
{
"key": "options.client.timeout",
"type": "float",
"description": "HTTP timeout in seconds"
}
]Notes:
- The response is generated from
config/servers.spec.php. choicesis included for enumerated fields when the spec defines it.
Probes an arbitrary backend connection and returns its type plus unique identifier.
Path:
type: Backend type such asplex,jellyfin, oremby.
Input:
- Query parameters for
GET, or JSON body forPOST. - Required fields:
url,token - Optional fields:
uuid,user, and selectedoptions.*
Response:
{
"type": "plex",
"identifier": "..."
}Errors:
400 Bad Requestif the backend type, URL, or token is invalid.500 Internal Server Errorif the remote probe fails.
Returns users from an arbitrary backend connection without saving it first.
Path:
type: Backend type.
Input:
- Query parameters for
GET, or JSON body forPOST. - Required fields:
url,token - Optional fields:
tokens- Include backend-specific user tokens when supported.target_user- Narrow the result to a specific backend user.no_cache- Force a fresh fetch.
Response:
[
{
"id": "...",
"name": "...",
"...": "backend user fields"
}
]Errors:
400 Bad Requestif the backend type, URL, or token is invalid.500 Internal Server Errorif the remote backend request fails.
Discovers available Plex servers for an arbitrary Plex connection.
Path:
type: Must beplex.
Input:
- Query parameters for
GET, or JSON body forPOST. - Required fields:
url,token - Optional fields:
options.ADMIN_TOKEN- Plex admin token used during discovery.
Response:
[
{
"name": "...",
"uri": "...",
"...": "Plex discovery fields"
}
]Errors:
400 Bad Requestif the backend type is notplex, or if required connection data is missing.500 Internal Server Errorif discovery fails.
Generates a Jellyfin or Emby access token using username/password credentials.
Path:
type: Must bejellyfinoremby.
Body:
{
"url": "https://jellyfin.example.com",
"username": "alice",
"password": "secret"
}Response:
{
"AccessToken": "...",
"...": "backend-specific token payload"
}Errors:
400 Bad Requestif credentials are missing or the backend type is unsupported.500 Internal Server Errorif token generation fails.
Validates a Plex token.
Path:
type: Must resolve to the Plex client.
Body:
{
"token": "plex-token"
}Response:
{
"info": {
"code": 200,
"message": "Token is valid."
}
}Errors:
400 Bad Requestif the endpoint is used with a non-Plex backend or iftokenis missing.401 Unauthorizedif the token is rejected.
Starts the Plex PIN flow used for browser/device login.
Access:
- Open.
Response:
{
"id": 123456,
"code": "ABCD",
"...": "Plex pin payload"
}Errors:
- Returns the upstream Plex status when the PIN request fails.
Notes:
- The response also includes the WatchState Plex client headers used for the request.
Polls the Plex PIN flow and returns the current PIN state.
Access:
- Open.
Body:
{
"id": 123456,
"code": "ABCD"
}Response:
{
"id": 123456,
"authToken": "...",
"...": "Plex pin status fields"
}Errors:
400 Bad Requestifidorcodeis missing.- Returns the upstream Plex status when the check fails.
These routes operate on a saved backend name and honor X-User or ?user=.
Returns a single saved backend definition.
Path:
name: Saved backend name.
Response:
{
"name": "plex_main",
"type": "plex",
"url": "https://plex.example.com",
"token": "secret-token",
"uuid": "...",
"...": "backend fields"
}Errors:
404 Not Foundif the user or backend does not exist.
Notes:
- This endpoint returns the stored backend object and is not redacted like
GET /v1/api/backends.
Replaces a saved backend configuration and revalidates it.
Path:
name: Saved backend name.
Body:
{
"url": "https://plex.example.com",
"token": "secret-token",
"user": "owner",
"uuid": "...",
"import": {
"enabled": true
},
"export": {
"enabled": false
},
"options": {
"client": {
"timeout": 30
}
}
}Response:
{
"name": "plex_main",
"type": "plex",
"...": "updated backend fields"
}Errors:
400 Bad Requestif validation fails.404 Not Foundif the user or backend does not exist.
Notes:
- Removed legacy keys are stripped automatically before the config is persisted.
- When
import.enabled=true,options.IMPORT_METADATA_ONLYis removed as a sanity check.
Partially updates a saved backend using a raw JSON patch list.
Path:
name: Saved backend name.
Body:
[
{
"key": "options.client.timeout",
"value": 30
},
{
"key": "import.enabled",
"value": true
}
]Response:
{
"name": "plex_main",
"type": "plex",
"...": "updated backend fields"
}Errors:
400 Bad Requestif the body is not valid JSON, if a key is missing, if a key is immutable, or if a value fails validation.404 Not Foundif the user or backend does not exist.
Notes:
- The body must be a JSON array, not an object.
- Immutable keys include
name,type,options,import,export, and removed legacy keys. - This route validates fields against the server spec but does not perform the same remote context validation as
PUT.
Deletes a backend definition and removes its metadata references from history.
Path:
name: Saved backend name.
Response:
{
"deleted": {
"references": 42,
"records": 7
},
"backend": {
"name": "plex_main",
"...": "deleted backend fields"
}
}Errors:
404 Not Foundif the user or backend does not exist.
Notes:
- Metadata and extra blocks for the backend are removed from the
statetable before the backend config is deleted. - Records with no remaining metadata are deleted.
Returns backend info and capabilities.
Path:
name: Saved backend name.
Query:
raw(optional) - Return the backend's raw response.
Response:
{
"...": "backend info payload"
}Errors:
404 Not Foundif the user or backend does not exist.500 Internal Server Errorif the backend request fails.
Returns the backend server version.
Path:
name: Saved backend name.
Response:
{
"version": "..."
}Errors:
404 Not Foundif the user or backend does not exist.500 Internal Server Errorif the backend request fails.
Returns users from a saved backend connection.
Path:
name: Saved backend name.
Query:
tokens(optional) - Include backend-specific tokens when supported.target_user(optional) - Return data for a single backend user.raw(optional) - Include the backend raw response.
Response:
[
{
"id": "...",
"name": "...",
"...": "backend user fields"
}
]Errors:
404 Not Foundif the user or backend does not exist.500 Internal Server Errorif the backend request fails.
Generates a per-user token from a saved backend.
Path:
name: Saved backend name.
Body:
{
"id": "backend-user-id",
"username": "optional-username"
}Response:
{
"token": "...",
"username": "optional-username"
}Errors:
400 Bad Requestifidis missing.404 Not Foundif the user or backend does not exist.500 Internal Server Errorif token generation fails.
Returns active sessions from a saved backend.
Path:
name: Saved backend name.
Query:
raw(optional) - Include the backend raw response.
Response:
[
{
"id": "...",
"user": "...",
"...": "session fields"
}
]Errors:
404 Not Foundif the user or backend does not exist.500 Internal Server Errorif the backend request fails.
Discovers Plex servers using a saved Plex backend configuration.
Path:
name: Saved backend name.
Response:
[
{
"name": "...",
"uri": "...",
"...": "Plex discovery fields"
}
]Errors:
400 Bad Requestif the backend is not Plex.404 Not Foundif the user or backend does not exist.500 Internal Server Errorif discovery fails.
Lists libraries exposed by a saved backend.
Path:
name: Saved backend name.
Response:
[
{
"id": "1",
"name": "Movies",
"supported": true,
"ignored": false
}
]Errors:
404 Not Foundif the user or backend does not exist.500 Internal Server Errorif the backend request fails.
Marks or un-marks a library as ignored in backend config.
Path:
name: Saved backend name.id: Library identifier.
Method Behavior:
POSTmarks the library as ignored.DELETEremoves the ignore flag.
Response:
[
{
"id": "1",
"name": "Movies",
"ignored": true
}
]Errors:
404 Not Foundif the user, backend, or library does not exist.409 Conflictif the library is already in the requested state.
Notes:
- Ignored library IDs are persisted in
options.ignore.
Gets, sets, or deletes a single backend option.
Path:
name: Saved backend name.option(optional) - Option key. You can also send it askey.
Method Behavior:
GETreads an option.POSTandPATCHset an option value.DELETEremoves the stored option value.
Input:
GET: option key in the path or query string.POST,PATCH,DELETE: JSON body or form fields withkeyand optionalvalue.
Response:
{
"key": "options.client.timeout",
"value": 30,
"real_val": "30",
"type": "float",
"description": "HTTP timeout in seconds"
}Errors:
400 Bad Requestif the key is missing, invalid, outside the allowed namespace, or if validation fails.404 Not Foundif the user, backend, or option does not exist.
Notes:
- External callers may only manage keys that start with
options.. - Internal requests can access non-
options.keys. - Boolean parsing accepts common values such as
true,false,on,off,yes, andno.
Searches backend content by backend item ID or free-text query.
Path:
name: Saved backend name.id(optional) - Backend item ID.
Query:
id(optional) - Alternative way to pass the backend item ID.q(optional) - Free-text search query.limit(optional) - Maximum number of results. Defaults to25.raw(optional) - Include raw backend payloads.
Response:
[
{
"id": 123,
"title": "Movie Title",
"type": "movie",
"via": "plex_main",
"webUrl": "...",
"...": "normalized entity fields"
}
]Errors:
400 Bad Requestif neitheridnorqis provided.404 Not Foundif the user or backend does not exist, or if no results are found.500 Internal Server Errorif the backend request fails.
Notes:
- Results are normalized through the shared entity formatter.
- When
raw=true, each item also includes the backend raw response.
Scans one library, or all supported non-ignored libraries, for items without supported GUIDs.
Path:
name: Saved backend name.id(optional) - Library ID. If omitted, WatchState scans every supported non-ignored library.
Query:
timeout(optional) - Override backend timeout.raw(optional) - Include raw backend payloads.
Response:
[
{
"title": "Unknown Item",
"type": "movie",
"webUrl": "...",
"library": "1",
"path": "/media/movies/Unknown Item.mkv"
}
]Errors:
404 Not Foundif the user or backend does not exist.500 Internal Server Errorif the scan fails.
Scans one library, or all supported non-ignored libraries, for likely bad title/path matches.
Path:
name: Saved backend name.id(optional) - Library ID. If omitted, WatchState scans every supported non-ignored library.
Query:
timeout(optional) - Override backend timeout.raw(optional) - Include raw backend payloads.percentage(optional) - Threshold below which a result is returned. Defaults to50.method(optional) -similarityorlevenshtein. Defaults tosimilarity.
Response:
[
{
"title": "Movie Title",
"percent": 32.7,
"matches": [
{
"path": "movie title 2024",
"title": "movie title",
"methods": {
"similarity": 32.7,
"levenshtein": 88.1,
"startWith": false
}
}
],
"webUrl": "...",
"library": "1"
}
]Errors:
400 Bad Requestifmethodis invalid.404 Not Foundif the user or backend does not exist.500 Internal Server Errorif the scan fails.
Compares local mapped records against one remote library and reports stale local references.
Path:
name: Saved backend name.id: Library ID.
Query:
ignore(optional) - Ignore the cached remote library snapshot and rebuild it.timeout(optional) - Override backend timeout.
Response:
{
"backend": {
"name": "plex_main",
"library": {
"id": "1",
"name": "Movies"
}
},
"counts": {
"remote": 1200,
"local": 1220,
"stale": 20
},
"items": [
{
"id": 101,
"title": "Old Record",
"...": "normalized entity fields"
}
]
}Errors:
400 Bad Requestifidis empty.404 Not Foundif the user or backend does not exist.
Accepts a list of stale IDs to remove from the mapper workflow.
Path:
name: Saved backend name.id: Library ID.
Body:
{
"ids": [101, 102, 103]
}Response:
{
"info": {
"code": 200,
"message": "Removed stale references."
}
}Errors:
400 Bad Requestifidis empty or ifidsis missing or empty.404 Not Foundif the user or backend does not exist.
Notes:
- The current implementation validates the request and loads mapper data, but does not yet delete the supplied IDs from storage.
Lists ignore rules that are scoped to one backend.
Path:
name: Saved backend name.
Response:
[
{
"rule": "movie://tmdb:123@plex_main",
"type": "Movie",
"backend": "plex_main",
"db": "tmdb",
"id": "123",
"scoped": "No",
"created": "2026-03-28T12:00:00+00:00"
}
]Errors:
404 Not Foundif the user or backend does not exist.
Adds a backend-scoped ignore rule.
Path:
name: Saved backend name.
Body:
{
"type": "movie",
"db": "tmdb",
"id": "123"
}Alternative Body:
{
"rule": "movie://tmdb:123@plex_main"
}Response:
201 Createdwith an empty body.
Errors:
400 Bad Requestif required parts are missing or the rule is invalid.404 Not Foundif the user or backend does not exist.409 Conflictif the exact rule already exists or if a global rule already exists.
Notes:
- If
scopedis provided, the rule becomes...?id=<scoped>.
Removes a backend-scoped ignore rule.
Path:
name: Saved backend name.
Body:
{
"rule": "movie://tmdb:123@plex_main"
}Response:
200 OKwith an empty body.
Errors:
400 Bad Requestifruleis missing or invalid.404 Not Foundif the user, backend, or rule does not exist.
All history routes honor X-User or ?user=.
Searches and paginates the local history database.
Query:
- Pagination:
page(optional) - Defaults to1.perpage(optional) - Defaults to12.
- Output shaping:
view(optional) - Comma-separated field list. Only the requested fields are returned for each item.with_duplicates(optional) - Include duplicate reference IDs.
- Sorting:
sort(optional, repeatable or array-style) - Sort expressions such asupdated_at:desc.
- Filters:
watchedidviayeartype(movieorepisode)titleseasonepisodeparentinprovider://idformatguidsinprovider://idformatrguidinguid://parentID/seasonNumber[/episodeNumber]formatmetadatawith companionkey,value, and optionalexactextrawith companionkey,value, and optionalexactpathsubtitlegenres
Response:
{
"paging": {
"total": 25,
"perpage": 12,
"current_page": 1,
"first_page": 1,
"next_page": 2,
"prev_page": null,
"last_page": 3
},
"filters": {
"watched": 1
},
"history": [
{
"id": 101,
"title": "Movie Title",
"type": "movie",
"via": "plex_main",
"watched": 1,
"webUrl": "...",
"reported_by": ["plex_main", "jellyfin_main"],
"not_reported_by": [],
"duplicate_reference_ids": []
}
],
"links": {
"self": "/v1/api/history?page=1",
"first_url": "/v1/api/history?page=1",
"next_url": "/v1/api/history?page=2",
"prev_url": null,
"last_url": "/v1/api/history?page=3"
},
"searchable": [
{
"key": "title",
"description": "Search using the title.",
"type": "string"
}
]
}Errors:
400 Bad Requestfor invalidrguid,parent,guids, or JSON field query syntax.404 Not Foundif the user does not exist or if no results match.
Notes:
metadata,extra,path,subtitle, andgenressearches can be slow.- The normalized item format includes extra fields such as
content_path,content_title,content_overview,content_genres,reported_by,not_reported_by, andisTainted.
Returns one local history record.
Path:
id: Numeric local record ID.
Query:
files(optional) - Include media file probes and sidecar subtitles.with_duplicates(optional) - Include duplicate reference IDs.
Response:
{
"id": 101,
"title": "Movie Title",
"type": "movie",
"via": "plex_main",
"content_path": "/media/movies/Movie Title (2024).mkv",
"content_exists": true,
"duplicate_reference_ids": [],
"files": [
{
"path": "/media/movies/Movie Title (2024).mkv",
"source": ["plex_main"],
"ffprobe": {
"streams": [],
"format": {}
},
"subtitles": ["/media/movies/Movie Title (2024).en.srt"]
}
],
"hardware": {
"codecs": [
{
"codec": "libx264",
"name": "H.264 (CPU) (All)",
"hwaccel": false
}
],
"devices": ["/dev/dri/renderD128"]
}
}Errors:
404 Not Foundif the user or item does not exist.
Notes:
files=1performs filesystem checks andffprobecalls, so it is heavier than a normal read.
Returns duplicate local history IDs for the record.
Path:
id: Numeric local record ID.
Response:
{
"duplicate_reference_ids": [102, 103]
}Errors:
404 Not Foundif the user or item does not exist.500 Internal Server Errorif duplicate lookup fails.
Deletes one local history record.
Path:
id: Numeric local record ID.
Response:
200 OKwith an empty body.
Errors:
404 Not Foundif the user or item does not exist.
Reads or changes the watched state of a history record.
Path:
id: Numeric local record ID.
Method Behavior:
GETreturns the current watched flag.POSTmarks the item as watched and queues a sync push.DELETEmarks the item as unwatched and queues a sync push.
GET Response:
{
"watched": true
}POST or DELETE Response:
- Returns the same payload as
GET /v1/api/history/{id}for the updated record.
Errors:
404 Not Foundif the user or item does not exist.409 Conflictif the item is already in the requested watched state.
Notes:
- The route records a
webui.markplayedorwebui.markunplayedevent in the entity extra data.
Validates that each backend reference for a local record still exists remotely.
Path:
id: Numeric local record ID.
Response:
{
"plex_main": {
"id": "12345",
"status": true,
"message": "Item found."
},
"jellyfin_main": {
"id": "67890",
"status": false,
"message": "Item not found."
}
}Errors:
404 Not Foundif the user or item does not exist.
Notes:
- The response includes
X-Cache: HITorX-Cache: MISS. - Results are cached for 10 minutes.
Removes one backend metadata block from a local history record.
Path:
id: Numeric local record ID.backend: Backend name.
Response:
- Returns the updated record payload, or:
{
"info": {
"code": 200,
"message": "Record deleted."
}
}Errors:
404 Not Foundif the user, item, or backend metadata block does not exist.
Notes:
- If the removed metadata block was the last one on the record, the entire local record is deleted.
Proxies a poster or background image for a history record.
Path:
id: Numeric local record ID.type:posterorbackground
Response:
- Binary image stream with headers such as
Content-TypeandX-Via.
Errors:
304 Not ModifiedifIf-Modified-Sinceis present.400 Bad Requestif image fetching fails.404 Not Foundif the user, item, remote item, or requested image is unavailable.
Notes:
- Images are fetched from the item's
viabackend only.
All ignore-rule routes honor X-User or ?user=.
Lists ignore rules for the current user.
Query:
type(optional)db(optional)id(optional)backend(optional)
Response:
[
{
"rule": "movie://tmdb:123@plex_main",
"id": "123",
"type": "Movie",
"backend": "plex_main",
"db": "tmdb",
"title": "Movie Title",
"scoped": false,
"scoped_to": null,
"created": "2026-03-28T12:00:00+00:00"
}
]Errors:
404 Not Foundif the user does not exist.
Adds an ignore rule.
Body:
{
"rule": "movie://tmdb:123@plex_main"
}Alternative Body:
{
"id": "123",
"db": "tmdb",
"backend": "plex_main",
"type": "movie",
"scoped": true,
"scoped_to": 101
}Response:
{
"rule": "movie://tmdb:123@plex_main?id=101",
"id": "123",
"type": "Movie",
"backend": "plex_main",
"db": "tmdb",
"title": "Movie Title",
"scoped": true,
"scoped_to": 101,
"created": "2026-03-28T12:00:00+00:00"
}Errors:
400 Bad Requestif required fields are missing or the rule is invalid.404 Not Foundif the user does not exist.409 Conflictif the rule already exists.
Removes an ignore rule.
Body:
{
"rule": "movie://tmdb:123@plex_main"
}Response:
{
"rule": "movie://tmdb:123@plex_main",
"id": "123",
"type": "Movie",
"backend": "plex_main",
"db": "tmdb",
"title": "Movie Title",
"scoped": false,
"scoped_to": null
}Errors:
400 Bad Requestifruleis missing or invalid.404 Not Foundif the user does not exist or the rule cannot be found.
Lists log, webhook dump, and debug files under WatchState's temp directories.
Response:
[
{
"filename": "access.20260328.log",
"type": "access",
"date": "20260328",
"size": 12345,
"modified": "2026-03-28T12:00:00+00:00"
}
]Returns the most recent lines from today's .log files.
Query:
limit(optional) - Defaults to50.
Response:
[
{
"filename": "access.20260328.log",
"type": "access",
"date": "20260328",
"size": 12345,
"modified": "2026-03-28T12:00:00+00:00",
"lines": [
{
"id": "...",
"item_id": "101",
"user": "main",
"backend": "plex_main",
"date": "2026-03-28T12:00:00+00:00",
"text": "Queuing main@plex_main request ..."
}
]
}
]Errors:
500 Internal Server Errorif the log path is not configured.
Notes:
- Only today's
.logfiles are returned, not older logs or JSON dump files.
Reads, downloads, streams, or deletes a single log, debug, or webhook file.
Path:
filename: File name returned byGET /v1/api/logs.
Query Parameters for GET:
download(optional) - Download the raw file.stream(optional) - Stream the file over SSE.offset(optional) - Reverse-pagination offset from the end of the file.
Normal GET Response:
{
"filename": "access.20260328.log",
"offset": 100,
"next": 200,
"max": 500,
"type": "log",
"lines": [
{
"id": "...",
"item_id": null,
"user": "main",
"backend": "plex_main",
"date": "2026-03-28T12:00:00+00:00",
"text": "Some log line"
}
]
}Stream Response:
Content-Type: text/event-stream; charset=UTF-8- Emits:
dataevents containing JSON formatted log linespingkeepalive events
DELETE Response:
200 OKwith an empty body.
Errors:
404 Not Foundif the file does not exist.
Notes:
- Path traversal is blocked with
realpathand base path checks. download=1returns the raw file stream instead of JSON.- In practice, bad or unresolvable file names usually surface as
404 Not Found;400 Bad Requestonly applies when the route argument itself is missing.
All player routes are open. Access is gated by the playback token in the path. Tokens are created with POST /v1/api/system/sign/{id}.
Builds the top-level HLS playlist for a signed media file.
Path:
token: Short-lived playback token.
Query:
debug(optional) - Enable verbose debug behavior in downstream segment/subtitle routes.sd(optional) - Segment duration. Defaults to6.000seconds.
Response:
Content-Type: application/x-mpegurl- HLS master playlist text with video and subtitle tracks.
Errors:
400 Bad Requestif the token is invalid, the path is empty, or media duration is unavailable.500 Internal Server Errorif playlist generation fails.
Notes:
- Sidecar subtitle files are auto-discovered when no subtitle is already selected.
- Child playlist URLs automatically include
?apikey=when secure API mode is enabled.
Builds the HLS segment playlist for a signed media file.
Path:
token: Short-lived playback token.
Response:
Content-Type: application/x-mpegurl- VOD segment playlist referencing
/v1/api/player/segments/{token}/{segment}.ts.
Errors:
304 Not ModifiedifIf-Modified-Sinceis present.400 Bad Requestif the token is invalid or expired.
Returns one MPEG-TS segment, either direct-played or transcoded with ffmpeg.
Path:
token: Short-lived playback token.segment: Zero-based segment number.type(optional) - Accepted by the route but not used by the segment generator.
Query:
sd(optional) - Override the duration of the final segment.
Response:
Content-Type: video/mpegts- Streaming TS payload.
Headers:
X-Transcode-TimeX-FfmpegandX-Transcode-Configwhen debug mode is enabled
Errors:
304 Not ModifiedifIf-Modified-Sinceis present.400 Bad Requestif the token is invalid, the path is empty, the path is not a file, or required hardware devices are missing.404 Not Foundif the media path or external subtitle path is missing.500 Internal Server Errorif ffprobe or ffmpeg fails.
Notes:
- Segment generation is serialized per playback token with a lock file.
- External subtitles can be burned in.
- Internal text subtitles can be extracted and burned in.
Builds a one-track HLS subtitle playlist.
Path:
token: Short-lived playback token.type: Subtitle type label used in the generated path, typicallywebvtt.source:xfor external subtitle files,ifor internal subtitle streams.index: Subtitle index.
Response:
Content-Type: application/x-mpegurl- HLS subtitle playlist pointing at the subtitle conversion endpoint.
Errors:
304 Not ModifiedifIf-Modified-Sinceis present.400 Bad Requestif the token is invalid, there are no matching subtitles, or the selected subtitle cannot be found.
Converts an external or internal subtitle track to WebVTT and streams it.
Path:
token: Short-lived playback token.source:xfor external subtitle files,ifor internal subtitle streams.index: Single-digit subtitle index.ext: Requested extension in the URL path.
Query:
reload(optional) - Bypass the subtitle conversion cache.
Response:
Content-Type: text/vtt- Converted subtitle body.
X-Cache: hit|miss
Errors:
304 Not ModifiedifIf-Modified-Sinceis present.400 Bad Requestif the token is invalid, the source is invalid, the subtitle codec is unsupported, or the target is not a subtitle stream.404 Not Foundif the media file or subtitle file does not exist.500 Internal Server Errorif conversion fails.
Notes:
- External formats such as
vtt,webvtt,srt, andassare supported. - Internal conversion currently supports text subtitle codecs listed in the player implementation.
- The route includes
{ext}, but the generated output is always WebVTT.
Returns a simple liveness payload.
Access:
- Open.
Response:
{
"status": "ok",
"message": "System is healthy"
}Returns build and runtime version metadata.
Response:
{
"version": "dev-master",
"build": "unknown",
"sha": "unknown",
"branch": "unknown",
"container": true
}Returns the list of supported backend types.
Response:
[
"plex",
"jellyfin",
"emby"
]All /v1/api/system/auth/* routes are open. Routes that need credentials validate them themselves.
Returns 200 OK if the auth route group is reachable.
Response:
200 OKwith an empty body.
Returns whether the system account exists and may include an auto-login token for trusted local clients.
Responses:
200 OKwith an empty body when no auto-login token is issued.204 No Contentwhen the system user/password is not configured.- JSON payload below when auto-login is allowed.
{
"auto_login": true,
"token": "..."
}Errors:
500 Internal Server Errorif token encoding fails.
Returns the decoded signed user token.
Auth:
Authorization: Token <token>?ws_token=<token>
Response:
{
"username": "admin",
"created_at": "2026-03-28T12:00:00+00:00"
}Errors:
401 Unauthorizedfor missing or invalid user tokens.500 Internal Server Errorif the system account is not configured.
Creates the initial WatchState admin account.
Body:
{
"username": "admin",
"password": "secret"
}Response:
201 Createdwith an empty body.
Errors:
400 Bad Requestifusernameorpasswordis missing.403 Forbiddenif the system account is already configured.
Exchanges username/password credentials for a signed user token.
Body:
{
"username": "admin",
"password": "secret"
}Response:
{
"token": "..."
}Errors:
400 Bad Requestif credentials are missing.401 Unauthorizedif the credentials are invalid.500 Internal Server Errorif the system account is not configured or token encoding fails.
Changes the configured system password.
Body:
{
"current_password": "old-secret",
"new_password": "new-secret"
}Response:
{
"info": {
"code": 200,
"message": "Password changed successfully."
}
}Errors:
400 Bad Requestif required fields are missing.401 Unauthorizedif the current password is invalid.500 Internal Server Errorif the stored password is missing or cannot be updated.
Invalidates all signed user sessions by rotating the signing secret.
Response:
{
"info": {
"code": 200,
"message": "Sessions invalidated successfully."
}
}Errors:
500 Internal Server Errorif secret rotation fails.
Lists supported env keys and current values for non-protected entries.
Query:
set(optional) - If true, only include keys present in the.envfile.
Response:
{
"data": [
{
"key": "WS_API_KEY",
"description": "API key used for X-APIKEY authentication",
"type": "string",
"mask": true,
"danger": true,
"value": "***",
"config_value": "...",
"config": "api.key"
}
],
"file": "/config/.env"
}Notes:
- Protected keys are omitted from external responses.
Reads one env key and its metadata.
Path:
key: Env key name.
Response:
{
"key": "WS_API_KEY",
"value": "secret",
"description": "API key used for X-APIKEY authentication",
"type": "string",
"mask": true,
"danger": true,
"config_value": "secret",
"config": "api.key"
}Errors:
400 Bad Requestif the key is invalid.404 Not Foundif the key is unset or protected from external access.
Sets or removes one env key.
Path:
key: Env key name.
POST Body:
{
"value": "new-value"
}Response:
- Returns the same metadata envelope as
GET /v1/api/system/env/{key}.
Errors:
400 Bad Requestif the key is invalid, the value is missing, or validation fails.404 Not Foundif the key is protected from external access.
Notes:
- This route edits
/config/.env. DELETEremoves the key from the.envfile.- Bool, int, and float values are coerced according to the env spec.
- Protected keys cannot be modified through external requests.
Lists supported GUID types and validators.
Response:
[
{
"guid": "imdb",
"type": "movie",
"validator": {
"pattern": "..."
}
}
]Reads the custom GUID configuration file.
Response:
{
"version": "0.0",
"guids": [],
"links": []
}Adds a custom GUID definition.
Body:
{
"name": "letterboxd",
"type": "plex",
"description": "Letterboxd movie GUID",
"validator": {
"pattern": "/^[a-z0-9-]+$/",
"example": "movie-name",
"tests": {
"valid": ["movie-name"],
"invalid": ["movie name"]
}
}
}Response:
{
"id": "uuid",
"type": "plex",
"name": "guid_letterboxd",
"description": "Letterboxd movie GUID",
"validator": {
"pattern": "/^[a-z0-9-]+$/",
"example": "movie-name",
"tests": {
"valid": ["movie-name"],
"invalid": ["movie name"]
}
}
}Errors:
400 Bad Requestif required fields are missing, the regex is invalid, or the tests do not match the supplied pattern rules.
Notes:
- If the name does not start with
guid_, WatchState adds the prefix automatically. typemust be one of the configured supported backend/client names such asplex,jellyfin, oremby, not a media type likemovie.
Deletes a custom GUID definition.
Path:
id: Custom GUID UUID.
Response:
{
"id": "uuid",
"name": "guid_letterboxd",
"type": "plex"
}Errors:
404 Not Foundif the GUID is not found.
Notes:
- Links that target the deleted GUID are removed automatically.
Lists custom GUID links for one backend client type.
Path:
client: Backend type such asplex,jellyfin, oremby.
Response:
[
{
"id": "uuid",
"type": "plex",
"map": {
"from": "GuidField",
"to": "guid_letterboxd"
}
}
]Errors:
404 Not Foundif the client type is unsupported.
Adds a custom GUID link for one backend client type.
Path:
client: Backend type.
Body:
{
"type": "plex",
"map": {
"from": "GuidField",
"to": "letterboxd"
},
"options": {
"legacy": true
},
"replace": {
"from": "old",
"to": "new"
}
}Response:
{
"id": "uuid",
"type": "plex",
"map": {
"from": "GuidField",
"to": "guid_letterboxd"
},
"options": {
"legacy": true
}
}Errors:
400 Bad Requestif required fields are missing, the target GUID is unsupported, or the mapping already exists.
Notes:
- For Plex links,
options.legacyis currently required and must be truthy to satisfy the current validation logic.
Deletes a custom GUID link for one backend client.
Path:
client: Backend type.id: Link UUID.
Response:
{
"id": "uuid",
"type": "plex",
"map": {
"from": "GuidField",
"to": "guid_letterboxd"
}
}Errors:
404 Not Foundif the link does not exist.
Returns the raw nested value stored at {client}.{index} in the custom GUID document.
Path:
client: Backend type.index: Numeric index.
Errors:
404 Not Foundif the client or requested index does not exist.
Notes:
- The current implementation looks up a nested path inside the custom GUID document, not an item from the
linksarray shown byGET /v1/api/system/guids/custom/{client}. - In a typical custom GUID file this means the route often returns
404 Not Foundunless the document contains a matching top-level{client}object with a numeric child key.
Lists queued and historical events.
Query:
page(optional) - Defaults to1.perpage(optional) - Defaults to10.filter(optional) - Partial match on the event name.
Response:
{
"paging": {
"page": 1,
"total": 25,
"perpage": 10,
"next": 2,
"previous": null
},
"items": [
{
"id": "uuid",
"event": "system:task",
"status": 0,
"status_name": "Pending",
"event_data": {},
"options": {},
"attempts": 0,
"created_at": "2026-03-28T12:00:00+00:00",
"updated_at": "2026-03-28T12:00:00+00:00"
}
],
"statuses": [
{
"id": 0,
"name": "Pending"
}
]
}Returns event counts grouped by status.
Query:
only(optional) - Comma-separated list of status names.
Response:
{
"pending": 3,
"running": 1,
"completed": 10,
"cancelled": 0,
"failed": 0
}Errors:
400 Bad Requestif any status name is invalid.
Queues a new event manually.
Body:
{
"event": "system:task",
"event_data": {
"name": "index"
},
"DELAY_BY": 30
}Response:
{
"info": {
"code": 202,
"message": "Event 'system:task' was queued."
},
"id": "uuid",
"event": "system:task",
"status": 0,
"status_name": "Pending"
}Errors:
400 Bad Requestifeventis missing.
Returns an event.
Path:
id: Event UUID.
Response:
{
"id": "uuid",
"event": "system:task",
"status": 0,
"status_name": "Pending",
"event_data": {}
}Errors:
404 Not Foundif the event does not exist.
Updates an event's mutable fields.
Path:
id: Event UUID.
Body:
{
"status": 3,
"event": "system:task",
"event_data": {
"name": "index"
},
"reset_logs": true
}Response:
{
"info": {
"code": 200,
"message": "Updated"
},
"id": "uuid",
"status_name": "Completed"
}Errors:
400 Bad Requestif the event is running,statusis not numeric, or the status value is invalid.404 Not Foundif the event does not exist.
Deletes one event.
Path:
id: Event UUID.
Response:
{
"id": "uuid",
"event": "system:task",
"status_name": "Cancelled"
}Errors:
400 Bad Requestif the event is currently running.404 Not Foundif the event does not exist.
Deletes all non-pending events.
Response:
200 OKwith an empty body.
Notes:
- Pending events are preserved.
Queues a one-time command for streamed execution.
Body:
{
"command": "system:index",
"cwd": "/home/coders/apps/watchstate",
"timeout": 120,
"force_color": true
}Response:
{
"token": "sha256-token",
"tracking": "/v1/api/system/command/sha256-token",
"expires": "2026-03-28T12:05:00+00:00"
}Errors:
400 Bad Requestif the body is empty orcommandis missing/invalid.
Notes:
- This is a high-risk admin endpoint.
- If
console.enable.allis enabled and the command starts with$, WatchState executes it throughsh -c.
Executes the queued command and streams output over SSE.
Path:
token: Command token returned byPOST /v1/api/system/command.
Response:
Content-Type: text/event-stream
Event Names:
cmdcwddatapingexit_codeclose
Example data Event Payload:
{
"data": "Console output line\n",
"type": "out"
}Errors:
400 Bad Requestif the token is invalid/expired or the queued command is invalid.
Notes:
- Tokens are single-use and are deleted before execution starts.
Returns task scheduler status.
Response:
{
"pid": "1234",
"status": true,
"restartable": true,
"message": "Task scheduler is running."
}Notes:
- When not running in a container, the endpoint still returns status metadata explaining the limitation.
Restarts the task scheduler.
Response:
{
"status": true,
"restartable": true,
"message": "Task scheduler restart has been requested."
}Errors:
400 Bad RequestifDISABLE_CRONis set or WatchState is not running in a container.
Notes:
- Admin operation. Restarts the background scheduler inside the container.
Returns the output of the system:report command.
Response:
{
"...": "report payload"
}Returns ini_get_all() for development builds.
Response:
{
"content": {
"memory_limit": {
"local_value": "512M",
"global_value": "512M"
}
}
}Errors:
403 Forbiddenoutside development builds.
Performs an outbound HTTP request for debugging connectivity, headers, and upstream responses.
Body:
{
"url": "https://example.com",
"method": "GET",
"headers": [
{
"key": "Authorization",
"value": "Bearer ..."
},
{
"key": "ws-timeout",
"value": "15"
}
]
}Response:
{
"request": {
"url": "https://example.com",
"method": "GET",
"headers": {
"Authorization": "Bearer ..."
}
},
"response": {
"status": 200,
"headers": {
"content-type": "text/html"
},
"body": "..."
}
}Errors:
400 Bad Requestifurlis missing, invalid, or if the HTTP method is invalid.
Notes:
- Transport failures still return HTTP
200, but the embeddedresponse.statusbecomes500and the embedded headers includeWS-ExceptionandWS-Error. - This is a high-risk admin endpoint because it can probe arbitrary URLs.
Converts the parsed request body to YAML.
Query:
inline(optional) - Inline nesting depth. Defaults to4.indent(optional) - Indent width. Defaults to2.
Body:
- Any parsed JSON payload.
Response:
Content-Type: text/yaml- YAML body rendered from the request payload.
Errors:
400 Bad Requestif YAML generation fails.
Notes:
- When
filenameis supplied, the response is returned as an attachment.
Creates a short-lived playback token for a filesystem path associated with a history item.
Path:
id: Numeric local history record ID.
Body:
{
"path": "/media/movies/Movie Title (2024).mkv",
"time": "PT24H",
"config": {
"audio": 1,
"subtitle": 2,
"debug": false
}
}Response:
{
"token": "play-abcdef123456",
"expires": "2026-03-29T12:00:00+00:00"
}Errors:
400 Bad Requestifpathis empty or the reference entity does not exist.404 Not Foundif the filesystem path does not exist.
Serves exported UI assets and allowlisted documentation files.
Access:
- Open.
Path:
file: Path relative to the static file root.
Response:
- File stream with
Content-Type,Content-Length, andLast-Modified.
Errors:
400 Bad Requestif the path is invalid.404 Not Foundif the file does not exist.
Returns a random poster or background image from the current history database.
Path:
type:posterorbackground
Query:
force(optional) - Ignore the cached random selection and choose a new item.
Response:
- Binary image stream.
Errors:
204 No Contentif no user context, no history rows, or no usable image can be found.
Lists available backup files.
Response:
[
{
"filename": "watchstate.20260328.json.zip",
"type": "automatic",
"size": 12345,
"date": "2026-03-28T12:00:00+00:00"
}
]Downloads or deletes one backup file.
Path:
filename: Backup file name.
GET Response:
- Raw file stream with detected
Content-Type.
DELETE Response:
200 OKwith an empty body.
Errors:
400 Bad Requestif the resolved file path escapes the backup directory.404 Not Foundif the file does not exist.
Notes:
DELETEpermanently removes the backup file.
Returns the current OS process list.
Response:
{
"processes": [
{
"user": "root",
"pid": "123",
"cpu": "0.0",
"mem": "0.1",
"command": "php-fpm"
}
]
}Errors:
500 Internal Server Errorifps auxfails.
Terminates one process by PID.
Path:
id: Numeric PID.
Response:
200 OKwith an empty body.
Errors:
400 Bad Requestif the PID is invalid.404 Not Foundif the process does not exist.500 Internal Server ErrorifSIGTERMorSIGKILLfails.
Notes:
- Admin operation. WatchState sends
SIGTERM, waits up to 5 seconds, then escalates toSIGKILLif needed.
Flushes the Redis cache database.
Response:
{
"info": {
"code": 200,
"message": "Cache purged successfully."
}
}Errors:
500 Internal Server Errorif Redis flush fails.
Notes:
- Flushes the entire Redis database used by WatchState.
Resets all user databases, clears sync timestamps, and flushes Redis.
Response:
{
"message": "System reset is complete."
}Notes:
- One of the most destructive routes in the API. Resets every user database, clears sync timestamps, and flushes Redis.
Resets PHP OPCache.
Response:
{
"message": "OPCache reset is complete."
}Notes:
- Affects PHP opcode cache for the running environment.
Finds history items whose media paths or parent directories no longer exist.
Query:
limit(optional) - Maximum number of broken items to return. Defaults to1000.
Response:
{
"items": [
{
"id": 101,
"title": "Movie Title",
"integrity": [
{
"backend": "plex_main",
"path": "/media/missing/file.mkv",
"status": false,
"message": "File does not exist."
}
]
}
],
"total": 1,
"fromCache": false
}Errors:
404 Not Foundif the user does not exist.
Notes:
- Directory and file existence checks are cached for 1 hour.
Clears the cached integrity scan state for the current user.
Response:
200 OKwith an empty body.
Errors:
404 Not Foundif the user does not exist.
Returns records that are missing metadata on some configured backends.
Query:
page(optional) - Defaults to1.perpage(optional) - Defaults to1000.min(optional) - Minimum number of backend metadata entries required.0means all configured backends.
Response:
{
"paging": {
"total": 12,
"perpage": 1000,
"current_page": 1,
"first_page": 1,
"next_page": null,
"prev_page": null,
"last_page": 1,
"params": {
"min": 3
}
},
"items": [
{
"id": 101,
"title": "Movie Title"
}
]
}Errors:
400 Bad Requestifminis greater than the number of backends.404 Not Foundif the user does not exist or the requested page is out of range.
Deletes records that fall below a required metadata parity threshold.
Input:
minis required in the parsed request data.
Response:
{
"deleted_records": 12
}Errors:
400 Bad Requestifminis zero, invalid, or larger than the number of backends.404 Not Foundif the user does not exist.
Finds duplicate local records that point at the same media path.
Query:
page(optional) - Defaults to1.perpage(optional) - Defaults to50.no_cache(optional) - Rebuild the duplicate cache instead of using the 30 minute cached result.
Response:
{
"paging": {
"total": 3,
"perpage": 50,
"current_page": 1,
"first_page": 1,
"next_page": null,
"prev_page": null,
"last_page": 1,
"params": []
},
"items": [
{
"id": 101,
"title": "Movie Title",
"duplicate_reference_ids": [102]
}
]
}Errors:
400 Bad Requestif the page number is invalid.404 Not Foundif the user does not exist.
Deletes the duplicate records found in the cached duplicate scan.
Response:
{
"deleted_records": 2
}Errors:
404 Not Foundif the duplicate cache has expired or if no duplicates are cached.
Notes:
- This route deletes every cached duplicate record ID, so it is destructive and cache-dependent.
Lists log suppressor rules.
Response:
{
"items": [
{
"id": "S1234567890",
"type": "contains",
"rule": "healthcheck",
"example": "GET /healthcheck"
}
],
"types": ["regex", "contains"]
}Adds a log suppressor rule.
Body:
{
"rule": "healthcheck",
"type": "contains",
"example": "GET /healthcheck"
}Response:
{
"id": "S1234567890",
"type": "contains",
"rule": "healthcheck",
"example": "GET /healthcheck"
}Errors:
400 Bad Requestif required fields are missing, the type is invalid, the regex is invalid, the example does not match, or another rule already suppresses the example.
Returns a suppressor rule.
Path:
id: 11 character suppressor rule ID.
Response:
{
"id": "S1234567890",
"type": "contains",
"rule": "healthcheck",
"example": "GET /healthcheck"
}Errors:
400 Bad Requestif the ID is invalid.404 Not Foundif the rule does not exist.
Replaces one suppressor rule.
Path:
id: 11 character suppressor rule ID.
Body:
{
"rule": "new-regex",
"type": "regex",
"example": "Some log line"
}Response:
- Same payload shape as create/view.
Errors:
- Same validation errors as
POST /v1/api/system/suppressor.
Deletes one suppressor rule.
Path:
id: 11 character suppressor rule ID.
Response:
{
"id": "S1234567890",
"type": "contains",
"rule": "healthcheck",
"example": "GET /healthcheck"
}Errors:
400 Bad Requestif the ID is invalid.404 Not Foundif the rule does not exist.
Lists scheduled tasks and shows which ones are queued.
Response:
{
"tasks": [
{
"name": "index",
"description": "Rebuild indexes",
"enabled": true,
"timer": "*/30 * * * *",
"next_run": "2026-03-28T12:30:00+00:00",
"prev_run": "2026-03-28T12:00:00+00:00",
"command": "system:index",
"args": [],
"hide": false,
"allow_disable": true,
"queued": false
}
],
"queued": []
}Notes:
- Hidden tasks are omitted from the list.
Returns a task definition.
Path:
id: Task name.
Response:
{
"name": "index",
"description": "Rebuild indexes",
"enabled": true,
"timer": "*/30 * * * *",
"next_run": "2026-03-28T12:30:00+00:00",
"prev_run": "2026-03-28T12:00:00+00:00",
"command": "system:index",
"args": [],
"hide": false,
"allow_disable": true,
"queued": false
}Errors:
404 Not Foundif the task is unknown.
Gets queue state, queues a run, or cancels a queued run.
Path:
id: Task name.
GET Response:
{
"task": "index",
"is_queued": false
}POST Response:
{
"id": "uuid",
"event": "system:task",
"status": 0,
"...": "queued event payload"
}DELETE Response:
200 OKwith an empty body.
Errors:
404 Not Foundif the task does not exist or, forDELETE, if it is not queued.409 Conflictif the task is already queued.400 Bad Requestif you try to remove a running task.
Notes:
POSTreturns202 Accepted.
Lists configured WatchState identities and each identity's backend names.
Response:
{
"identities": [
{
"identity": "main",
"backends": ["plex_main", "jellyfin_main"]
}
]
}Creates a new WatchState identity configuration set.
Body:
{
"identity": "family"
}Response:
201 Createdwith an empty body.
Errors:
400 Bad Requestifidentityis missing or invalid.409 Conflictif the identity already exists.500 Internal Server Errorif creation fails.
Notes:
- Identity names are normalized to lowercase.
Deletes one WatchState identity configuration set.
Path:
identity: Identity name.
Response:
200 OKwith an empty body.
Errors:
400 Bad Requestif the identity name is missing or invalid.403 Forbiddenif the identity ismain.404 Not Foundif the identity does not exist.500 Internal Server Errorif deletion fails.
Returns the full backend config object for one identity.
Path:
identity: Identity name.
Response:
{
"plex_main": {
"type": "plex",
"url": "https://plex.example.com",
"token": "..."
}
}Errors:
400 Bad Requestif the identity name is missing or invalid.404 Not Foundif the identity does not exist.
Replaces the full backend config object for one identity.
Path:
identity: Identity name.
Body:
- JSON object or YAML document describing the entire backend config file.
JSON Example:
{
"plex_main": {
"type": "plex",
"url": "https://plex.example.com",
"token": "..."
}
}Response:
{
"plex_main": {
"type": "plex",
"url": "https://plex.example.com",
"token": "..."
}
}Errors:
400 Bad Requestif the identity name is invalid, the body cannot be parsed, the body is not an object, or validation fails.404 Not Foundif the identity does not exist.
Notes:
- Accepts JSON and YAML request bodies.
- Requests with
application/jsonare parsed as JSON; other request bodies are parsed as YAML. - Validation errors include an
errorsarray in the response body.
Returns the current identity provisioning preview built from live backend member discovery and the saved mapper file.
Query:
force(optional) - Bypass the 5 minute cache and rebuild the preview.
Response:
{
"has_identities": true,
"has_mapping": true,
"backends": ["plex_main", "jellyfin_main"],
"matched": [
{
"identity": "alice",
"members": [
{
"id": "123",
"username": "alice",
"backend": "plex_main",
"real_name": "Alice",
"type": "plex",
"protected": false,
"options": {}
}
]
}
],
"unmatched": [
{
"id": "789",
"username": "bob",
"backend": "plex_main",
"real_name": "Bob",
"type": "plex",
"protected": true,
"options": {}
}
],
"expires": "2026-03-28T12:05:00+00:00"
}Notes:
has_identitiesmeans generated identity YAML files already exist underusers/*/*.yaml.has_mappingmeans a non-empty mapper file was loaded.- Matching can still happen with
has_mapping=falsethrough direct normalized username matching. usernameis the normalized internal name.real_nameis the original backend-reported name.
Creates or replaces the saved cross-backend identity mapping file.
Body:
{
"version": "1.6",
"identities": [
{
"identity": "alice",
"members": [
{
"backend": "plex_main",
"username": "alice",
"options": {}
},
{
"backend": "jellyfin_main",
"username": "alice",
"options": {}
}
]
}
]
}Response:
{
"info": {
"code": 200,
"message": "Identity mapping successfully updated."
},
"version": "1.6",
"identities": [
{
"identity": "alice",
"members": [
{
"backend": "plex_main",
"username": "alice",
"options": {}
}
]
}
]
}Errors:
400 Bad Requestifidentitiesis missing, empty, not an array, or ifversionis lower than1.5.
Creates, updates, or recreates identities directly through the API.
Body:
{
"mode": "update",
"dry_run": false,
"generate_backup": true,
"regenerate_tokens": false,
"allow_single_backend_identities": false,
"save_mapping": true,
"mapping": {
"version": "1.6",
"identities": [
{
"identity": "alice",
"members": [
{
"backend": "plex_main",
"username": "alice",
"options": {}
},
{
"backend": "jellyfin_main",
"username": "alice",
"options": {}
}
]
}
]
}
}Response:
{
"info": {
"code": 200,
"message": "Identities updated successfully."
},
"mode": "update",
"dry_run": false,
"save_mapping": true,
"allow_single_backend_identities": false,
"count": 1,
"identities": [
{
"identity": "alice",
"backends": ["plex_main", "jellyfin_main"],
"members": [
{
"backend": "plex_main",
"username": "alice"
},
{
"backend": "jellyfin_main",
"username": "alice"
}
]
}
]
}Errors:
400 Bad Requestifmodeis invalid or provisioning cannot produce identities.409 Conflictifmode=createis requested while local identities already exist.500 Internal Server Errorif provisioning fails unexpectedly.
Notes:
modeacceptscreate,update, orrecreate.save_mapping=truepersists the provided mapping before provisioning.allow_single_backend_identities=truerequires exactly one configured backend and allows one-member identity groups.
Safely syncs already-linked identity backends from the current main backend configuration.
This route does not create, delete, or rematch identities. It only updates existing linked backend configs.
Body:
{
"dry_run": false
}Response:
{
"info": {
"code": 200,
"message": "Synced 4 identity backend(s) successfully."
},
"dry_run": false,
"updated_count": 4,
"skipped_count": 2,
"failed_count": 0,
"updated": [
{
"identity": "alice",
"backend": "plex_alice",
"source_backend": "plex_main"
}
],
"skipped": [
{
"identity": "manual_profile",
"backend": "custom_backend",
"reason": "Backend is not linked to a source backend."
}
],
"failed": []
}Notes:
- This is the safe maintenance path for propagating changes like backend URL, shared tokens, UUID, import/export settings, and shared backend options.
- Per-identity values such as backend user IDs, Plex child tokens, Plex user UUID/name, and protected user PINs are preserved.
dry_run=truereports what would change without writing any identity config files.
Receives backend webhook payloads, matches them to configured users/backends, and queues import processing.
Access:
- Open when
WS_SECURE_API_ENDPOINTS=false. - Otherwise standard API auth.
Input:
- Backend-specific headers and payload.
- The body shape depends on the backend sending the webhook.
Response:
200 OKwhen the webhook was parsed and queued successfully.304 Not Modifiedwhen the payload is intentionally ignored.406 Not Acceptablewhen import is disabled for the target backend.
Errors:
400 Bad Requestif no backend parser can recognize the payload.400 Bad Requestif the payload lacks the backend unique ID.400 Bad Requestif a non-generic payload lacks the backend user ID.400 Bad Requestif the payload does not map to any configured user/backend pair.
Notes:
- The endpoint fans out across all matching user/backend pairs for generic webhook payloads.
- Unsupported or unusable items are ignored with
304 Not Modified, for example:- items without supported GUIDs
- episode events without season/episode numbers
- generic requests that the backend parser cannot fully resolve
- When a request is queued, WatchState creates a unique event reference based on item type, backend, user, and tainted state.
Most endpoints return standard error codes (400, 401, 403, 404, 409, 500, etc.) and a JSON envelope on failure. For example:
{
"error": {
"code": 400,
"message": "Description of the problem"
}
}Informational success responses use the same structure under info:
{
"info": {
"code": 200,
"message": "Human readable message"
}
}Some endpoints return a bare array, 204 No Content, SSE, HLS text, or binary file content instead of a JSON envelope.