Files
tuliprox/docs/src/rest-api-cookbook.md
T
euzuandGitHub 62aea2a2fe persist table layouts and unify column metadata (#906)
* **New Features**
  * Customize table columns by showing, hiding, and reordering them, with keyboard or pointer controls.
  * Save layouts per account, restore defaults, and resolve conflicts when preferences change elsewhere. On installations without web authentication, settings are shared.
  * CSV tables now support saved layouts for recognized table structures.
* **Bug Fixes**
  * Improved pagination bounds and page-size validation.
  * Settings requests now return clearer errors for oversized or unreadable request bodies, and recheck access after waiting for settings to become available.
* **Documentation**
  * Added guidance for table customization and the settings API.
2026-10-08 13:28:41 +02:00

26 KiB

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:

http://localhost:8901/auth/token

If you configured a custom Web UI path such as web, the login endpoint becomes:

http://localhost:8901/web/auth/token

The protected REST API lives below:

http://localhost:8901/api/v1

or, with a Web UI path:

http://localhost:8901/web/api/v1

Example 1: Get a JWT token

#!/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

#!/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:

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

#!/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

#!/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

Example 5: Diagnose the event bus

#!/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.
  • 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

#!/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

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.

#!/bin/bash

BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"

curl -s -i -X POST "$BASE_URL/api/v1/recording/requests" \
    -H "Authorization: Bearer $TOKEN" \
    -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"
    }'

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.

Typical use:

  • 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

Example 8: Control an existing recording

#!/bin/bash

BASE_URL="http://localhost:8901"
TOKEN="PUT_YOUR_TOKEN_HERE"
REQUEST_ID="PUT_A_REQUEST_ID_HERE"

# 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"

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.

Typical use:

  • leave a shared recording without stopping it for anyone else
  • test recording.manage and recording.delete access

Example 9: Trigger a playlist update

#!/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

Example 10: Preview a Stalker live input in the Web UI playlist API

#!/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.
  • 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 playback resolves a real media URL later through the reverse-proxy path.
  • 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.
  • Runtime refresh only produces http/https playback URLs; Stalker rtmp:// / rtsp:// commands are rejected explicitly rather than proxied half-supported.

Example 11: Dry-run a filter expression against a target

#!/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.

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
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.
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
GET /api/v1/events/stats Event-bus counters and the last 256 events, with the outcome of each
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

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.

Method Path Purpose
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)
POST /api/v1/recording/conflicts/preview Advisory conflict preview (severity, optional provider scope, overlap segments)
GET /api/v1/recording/availability Whether recording is available to the caller
GET /api/v1/recording/health Supervisor and recovery health (admin)
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

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.

DVR WebSocket protocol

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.

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.

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.

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.

Playlist and web-player helpers

Method Path Purpose
POST /api/v1/playlist/live Query live playlist content for the Web UI, including Stalker inputs
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
POST /api/v1/playlist/filter/preview Dry-run a filter DSL expression against a target's stored playlist
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

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:

{
  "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:

{
  "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.

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.

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
  • recording.read
  • recording.create
  • recording.manage
  • recording.delete

If a request is rejected, verify the logged-in Web UI user's RBAC group assignments first.

See also: the full DVR Operator Reference 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.