Global endpoints
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 (registry)
Section titled “Apps (registry)”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) |
POST /apps — body
Section titled “POST /apps — body”| 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 |
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).
PATCH /apps/{name} — body
Section titled “PATCH /apps/{name} — body”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) |
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"}'DELETE /apps/{name}
Section titled “DELETE /apps/{name}”curl -s -X DELETE "$BASE/apps/demo?deleteVods=true" \ -H "Authorization: Bearer $STREAMHUB_TOKEN"# { "deleted": true, "name": "demo" }App config: raw editor, backups, presets
Section titled “App config: raw editor, backups, presets”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 |
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": [] } }Config presets
Section titled “Config presets”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. |
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": [] } }App S3
Section titled “App S3”| 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 | minio |
|
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 |
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"}'API tokens
Section titled “API tokens”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 |
Cluster
Section titled “Cluster”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|draining|disabled) |
| 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 |
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}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}}'Stats & settings
Section titled “Stats & settings”| 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 |
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.