Skip to content

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.

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.

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

Authorization: Bearer <credential> accepts three different kinds of credential, resolved in this order:

  1. sk_... API token — a long-lived, hashed-at-rest bearer credential for automation and server-to-server calls. Scope is either global (full access, used by the dashboard’s own backend) or app (bound to one app, inherits that app’s tenant). Minted once via deploy/seed-token.js at install time, then managed through /tokens. See Authentication for the full lifecycle.
  2. Dashboard JWT — a short-lived (~12h) session token minted by /auth/login, /auth/signup or /auth/magic/verify, HS256-signed with STREAMHUB_JWT_SECRET. Covers human sign-in: email/password, passwordless magic-link, optional TOTP 2FA, and a break-glass ADMIN_USER/ADMIN_PASS superadmin that can never be locked out.
  3. Anonymous play-tokenGET /apps/:app/play-token/:room is public (no Authorization header at all) and mints a subscribe-only viewer token on the fly. It’s what powers the public /play and /embed pages, and can be turned off per app via the publicPlayback feature flag.
Terminal window
curl -s $BASE/apps \
-H "Authorization: Bearer sk_yourtoken..."

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.

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 | on
  • off — 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.

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-link and POST /auth/reset-request share 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-link additionally enforces a 60-second resend cooldown per email, returned as 429 with a machine-readable retryAfterSeconds:

    { "statusCode": 429, "message": "Please wait 42s before requesting another link.",
    "error": "Too Many Requests", "retryAfterSeconds": 42 }

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.

A first, unauthenticated call — the liveness probe every load balancer hits:

Terminal window
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:

Terminal window
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.