API overview
StreamHub exposes a single REST API that drives both the dashboard and any external
integration — everything the SPA does, an sk_ API token can do too.
Base path
Section titled “Base path”Every endpoint lives under /api/v1, with three deliberate exceptions that are
mounted at the domain root:
| Path | Why it’s outside /api/v1 |
|---|---|
/metrics |
Prometheus scrape convention |
/hls, /samples, /sdk, /play, /embed, /assets |
static/media mounts, not JSON endpoints |
Interactive docs are always available on the running server itself:
- Swagger UI —
/api/v1/docs - Raw OpenAPI JSON —
/api/v1/openapi.json
In the examples on this page and the rest of this section, $BASE stands for
https://<your-domain>/api/v1.
Response envelope
Section titled “Response envelope”Most endpoints return a small, consistent envelope:
{ "data": { "...": "..." }, "error": null }A handful of older/simple endpoints (documented per-route) return the payload directly instead of wrapping it — the endpoint reference pages call this out wherever it applies. Errors don’t use the envelope; they return the matching HTTP status with NestJS’s default error body:
{ "statusCode": 401, "message": "Invalid or revoked API token", "error": "Unauthorized" }Auth planes
Section titled “Auth planes”Authorization: Bearer <credential> accepts three different kinds of
credential, resolved in this order:
sk_...API token — a long-lived, hashed-at-rest bearer credential for automation and server-to-server calls. Scope is eitherglobal(full access, used by the dashboard’s own backend) orapp(bound to one app, inherits that app’s tenant). Minted once viadeploy/seed-token.jsat install time, then managed through/tokens. See Authentication for the full lifecycle.- Dashboard JWT — a short-lived (~12h) session token minted by
/auth/login,/auth/signupor/auth/magic/verify, HS256-signed withSTREAMHUB_JWT_SECRET. Covers human sign-in: email/password, passwordless magic-link, optional TOTP 2FA, and a break-glassADMIN_USER/ADMIN_PASSsuperadmin that can never be locked out. - Anonymous play-token —
GET /apps/:app/play-token/:roomispublic(noAuthorizationheader at all) and mints a subscribe-only viewer token on the fly. It’s what powers the public/playand/embedpages, and can be turned off per app via thepublicPlaybackfeature flag.
curl -s $BASE/apps \ -H "Authorization: Bearer sk_yourtoken..."JWT=$(curl -s -X POST $BASE/auth/login \ -H 'Content-Type: application/json' \ -d '{"user":"alice@example.com","password":"s3cret-passphrase"}' \ | jq -r .data.token)
curl -s $BASE/auth/me -H "Authorization: Bearer $JWT"A handful of routes use a different header instead of Authorization:
POST /cluster/join and POST /cluster/heartbeat authenticate with
X-Cluster-Token: <STREAMHUB_CLUSTER_TOKEN> — see
Global endpoints → Cluster. The LiveKit webhook sink
(POST /webhooks/livekit) verifies a LiveKit-issued signature instead of a
Bearer token — see Webhooks.
Authorization (RBAC + quotas)
Section titled “Authorization (RBAC + quotas)”Every route also declares a Casbin resource:action permission (shown in the
tables on the following pages, e.g. stream:write, config:read). Whether
that permission is actually enforced is controlled by one env var:
STREAMHUB_AUTHZ_ENFORCE = off | log | onoff— no checks at all (useful while migrating an existing deployment).log— the default: a would-be denial is logged but the request still succeeds, so you can see what a stricter mode would reject.on— denials are enforced (403 Forbidden).
Superadmin sessions and global-scope API tokens bypass tenant scoping and
Casbin entirely — they’re the platform-operator escape hatch. Tenant quotas
(max apps, max concurrent streams, etc.) are enforced independently of the
STREAMHUB_AUTHZ_ENFORCE phase; see GET /tenants/:id/usage.
Rate limits
Section titled “Rate limits”Login (POST /auth/login) itself has no dedicated per-request throttle — it
relies on password hashing cost and, when enabled, a required TOTP code.
Passwordless/recovery flows are rate-limited, since they email a link to an
address the caller doesn’t have to prove ownership of yet:
-
POST /auth/magic-linkandPOST /auth/reset-requestshare the same sliding-window limiter: max 3 requests per email and max 10 requests per IP, both within a 15-minute window. Over the limit, the endpoint still returns its normal generic 200 (it never reveals whether the email exists) — the request is just silently not dispatched. -
POST /auth/magic-linkadditionally enforces a 60-second resend cooldown per email, returned as429with a machine-readableretryAfterSeconds:{ "statusCode": 429, "message": "Please wait 42s before requesting another link.","error": "Too Many Requests", "retryAfterSeconds": 42 }
/metrics is deny-by-default
Section titled “/metrics is deny-by-default”The Prometheus scrape endpoint at the domain root (/metrics, not under
/api/v1) is disabled unless METRICS_TOKEN is set. Without it, the
route returns a plain 404 — not 401/403 — so an anonymous scanner can’t
even confirm it exists. With a token configured, a matching
Authorization: Bearer <token> (or ?token= query param) is required, and a
wrong one gets 403.
Try it
Section titled “Try it”A first, unauthenticated call — the liveness probe every load balancer hits:
curl -s https://your-domain.example.com/api/v1/health{ "status": "ok", "up": true, "version": "0.1.0", "ts": "2026-06-30T12:00:00.000Z", "uptimeSeconds": 1234 }And an authenticated one, listing the apps visible to your token:
curl -s https://your-domain.example.com/api/v1/apps \ -H "Authorization: Bearer $STREAMHUB_TOKEN"Continue to Authentication to mint that token, or jump straight to the Global and App endpoint reference.