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` |
|
|
|
|
|
|
2026-10-08 13:28:41 +02:00
|
|
|
### 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.
|