Files
tuliprox/docs/src/rest-api-cookbook.md
T

668 lines
26 KiB
Markdown
Raw Normal View History

2026-04-03 16:23:15 +02:00
# REST API Cookbook
This page contains copy-paste examples for the Tuliprox Web UI REST API.
The examples use:
- `curl` for HTTP requests
- `jq` for token extraction and pretty-printing JSON
## Requirements
- Tuliprox is running in server mode
- the Web UI REST API is enabled
- `curl` and `jq` are installed
- you have a valid Web UI username and password
## Base URLs
By default, the login endpoint is:
```text
http://localhost:8901/auth/token
```
If you configured a custom Web UI path such as `web`, the login endpoint becomes:
```text
http://localhost:8901/web/auth/token
```
The protected REST API lives below:
```text
http://localhost:8901/api/v1
```
or, with a Web UI path:
```text
http://localhost:8901/web/api/v1
```
## Example 1: Get a JWT token
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
USERNAME="admin"
PASSWORD="12345678"
TOKEN=$(curl -s -X POST "$BASE_URL/auth/token" \
-H 'accept: application/json' \
-H 'content-type: application/json' \
--data-raw "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" | jq -r '.token')
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then
echo "Error: no token available"
exit 1
fi
echo "$TOKEN"
```
If your local setup uses HTTPS with a self-signed certificate, add `--insecure` to the `curl` commands.
## Example 2: Query server status
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
USERNAME="admin"
PASSWORD="12345678"
TOKEN=$(curl -s -X POST "$BASE_URL/auth/token" \
-H 'accept: application/json' \
-H 'content-type: application/json' \
--data-raw "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" | jq -r '.token')
curl -s -X GET "$BASE_URL/api/v1/status" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq .
```
Typical use:
- verify that login works
- use as a simple health and diagnostics check
- inspect active users, active provider connections, cache state, and current server time
From this point onward, the examples use:
```bash
TOKEN="PUT_YOUR_TOKEN_HERE"
```
This is only to keep the examples shorter and easier to copy-paste.
Example 1 above already shows how to obtain the JWT token, so it is not repeated in every script block below.
## Example 3: List active streams
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
curl -s -X GET "$BASE_URL/api/v1/streams" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq .
```
Typical use:
- inspect currently active streams
- see who is connected
- correlate user activity with provider slot pressure
## Example 4: Query stream history summary
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
curl -s -X GET "$BASE_URL/api/v1/stream-history/summary?from=2026-04-01&to=2026-04-03" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq .
```
Typical use:
- get an aggregated view over recent stream activity
- inspect disconnect patterns and provider churn
- verify that stream history collection is working
2026-08-29 15:32:08 +02:00
## Example 5: Diagnose the event bus
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
curl -s -X GET "$BASE_URL/api/v1/events/stats" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq .
```
Every runtime event — playlist updates, config reloads, recording changes,
metadata refreshes — passes through one in-process broadcast bus on its way to
the Web UI websocket and the notification pipeline. This endpoint reports what
that bus has carried:
- `emitted` — a count per event, using the same stable names plugins and
notification subscriptions use.
- `no_subscribers` — events published while nothing was listening. Normal when
the Web UI is closed and notifications are off; a large number beside "I never
got that notification" is the answer.
- `coalesced` — payload-free nudges suppressed because an identical one had just
gone out. Deleting a recording naturally produces a few.
- `lagged` — events a subscriber was told it had missed. Should be `0`. If it is
not, a subscriber cannot keep up: raise
[`event_channel_capacity`](configuration/config.md).
- `recent` — the last 256 events with the outcome of each, so you can see
whether a specific event fired at all.
Typical use:
- confirm an event fired before hunting for a bug in whatever consumes it
- tell "no notification was sent" apart from "no notification was configured"
- size `event_channel_capacity` from observed lag rather than by guessing
## Example 6: Query QoS snapshots
2026-04-03 16:23:15 +02:00
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
curl -s -X GET "$BASE_URL/api/v1/qos-snapshots" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq .
```
Typical use:
- inspect per-stream reliability snapshots
- compare `24h`, `7d`, and `30d` quality windows
- prepare later failover and ranking analysis
2026-10-04 16:02:55 +02:00
## Example 7: Request a recording
Recording requests name server-owned source ids. The client never supplies a
URL, a filename or a path: the server resolves all three, so a caller cannot
point a recording at arbitrary storage.
2026-04-03 16:23:15 +02:00
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
2026-10-04 16:02:55 +02:00
curl -s -i -X POST "$BASE_URL/api/v1/recording/requests" \
2026-04-03 16:23:15 +02:00
-H "Authorization: Bearer $TOKEN" \
2026-10-04 16:02:55 +02:00
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f1c0f1e-2b7a-4f5d-9d2e-6c3f0f1b2a44" \
--data-raw '{
"source": {
"target_id": "1",
"virtual_id": "4242",
"cluster": "live",
"input_name": "provider-a"
},
"program_title": "Example Programme",
"program_start": 1756000000,
"program_end": 1756003600,
"visibility": "private"
}'
2026-04-03 16:23:15 +02:00
```
2026-10-04 16:02:55 +02:00
The response is `204 No Content` — a command, not a query. The new entry
reaches the client on the next recording snapshot over the WebSocket, so
there is one description of a recording rather than two that can disagree.
`Idempotency-Key` is optional. Repeating the same key with the same body
returns `204` again without creating a second recording, for 24 hours from
the first accepted request. The same key with a *different* body returns
`409 Conflict` rather than quietly answering with the first request's result.
2026-04-03 16:23:15 +02:00
Typical use:
2026-10-04 16:02:55 +02:00
- queue a recording without polling for its state afterwards
- make a client retry safe across a dropped connection or a server restart
- test `recording.create` access
2026-04-03 16:23:15 +02:00
2026-10-04 16:02:55 +02:00
## Example 8: Control an existing recording
2026-04-03 16:23:15 +02:00
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
2026-10-04 16:02:55 +02:00
REQUEST_ID="PUT_A_REQUEST_ID_HERE"
2026-04-03 16:23:15 +02:00
2026-10-04 16:02:55 +02:00
# A user acts on their own library entry.
curl -s -i -X POST "$BASE_URL/api/v1/recording/requests/$REQUEST_ID/cancel" \
-H "Authorization: Bearer $TOKEN"
# Removing a finished entry detaches it; the file survives while anyone
# else still holds it.
curl -s -i -X DELETE "$BASE_URL/api/v1/recording/requests/$REQUEST_ID" \
-H "Authorization: Bearer $TOKEN"
2026-04-03 16:23:15 +02:00
```
2026-10-04 16:02:55 +02:00
Every command answers `204`. Pause, resume and retry act on the *file*
rather than one user's entry, so they are administrator routes under
`/materializations/{id}` and are rejected for Live captures, which cannot be
paused, resumed or retried.
2026-04-03 16:23:15 +02:00
Typical use:
2026-10-04 16:02:55 +02:00
- leave a shared recording without stopping it for anyone else
- test `recording.manage` and `recording.delete` access
2026-04-03 16:23:15 +02:00
2026-08-29 15:32:08 +02:00
## Example 9: Trigger a playlist update
2026-04-03 16:23:15 +02:00
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
curl -s -X POST "$BASE_URL/api/v1/playlist/update" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-raw '["main"]'
```
Typical use:
- trigger a manual processing run for one or more targets
- verify `playlist.write` permission
- integrate Tuliprox into external automation
2026-08-29 15:32:08 +02:00
## Example 10: Preview a Stalker live input in the Web UI playlist API
2026-07-20 14:58:06 +02:00
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
curl -s -X POST "$BASE_URL/api/v1/playlist/live" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-raw '{"Input":"stalker"}' | jq .
```
Typical use:
- verify that a configured Stalker input handshakes and returns playlist preview rows
- confirm that the Web UI playlist explorer can inspect Stalker live content
- distinguish preview/catalog problems from later playback-resolution problems
Notes:
- Replace `"stalker"` with the configured input name.
2026-09-26 18:56:17 +02:00
- The preview endpoint returns catalog items. Actual playback resolves the Stalker `create_link` URL when the request reaches
the stream endpoint.
- If a Stalker item has not been materialized yet, Tuliprox does not expose the raw portal `cmd` as the playlist URL. Runtime
2026-07-20 14:58:06 +02:00
playback resolves a real media URL later through the reverse-proxy path.
2026-09-26 18:56:17 +02:00
- Expired temp links are refreshed automatically, including one re-handshake when the portal rejects the session. Only
portal-specific extra header/cookie requirements remain a possible follow-up.
2026-07-20 14:58:06 +02:00
- Runtime refresh only produces `http`/`https` playback URLs; Stalker `rtmp://` / `rtsp://` commands are rejected explicitly rather
than proxied half-supported.
2026-08-29 15:32:08 +02:00
## Example 11: Dry-run a filter expression against a target
2026-08-21 17:50:12 +05:00
```bash
#!/bin/bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
curl -s -X POST "$BASE_URL/api/v1/playlist/filter/preview" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw '{"target": 1, "filter": "Group ~ \"^DE.*\" AND NOT Title CONTAINS \"Shopping\"", "limit": 10}' | jq .
```
Typical use:
- test a filter DSL expression against a target's stored playlist before writing it into `source.yml`
- see matched/total counts overall and per cluster (live/vod/series)
- inspect sample matched and excluded channels to verify the expression does what you expect
Notes:
- `target` is the numeric target id (same id the playlist explorer uses).
- `filter` supports the full filter DSL including `!TEMPLATE!` references from your configured templates.
- Optional `limit` caps the sample lists (default 25, max 50); optional `match_as_ascii` mirrors the target option.
- An invalid filter expression returns HTTP 422 with `{"error": "...", "line": n, "column": n}`; `line`/`column`
are `null` for semantic errors without a source position (e.g. an invalid regex value).
- The preview reads the target's already-processed playlist; it never contacts providers or triggers an update.
2026-04-03 16:23:15 +02:00
## Available `/api/v1` Endpoints
This is a compact operator-oriented overview of the `/api/v1` REST API groups currently registered by the backend.
### System and diagnostics
| Method | Path | Purpose |
| --- | --- | --- |
2026-08-22 21:52:19 +05:00
| `GET` | `/healthcheck` | Liveness probe — 200 while the process is running. Used by Docker via the `--healthcheck` CLI flag. |
| `GET` | `/ready` | Readiness probe — 200 when at least one input group has spare connection capacity; 503 when every input group is exhausted. |
2026-04-03 16:23:15 +02:00
| `GET` | `/api/v1/status` | Server status, version, active users, provider connections, cache state |
| `GET` | `/api/v1/streams` | Current active streams |
| `GET` | `/api/v1/ipinfo` | External IPv4/IPv6 check if configured |
2026-08-29 15:32:08 +02:00
| `GET` | `/api/v1/events/stats` | Event-bus counters and the last 256 events, with the outcome of each |
2026-04-03 16:23:15 +02:00
| `GET` | `/api/v1/stream-history` | Query raw persisted stream history |
| `GET` | `/api/v1/stream-history/summary` | Aggregated stream history summary |
| `GET` | `/api/v1/qos-snapshots` | List QoS snapshots |
| `GET` | `/api/v1/qos-snapshots/{stream_identity_key}` | QoS detail for one stream |
| `GET` | `/api/v1/geoip/update` | Trigger GeoIP DB update |
2026-10-04 16:02:55 +02:00
### Recordings
Recording REST is **commands only**. There is no list, task, status or quota
endpoint to poll: every successful command answers `204 No Content`, and all
list, progress, quota and availability data reaches a client on the recording
WebSocket snapshot. That way a recording has one description rather than two
that can disagree.
Requests are addressed by the caller's own library-entry id. The three
materialization routes act on the shared *file* and are administrator-only;
the id for them appears solely in the administrator block of a snapshot, so
a regular user's DTO can never be used to reach one.
2026-04-03 16:23:15 +02:00
2026-08-21 17:50:12 +05:00
| Method | Path | Purpose |
| -------- |------------------------------------------------------|---------------------------------------------------------------------------------|
2026-10-04 16:02:55 +02:00
| `POST` | `/api/v1/recording/requests` | Request a recording from server-owned source ids |
| `PATCH` | `/api/v1/recording/requests/{id}` | Edit an upcoming recording |
| `POST` | `/api/v1/recording/requests/{id}/cancel` | Cancel the caller's recording |
| `DELETE` | `/api/v1/recording/requests/{id}` | Remove the caller's library entry |
| `POST` | `/api/v1/recording/materializations/{id}/pause` | Pause a transfer (admin; never Live) |
| `POST` | `/api/v1/recording/materializations/{id}/resume` | Resume a paused transfer (admin; never Live) |
| `POST` | `/api/v1/recording/materializations/{id}/retry` | Retry a failed transfer (admin; never Live) |
| `DELETE` | `/api/v1/recording/materializations/{id}` | Delete the physical recording (admin; refused while referenced) |
2026-08-21 17:50:12 +05:00
| `POST` | `/api/v1/recording/conflicts/preview` | Advisory conflict preview (severity, optional provider scope, overlap segments) |
2026-10-04 16:02:55 +02:00
| `GET` | `/api/v1/recording/availability` | Whether recording is available to the caller |
| `GET` | `/api/v1/recording/health` | Supervisor and recovery health (admin) |
2026-08-21 17:50:12 +05:00
| `GET` | `/api/v1/recording/rules` | List visible recurring recording rules |
| `POST` | `/api/v1/recording/rules` | Create a weekly recurring recording rule |
| `PATCH` | `/api/v1/recording/rules/{id}` | Edit a recurring recording rule |
| `DELETE` | `/api/v1/recording/rules/{id}?future=retain\|cancel` | Delete a recurring recording rule |
2026-10-04 16:02:55 +02:00
Every route is gated by `recording.enabled`. With recording switched off they
answer `501 Not Implemented` with the code `recording_disabled`, which
distinguishes "switched off here" from `403` (not allowed) and `404` (does
not exist).
#### Idempotent creates
`POST /api/v1/recording/requests` honours an optional `Idempotency-Key`
header:
- the same principal, key and body returns the original `204` without
creating a second recording;
- the same key with a different body returns `409 Conflict`, because
answering it with the first request's result would hide a caller bug;
- the record is kept for 24 hours from first acceptance and survives a
restart, so a client retrying across one cannot duplicate its recording;
- after that it is forgotten, and the request is treated as new.
2026-08-21 17:50:12 +05:00
#### DVR WebSocket protocol
2026-10-04 16:02:55 +02:00
The DVR layer ships its own scoped protocol messages on the same WebSocket
connection as the rest of the backend. The client sends
`ProtocolMessage::RecordingSnapshotRequest` to subscribe and receives
`ProtocolMessage::RecordingSnapshotResponse { revision, available, quota, tasks }`
— the complete filtered list for the caller's session, sent on connect and
after every change the session is permitted to see.
There is no delta message. A snapshot is always complete, so a client that
misses one loses nothing.
2026-08-21 17:50:12 +05:00
2026-10-04 16:02:55 +02:00
Revisions are global across the server, so a session sees gaps whenever a
change touched somebody else. A gap is normal and must be applied; a
revision that goes *backwards* is a reordered delivery and must be ignored,
or the list regresses to a state the server has already moved past.
2026-08-21 17:50:12 +05:00
2026-10-04 16:02:55 +02:00
Progress-only updates are coalesced to at most one snapshot per second per
session. State transitions, command results, quota and availability changes,
and terminal states bypass that timer and publish immediately — a finished
recording must never look like it is still running.
2026-08-21 17:50:12 +05:00
#### DVR conflict preview
`POST /api/v1/recording/conflicts/preview` is **advisory only** — the response carries a severity bucket
(`none` / `soft` / `hard`) plus optional provider scope and overlap segments, but the server does not
reject the create call based on it. The frontend renders the preview as a hint next to the recording
form's scheduled interval, never as a hard block.
2026-04-03 16:23:15 +02:00
### Playlist and web-player helpers
| Method | Path | Purpose |
| --- | --- | --- |
2026-07-20 14:58:06 +02:00
| `POST` | `/api/v1/playlist/live` | Query live playlist content for the Web UI, including Stalker inputs |
2026-04-03 16:23:15 +02:00
| `POST` | `/api/v1/playlist/vod` | Query VOD playlist content |
| `POST` | `/api/v1/playlist/series` | Query series playlist content |
| `POST` | `/api/v1/playlist/resolve_url` | Resolve provider-backed stream URLs |
| `POST` | `/api/v1/playlist/update` | Trigger target updates |
| `POST` | `/api/v1/playlist/epg` | Query EPG data for the Web UI |
| `POST` | `/api/v1/playlist/series_info/{virtual_id}/{provider_id}` | Series metadata lookup |
| `POST` | `/api/v1/playlist/series/episode/{virtual_id}` | Episode item lookup |
2026-08-21 17:50:12 +05:00
| `POST` | `/api/v1/playlist/filter/preview` | Dry-run a filter DSL expression against a target's stored playlist |
2026-04-03 16:23:15 +02:00
| `GET` | `/api/v1/playlist/resource/{resource}` | Public resource access for playlist-related assets |
### Configuration
| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/v1/config` | Read effective configuration |
| `GET` | `/api/v1/config/batchContent/{input_id}` | Inspect batch input content |
| `POST` | `/api/v1/config/xtream/login-info` | Test or inspect Xtream login information |
| `POST` | `/api/v1/config/main` | Save `config.yml` |
| `POST` | `/api/v1/config/sources` | Save `source.yml` |
| `GET` | `/api/v1/config/apiproxy` | Read `api-proxy.yml` |
| `PUT` | `/api/v1/config/apiproxy` | Save `api-proxy.yml` |
### Personal settings (`me`)
`me` identifies the caller from the authenticated session. These routes do not accept a username or user ID
to select another account. Web UI users and API proxy users with Web UI access can manage their own settings;
administrator privileges are not required. If Web UI authentication is disabled, all callers use the same local
settings and the response reports `shared: true`.
The examples request JSON with `Accept: application/json`. Responses also support `Accept: application/cbor`,
and `PUT` accepts CBOR with `Content-Type: application/cbor`.
| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/v1/me/settings` | Read the caller's preferences and per-table version tags |
| `GET` | `/api/v1/me/settings/tables/{table}` | Read one table layout and its `ETag` header |
| `PUT` | `/api/v1/me/settings/tables/{table}` | Replace one table layout; requires `If-Match` |
| `DELETE` | `/api/v1/me/settings/tables/{table}` | Reset one table's layout overrides; requires `If-Match` |
#### Read all preferences
```bash
BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
curl -sS "$BASE_URL/api/v1/me/settings" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq .
```
An account without stored preferences returns:
```json
{
"preferences": {
"web_ui": {
"tables": {}
}
},
"section_etags": {},
"shared": false
}
```
Stored layouts appear in `preferences.web_ui.tables`, keyed by table ID. `section_etags` contains the corresponding
quoted version tags, also keyed by table ID. This endpoint does not return a top-level `ETag` header.
To obtain a version tag for a table without stored overrides, read that table's endpoint.
#### Read, save, and reset a table layout
`{table}` is the stable table ID used by the Web UI, for example `users` for the API proxy user table.
Percent-encode the ID when putting it in the URL. Column IDs are stable identifiers such as `username` and `enabled`,
rather than translated display labels.
A table layout has two fields:
```json
{
"column_order": ["username", "enabled"],
"column_visibility": {
"enabled": false
}
}
```
`column_order` specifies the preferred order. `column_visibility` maps column IDs to visibility flags.
Columns without overrides use the Web UI defaults, and required columns remain visible.
`PUT` replaces both fields: omitted fields default to an empty list or map. Unknown payload fields and duplicate
IDs in `column_order` are rejected.
The following sequence reads the layout, saves an override, and then resets it. It uses the token obtained above.
When authentication is disabled, omit the `Authorization` headers.
```bash
TABLE_URL="$BASE_URL/api/v1/me/settings/tables/users"
HEADERS_FILE=$(mktemp)
trap 'rm -f "$HEADERS_FILE"' EXIT
# Read the layout and capture its quoted ETag, including for an unsaved table.
curl --fail-with-body -sS -D "$HEADERS_FILE" "$TABLE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" || exit 1
ETAG=$(awk 'tolower($1) == "etag:" {gsub("\r", "", $2); print $2}' "$HEADERS_FILE")
# Save only if this table still matches the version just read.
curl --fail-with-body -sS -D "$HEADERS_FILE" -X PUT "$TABLE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "If-Match: $ETAG" \
--data-raw '{"column_order":["username","enabled"],"column_visibility":{"enabled":false}}' || exit 1
# Use the version returned by the successful save for the next change.
ETAG=$(awk 'tolower($1) == "etag:" {gsub("\r", "", $2); print $2}' "$HEADERS_FILE")
curl --fail-with-body -sS -i -X DELETE "$TABLE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "If-Match: $ETAG" || exit 1
```
All three table operations return `200 OK`, the resulting layout, and its `ETag` header.
An unsaved or reset layout is `{"column_order":[],"column_visibility":{}}`; the Web UI applies its defaults.
`DELETE` resets only the selected table's layout overrides. It does not delete the account or other tables' settings.
Successful settings responses use `Cache-Control: private, no-store`.
#### Concurrent changes and errors
Send exactly one `If-Match` header with the quoted tag returned by the server. Tags are scoped to the account and table;
changes to another table do not invalidate this table's tag. Weak tags, `*`, unquoted tags, and lists of tags are rejected.
If another client changes the same table before your save or reset, the server returns `412 Precondition Failed`
without applying your change. Read the table again, reconcile your draft with the current layout, and use the fresh
tag only after deciding to apply the change again.
Settings validation and storage errors return a JSON object such as `{"error":"settings_precondition_failed"}`
when requesting JSON. Authentication errors return a plain-text error message.
| Status | Error code | Meaning |
| --- | --- | --- |
| `400` | `settings_precondition_invalid` | `If-Match` is malformed or supplied more than once |
| `400` | `settings_payload_invalid` | Invalid layout payload, unknown fields, or duplicate ordered column IDs |
| `400` | `settings_id_invalid` | Invalid table or column ID |
| `400` | `settings_limits_exceeded` | The proposed layout or resulting settings exceeds a storage limit |
| `401` / `403` | Authentication error | Invalid session or caller without Web UI access |
| `409` | `settings_invalid`, `settings_version_unsupported`, `settings_owner_mismatch`, `settings_file_unsafe`, or `settings_limits_exceeded` | Stored settings cannot be safely read or changed |
| `412` | `settings_precondition_failed` | The tag no longer matches the table |
| `413` | `settings_request_too_large` | The `PUT` request body exceeds 64 KiB |
| `428` | `settings_precondition_required` | `PUT` or `DELETE` is missing `If-Match` |
| `500` | `settings_io_error` | Settings storage I/O failed |
IDs must be nonempty, contain no control characters, and fit in 128 UTF-8 bytes. Each layout supports at most
256 distinct column IDs across its order and visibility fields. Each account supports at most 64 stored tables
and a 256 KiB settings file. Invalid or unsupported stored files are not overwritten.
For storage locations and backup guidance, see [Personal table layouts](features.md#personal-table-layouts).
2026-04-03 16:23:15 +02:00
### API proxy users
| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/v1/user/{target}` | Create a target user |
| `PUT` | `/api/v1/user/{target}` | Update or move a target user |
| `DELETE` | `/api/v1/user/{target}/{username}` | Delete a target user |
### Library
| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/v1/library/status` | Local library status |
| `POST` | `/api/v1/library/scan` | Trigger a library scan |
| `GET` | `/api/v1/library/thumbnail/{uuid}` | Read a generated thumbnail |
### RBAC management
| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/v1/rbac/users` | List Web UI users |
| `POST` | `/api/v1/rbac/users` | Create a Web UI user |
| `PUT` | `/api/v1/rbac/users/{username}` | Update a Web UI user |
| `DELETE` | `/api/v1/rbac/users/{username}` | Delete a Web UI user |
| `GET` | `/api/v1/rbac/groups` | List permission groups |
| `POST` | `/api/v1/rbac/groups` | Create a permission group |
| `PUT` | `/api/v1/rbac/groups/{name}` | Update a permission group |
| `DELETE` | `/api/v1/rbac/groups/{name}` | Delete a permission group |
| `GET` | `/api/v1/rbac/permissions` | List available permissions |
## Permissions
With Web UI authentication enabled, many endpoints require matching permissions such as:
- `system.read`
- `system.write`
- `playlist.read`
- `playlist.write`
- `config.read`
- `config.write`
- `source.read`
- `source.write`
- `user.read`
- `user.write`
- `library.read`
- `library.write`
2026-08-21 17:50:12 +05:00
- `recording.read`
2026-10-04 16:02:55 +02:00
- `recording.create`
- `recording.manage`
- `recording.delete`
2026-04-03 16:23:15 +02:00
If a request is rejected, verify the logged-in Web UI user's RBAC group assignments first.
2026-08-21 17:50:12 +05:00
> **See also:** the full [DVR Operator Reference](./operator/dvr.md) for the authorization matrix, identity-registry
> bootstrap, token refresh, recurring-rule matching + DST, cross-store reconciliation, at-most-once notification
> protocol, and the migration checklist.