Skip to content

Global endpoints

This content is for the 1.0 version. Switch to the latest version for up-to-date documentation.

Endpoints on this page are not scoped to a single app — they manage the platform itself: the apps registry, API tokens, the edge-node cluster, and server observability. Everything requires Authorization: Bearer <credential> except /health. See Authentication for how to get a token, and App endpoints for everything scoped to one app under /apps/{app}/....

$BASE = https://<your-domain>/api/v1.

Apps are StreamHub’s tenants: each owns its own room prefix, config, database and S3 bucket. GET /apps is tenant-scoped — an app-scope token or a non-superadmin user only ever sees apps belonging to their own tenant; a global-scope token or superadmin session sees every app on the platform.

Method Path Permission Description
GET /apps app:read List apps visible to the caller (own tenant; superadmin/global sees all)
POST /apps app:create Create an app — scaffolds apps/<name>/ (config.yaml, app.db, recordings/, snapshots/, samples/)
GET /apps/{name} app:read Get one app by name
GET /apps/{name}/sizes app:read Storage footprint: app.db size + total VOD bytes/count
PATCH /apps/{name} config:write Edit the app’s top-level config (display name, room prefix, recording toggle, callback URL/secret, feature flags)
DELETE /apps/{name}?deleteVods app:delete Delete an app; deleteVods=true also purges its VODs (DB + S3 + local files)
Field Type Required Rules
name string yes Lowercase slug ^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$, unique
displayName string no ≤ 100 chars
roomPrefix string no ≤ 40 chars; defaults to name
Terminal window
curl -s -X POST $BASE/apps \
-H "Authorization: Bearer $STREAMHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"demo","displayName":"Demo","roomPrefix":"demo"}'

New apps are created against your own tenant automatically (a superadmin or global-scope token creates platform-owned apps).

Only the most commonly edited top-level fields — the full transcoding/webrtc/ rtmp config is patched separately through the per-app PATCH /apps/{app}/config. S3 credentials are never accepted here (use PUT /apps/{name}/s3 below).

Field Type Notes
displayName string ≤ 100
roomPrefix string slug ^[a-z0-9][a-z0-9-]{0,39}$
recordingEnabled boolean toggles recording.enabled
splitMinutes / snapshotSeconds number recording split / snapshot cadence
callbackUrl string ≤ 2048; outbound callback URL
callbackSecret string ≤ 256; HMAC signing secret — see Webhooks
features object per-app feature flags (partial patch, merged)
Terminal window
curl -s -X PATCH $BASE/apps/live \
-H "Authorization: Bearer $STREAMHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"recordingEnabled":true,"callbackUrl":"https://hooks.example.com/streamhub"}'
Terminal window
curl -s -X DELETE "$BASE/apps/demo?deleteVods=true" \
-H "Authorization: Bearer $STREAMHUB_TOKEN"
# { "deleted": true, "name": "demo" }

For the full config.yaml reference see the Configuration section; these routes read/write it.

Method Path Permission Description
GET /apps/{name}/config/raw config:read Raw YAML text
PUT /apps/{name}/config/raw config:write Validate + backup + write + hot-reload; 400 on parse error (nothing is written)
POST /apps/{name}/config/raw/validate config:read Dry-run: validate + return a diff vs. current, without writing
GET /apps/{name}/config/backups config:read List timestamped backups, newest first
GET /apps/{name}/config/backups/{ts} config:read Read one backup’s verbatim YAML
POST /apps/{name}/config/backups/{ts}/revert config:write Restore a backup as the live config (current is itself backed up first) + hot-reload
POST /apps/{name}/reload config:write Hot-reload: re-read config.yaml + re-init the S3 client, no process restart
Terminal window
curl -s -X PUT $BASE/apps/live/config/raw \
-H "Authorization: Bearer $STREAMHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"yaml":"name: live\nroom_prefix: live\n..."}'
# { "data": { "reloaded": true, "warnings": [] } }

Three built-in, declarative delivery profiles you can apply in one call. A preset is deep-merged over the app’s current config and hot-reloaded; it never touches credentials or identity fields (s3, callbacks, name, display_name, room_prefix are stripped from every preset patch before the merge).

Method Path Permission Description
GET /apps/{name}/presets config:read List the built-in presets with what each one sets
POST /apps/{name}/presets/{preset}/apply config:write Apply a preset (deep-merge + backup + hot-reload); returns the diff
Preset id Profile
low-latency WebRTC-first, passthrough (no server re-encode), simulcast on, edge distribution. Sub-second interactive playback — CCTV, auctions, telemedicine, live-shopping.
high-quality-recording Transcoding on, H.264 (optionally + VP8/WebM alternate), room-composite recording, adaptive HLS VOD ladder (1080/720/480). Prioritizes archival quality over latency.
mass-audience-HLS Transcoded HLS ladder (720/480/360) behind a CDN (distribution.mode: cdn), longer segment/list window. Scales to large audiences at 6-15s latency.
Terminal window
curl -s -X POST $BASE/apps/live/presets/low-latency/apply \
-H "Authorization: Bearer $STREAMHUB_TOKEN"
# { "data": { "preset": "low-latency", "applied": true, "reloaded": true, "changed": [...], "diff": "...", "warnings": [] } }
Method Path Permission Description
GET /apps/{name}/s3 s3:read Get the app’s S3 config, credentials masked
PUT /apps/{name}/s3 s3:write Set the S3 block + credentials, then re-init the app’s S3 client

PUT /apps/{name}/s3 body — every field optional (patch what changed):

Field Type Notes
provider aws wasabi
bucket string ≤ 255
region string ≤ 64
endpoint string ≤ 255, e.g. https://s3.us-east-1.wasabisys.com
forcePathStyle boolean path-style addressing (needed for MinIO)
prefix string ≤ 255, object-key prefix, e.g. streamhub/live
public_url string ≤ 255; public/CDN base — when set, VOD URLs become <public_url>/<key> instead of presigned
key / secret string S3 access key/secret — written to data/secrets.json (never the YAML)
confirmPublic boolean required true to enable a non-empty public_url (makes recordings publicly accessible, not presigned); not needed to clear it
Terminal window
curl -s -X PUT $BASE/apps/live/s3 \
-H "Authorization: Bearer $STREAMHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider":"wasabi","bucket":"my-bucket","region":"us-east-1","endpoint":"https://s3.us-east-1.wasabisys.com","key":"AKIA...","secret":"...","prefix":"streamhub/live"}'

Covered in full on Authentication.

Method Path Permission Description
GET /tokens Bearer List tokens (hashes/plaintext never returned)
POST /tokens Bearer Create a token; plaintext returned once
DELETE /tokens/{id} Bearer Revoke (soft-delete) → 204

Edge-node registry backing the one-liner installer and the dashboard’s cluster manager. POST /cluster/join and POST /cluster/heartbeat are node- facing and authenticate with X-Cluster-Token: <STREAMHUB_CLUSTER_TOKEN>not a Bearer token; both return 503 if that env var is unset. Every other route below is Bearer, global-scope only (an app-scope token gets 403).

Method Path Auth Description
POST /cluster/join X-Cluster-Token Register (or refresh) an edge node — idempotent by name; hands back bootstrap config (LiveKit creds, Redis/WS URLs)
POST /cluster/heartbeat X-Cluster-Token Liveness ping for an already-joined node, optional stats blob (capped ~4KB, 413 over)
GET /cluster/info Bearer (global) Cluster overview: enabled?, node count, cluster token, a ready-to-copy join one-liner
GET /cluster/nodes Bearer (global) List nodes with parsed last-heartbeat stats and a derived stale flag (true after 90s with no heartbeat)
PATCH /cluster/nodes/{id} Bearer (global) Update a node’s name/region/status (active
DELETE /cluster/nodes/{id} Bearer (global) Remove a node from the registry

POST /cluster/join body:

Field Type Required Rules
name string yes ^[a-zA-Z0-9._-]+$, 1-64 chars — the idempotency key
ip string yes valid IPv4 or IPv6
region string no ≤ 64
url string no ≤ 255; public URL, falls back to ip
Terminal window
curl -s -X POST $BASE/cluster/join \
-H "X-Cluster-Token: $STREAMHUB_CLUSTER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"edge-fra-1","ip":"203.0.113.10","region":"eu-central"}'
{
"data": {
"nodeId": "4b2f0c2e-1a3d-4c5e-8f9a-0b1c2d3e4f5a",
"name": "edge-fra-1",
"redisUrl": "redis://cluster-redis:6379",
"publicWsUrl": "wss://media.example.com",
"livekit": { "apiKey": "API…", "apiSecret": "", "wsUrl": "ws://127.0.0.1:7880" }
},
"error": null
}
Terminal window
curl -s -X POST $BASE/cluster/heartbeat \
-H "X-Cluster-Token: $STREAMHUB_CLUSTER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"nodeId":"4b2f0c2e-1a3d-4c5e-8f9a-0b1c2d3e4f5a","stats":{"cpu":0.42,"activeStreams":3}}'
Method Path Auth Description
GET /stats Bearer Server stats: CPU/mem/disk, uptime, LiveKit reachability, counts of apps/rooms/active streams, egress/ingress status
GET /system/settings Bearer (global) Read-only effective config with secrets redacted (booleans + masked keys only), plus per-group upgrade guidance
GET /logs Bearer Query structured server logs, paginated (app, level, source, q, since, until, limit, offset)
GET /system/gpu?refresh Bearer GPU/hwaccel detection status
POST /system/gpu/refresh Bearer Force a GPU re-probe
GET /health public Liveness probe
Terminal window
curl -s $BASE/stats -H "Authorization: Bearer $STREAMHUB_TOKEN"
{
"ts": "2026-06-30T12:00:00.000Z", "uptimeSeconds": 1234, "version": "0.1.0",
"cpu": { "loadAvg": [0.5, 0.4, 0.3], "cores": 8 },
"memory": { "totalBytes": 16777216000, "freeBytes": 8388608000, "usedBytes": 8388608000 },
"livekitReachable": true,
"counts": { "apps": 3, "rooms": 2, "activeStreams": 4 },
"egress": { "reachable": true, "active": 1, "total": 2 },
"ingress": { "reachable": true, "active": 1, "total": 2 }
}

GET /system/settings powers the dashboard’s “Server settings” panel: it only shows config and prints copy-paste commands to change it — there is no corresponding write endpoint. authzEnforce is returned verbatim (it’s a security mode, not a secret); JWT/API/admin/cluster/SMTP secrets and the Redis password are never returned, only …Set booleans and a masked API key. Anonymous callers get 401; an app-scope token gets 403.