Skip to main content

Sphere API

The Sphere web app is a plain client of a REST API under /api/v1. Everything the app does, you can do with the API. The full endpoint reference is the Sphere API reference, rendered from entrosity-sphere.backend/api/openapi.yaml, the spec the server's router and request validation (and the app's TypeScript types) are generated from.

Conventions​

  • Base URL: https://hub.entrosity.com/sphere/api/v1 (the proxy strips /sphere). The connector API is under /sphere/api/connector/v1 (Sphere connector protocol).
  • Authentication: Authorization: Bearer <product token>, a five-minute token from Entrosity Hub for the product sphere (POST /api/platform/v1/auth/product-token {"product":"sphere"} with the Hub's session, Hub API). Sphere has no sign-in of its own; users, tenants and roles come from the Hub. While Sphere is in beta, the Hub issues its tokens to platform admins only.
  • Access rules: every route has a rule in entrosity-sphere.backend/internal/http/portal/access.go: public, signed in, global admin, or a tenant permission (Roles and permissions). Tenant users may only call /tenants/{their tenant id}/…; any other tenant answers 404. A missing permission answers 403. Two routes check a second permission in the handler: a playback (kind: playback) also needs playback:view, and a shared view also needs views:manage.
  • JSON bodies with snake_case keys, at most 1 MB. Request schemas refuse unknown fields (422).
  • Lists: the audit logs and the admin tenant list take ?page=&page_size= (default 50, maximum 200) and return items, page, page_size and total. Other lists return all items.
  • Alarm log: GET /tenants/{tenantID}/alarms is newest first with keyset pagination: limit (1–500, default 100), filters from, to, nvr_id, camera_id, acked and code (repeat for several, at most 20). When the page is full, the answer has next_before_at and next_before_id: pass them as before_at and before_id for the next page.
  • Rate limit: 20 requests per second per client IP, burst 60 (SPHERE_API_RATE_PER_SECOND, SPHERE_API_RATE_BURST); 429 beyond.
  • Every response carries X-Request-ID.

Errors​

Errors are RFC 7807 application/problem+json with a stable code:

{
"type": "/problems/stream_limit",
"title": "Conflict",
"status": 409,
"code": "stream_limit",
"detail": "too many streams are open for this site or NVR; close some first",
"request_id": "…"
}

Validation errors have code: "validation" and fields with a message per field. The conflicts you meet most:

CodeWhen
stream_limitPOST /streams: the connector's, or the NVR's main-stream or playback, limit is reached and no idle live stream can stop to make room (Limits).
nvr_offlinePOST /streams, PTZ, recording searches: the NVR is disconnected or not online (also while its connector is offline). POST /nvrs/{nvrID}/tune-live: the NVR is disconnected.
camera_disabledPOST /streams: the camera is switched off.
connector_offlinePOST /nvrs/{nvrID}/test, POST /nvrs/{nvrID}/tune-live: the NVR's connector is not connected.
nvr_address_takenAdding or changing an NVR: the connector already drives an NVR at that host and port.
connector_has_nvrsRemoving a connector that still drives NVRs.

All codes: Error codes.

Watching video​

Video does not go through the API: the API hands out a viewer session with a short-lived token, and the browser plays the stream from the media server over WebRTC (WHEP).

  1. Open: POST /tenants/{tenantID}/streams with camera_id and:

    • live: kind: "live" (the default) and profile sub (the default, for grids) or main (full resolution);
    • playback: kind: "playback", start and end (after start, at most 24 hours later, start not in the future).

    The answer (201) is a StreamSession: its id (the viewer session), the media path, whep_url, a token and token_expires_at (five minutes), and the stream's state and error.

  2. Play: send the WebRTC offer to whep_url (WHEP) with Authorization: Bearer <token>. The token is checked when the WHEP session opens, and only for that path.

  3. Keep alive: POST /tenants/{tenantID}/streams/{streamID}/keepalive every 15 seconds. It answers the session again with the current state and a fresh token (use it if the player has to reconnect). A viewer without a keepalive for 45 seconds is dropped.

  4. Close: DELETE /tenants/{tenantID}/streams/{streamID} (204). A live stream nobody watches keeps running for its idle grace period, 5 minutes for a sub stream and 2 minutes for a main stream (SPHERE_LIVE_IDLE_GRACE, SPHERE_LIVE_IDLE_GRACE_MAIN), so a viewer switching back to the camera joins it without a new start on the NVR; at a stream limit, the idle stream unwatched the longest stops to make room. A playback stops as soon as its viewer is gone, at the next streams.sweep (within 10 seconds), also when the viewer was dropped for missing keepalives. The web app closes its sessions when the page unloads (a keepalive request on pagehide), so a reload or a closed tab frees them at once.

stateMeaning
startingWanted; the connector has not published it yet (also while its connector reconnects).
liveThe connector publishes it (from the moment its start job succeeds): play it.
failedThe connector could not start it; error says why. The next viewer who opens it starts it again.
stoppedNobody wants it any more (closed, or the NVR was disconnected), or a playback reached its end.
  • A live stream (t/<tenant>/live/<camera>/<main|sub>) is shared: the first viewer starts it on the connector, later viewers join it, and it is pulled from the NVR once whatever the number of viewers. A playback (t/<tenant>/pb/<session>) is the viewer's own.
  • A viewer session belongs to the user who opened it: another user's session answers 404.
  • Changes of state also arrive as stream.state events (below).

Asynchronous operations​

Some calls start a job on the connector and return the job at once (202):

CallJobResult
POST /tenants/{tenantID}/nvrs/{nvrID}/testsphere.nvr.test{info, rtsp_reachable, warnings}; on success the NVR's details and cameras are updated.
POST /tenants/{tenantID}/cameras/{cameraID}/recordings {from, to} (at most 7 days)sphere.recordings.find{segments: [{start, end, type, size_bytes}], truncated}
POST /tenants/{tenantID}/cameras/{cameraID}/ptz {code, action, speed?, preset?}sphere.ptznone
POST /tenants/{tenantID}/nvrs/{nvrID}/tune-live {keyframe_seconds?}sphere.nvr.tune_liveNvrLiveTuneResult: {channels: [{channel, fps, gop_before, gop_after, changed, error, measured_ms}]} (measured_ms: the interval measured in the sub stream; a camera that keeps a longer one is changed: false)

Poll GET /tenants/{tenantID}/jobs/{jobID} until status is succeeded, failed, timeout or cancelled (or watch job.update); result, error_code and error carry the outcome.

  • PTZ: a movement (Up, Down, Left, Right, the diagonals, ZoomTele, ZoomWide, FocusNear, FocusFar, IrisLarge, IrisSmall) runs from action: "start" until action: "stop" (hold-to-move); speed is 1–8. GotoPreset, SetPreset and ClearPreset use start only, with preset 1–255. A command expires after 5 seconds, so a late one never moves the camera. Cameras the NVR does not report as PTZ are not refused; the command fails on the connector if the camera cannot do it.
  • NVR state: adding an NVR, connect and disconnect (POST /nvrs/{nvrID}/connect, …/disconnect), and, for a connected NVR, a new address or time zone and new credentials (PUT /nvrs/{nvrID}/credentials) each queue a sphere.nvr.apply job by themselves; a newer one replaces an older one still waiting. GET /tenants/{tenantID}/nvrs/{nvrID}/jobs lists the NVR's last 50 jobs.
  • Optimise for live view: tune-live (nvrs:manage, audited as nvr.tune_live) sets each camera's sub-stream keyframe interval on the NVR to at most keyframe_seconds (1–4, default 1; the body is optional) × its frame rate. Main streams are never changed, shorter intervals are kept. Per channel, fps is the sub stream's frame rate, gop_before and gop_after the interval in frames, changed whether the NVR took the new one (false without error: already short enough), and error why not (also when the NVR accepted a value it did not keep). 422 validation for another keyframe_seconds, 409 nvr_offline when the NVR is disconnected, 409 connector_offline when its connector is offline (NVRs).
  • NVR passwords are write-only: they are accepted by POST /nvrs and PUT /nvrs/{nvrID}/credentials and never returned.

Live updates​

The app receives live updates as server-sent events:

  1. POST /auth/sse-token {"tenant_id": …} returns a token valid for 60 seconds, bound to the tenant and the session.
  2. GET /tenants/{tenantID}/stream?sse_token=<token> (or with the bearer token) streams text/event-stream, with a keep-alive comment every 25 seconds.
EventData
alarm.newAn array of new alarms {id, occurred_at, code, action, channel, nvr_id, camera_id} (up to 50 per event).
alarm.ack{ids?, before?}: alarms were acknowledged, by id or up to a moment.
nvr.status{nvr_id, status, error?, changed?}; changed means the NVR's details or cameras changed (read them again).
stream.state{path, state, error?}
connector.status{connector_id, status} (online, offline, deleted)
job.update{id, connector_id, nvr_id?, type, status, error_code?, finished_at?}: a connector job changed state.

Alarms and views​

  • POST /tenants/{tenantID}/alarms/ack acknowledges alarms by ids (at most 500), or every alarm up to before, or both; it answers {acked} (how many) and sends alarm.ack. Acknowledging records who and when; alarms are never changed otherwise.
  • Saved views (/tenants/{tenantID}/views): name, layout (the split by its tile count: 1, 4, 6, 8, 9, 16, 25, 36 or 64, where 6 and 8 are "one big + 5" and "one big + 7"), cells (a camera per cell, in order, at most 64; null for an empty cell), shared. A view is the user's own, or shared with the tenant (views:manage to save, change or delete). Another user's own views answer 404.

Connector installers​

  • GET /tenants/{tenantID}/connector-release (nvrs:manage) answers the newest connector release of SPHERE_CONNECTOR_UPDATE_CHANNEL, a ConnectorRelease: {version, channel, sha256, size_bytes, notes, created_at, download_url, download_expires_at}. download_url needs no sign-in and works for one hour; every call makes a fresh one. 404 when no release is stored (or releases are off). Download connector uses it.
  • New enrollment tokens (POST /tenants/{tenantID}/enrollment-tokens) carry the same kind of link in download_url when a release is stored, else SPHERE_CONNECTOR_DOWNLOAD_URL (may be empty).

The installers themselves are served by the release API, /api/releases/v1 (https://hub.entrosity.com/sphere/api/releases/v1), outside /api/v1 and its sign-in:

Method and pathPurpose
POST /connector?version=&channel=&notes=CI uploads a build: the body is the MSI (at most 64 MiB), Authorization: Bearer <SPHERE_RELEASE_TOKEN>, channel stable (default) or dev. 201 {id, version, channel, sha256, size_bytes, created_at}; 401 wrong token; 409 release_exists (the version is already published); 422 invalid version, channel or size; 503 releases_disabled (no SPHERE_RELEASE_SIGNING_KEY).
GET /connector/{releaseID}/{file}?t=<link token>Downloads the installer (application/x-msi, sphere-connector-<version>.msi). t is an expiry and an HMAC; a wrong or expired link answers 404.

Setting up and rollout: Running Entrosity Sphere → Connector releases.

Sensitive actions​

Deleting an enrollment token permanently (POST /tenants/{tenantID}/enrollment-tokens/{tokenID}/delete) is for global admins and needs a step_up_token: the Hub issues it for the admin's password (POST /api/platform/v1/auth/step-up, product sphere, Hub API).