Reference
API
Every endpoint below exists in the deployed build. The list was generated from the router source and then reconciled against the running service by probing each path.
Authentication
Obtain a bearer token, then send it on every other request.
curl -X POST https://<your-core>/auth/login \
-H 'content-type: application/json' \
-d '{"email":"operator@example.gov","password":"..."}'
# => {"access_token":"eyJhbGci...","role":"operator"}
curl https://<your-core>/cameras \
-H 'Authorization: Bearer eyJhbGci...'
Tokens are signed and carry a shift-length expiry. The caller's role is re-read from the database on every request rather than trusted from the token, so revocation is immediate.
Machine-to-machine ingest
Device endpoints — /events/ingest, /nodes/heartbeat,
/wearables/capture, /tscm/ingest/* — do not use bearer tokens. They
require an HMAC-SHA256 signature over the raw request body under a shared node secret:
X-Pharoah-Signature: hex(HMAC-SHA256(raw_body, NODE_HMAC_SECRET))
This is one scheme for every device class, not one per device. If the secret is not configured on a deployment, these endpoints refuse all traffic rather than accepting unsigned posts.
Status codes
| Code | Meaning |
|---|---|
200/201 | Success. |
204 | Deleted. |
400 | Malformed request, or a required consent flag not given. |
401 | Missing, malformed, expired or revoked credentials. Every protected route answers this unauthenticated. |
403 | Authenticated but the role lacks the scope, or the add-on module is not enabled for this deployment. |
404 | No such object — or no such route. |
409 | A governance control refused the operation. The masking-required-but-unconfigured export is the one to know. |
422 | Body failed validation (bad mask geometry, unknown legal profile, missing device binding). |
502 | A downstream the request depends on answered badly. |
503 | A capability is enabled but not configured on this deployment, or an external lookup could not be performed. Never treat this as a negative result. |
Module gating
Some surfaces are add-on modules. When a module is not enabled for a deployment, its routes
answer 403 {"detail": "module '<key>' not enabled for this deployment"}. The
modules are: nvr_recording, ai_detection, governance,
anonymisation, attestation, live_view,
edge_nodes, wearable_capture, tscm_rf_sweep. Call
GET /modules to see what your own deployment has.
A module being enabled means the routes are open. It does not mean an external dependency is attached — see deliberate limits.
Endpoints
“Scope” is the permission the calling role must hold. HMAC marks device
endpoints that take a signature instead of a token.
| Method | Path | Scope | Behaviour |
|---|---|---|---|
GET | /health | none | Liveness. The only route reachable without a token. Returns {status, version}. |
POST | /auth/login | none | Exchange {email, password} for a bearer token. 400 if either is missing, 401 on bad credentials. |
GET | /auth/whoami | any token | The calling principal: id, email, role. |
GET | /cameras | camera:read | List cameras. |
POST | /cameras | camera:write | Create a camera. Applies the legal profile's defaults, validates retention against its ceiling, rejects forbidden zone placements, audit-logs the write. |
GET | /cameras/{id} | camera:read | One camera. 404 if absent. |
PUT | /cameras/{id} | camera:write | Update a camera, re-validating profile defaults and retention. Also where privacy-mask rectangles are set. |
DELETE | /cameras/{id} | camera:write | Delete a camera. Audit-logged. 204. |
GET | /cameras/{id}/signage | camera:read | The statutory video-surveillance notice for this camera's jurisdiction, carrying the deployment's controller identity, purpose and rights contact. |
POST | /cameras/onvif-discover | camera:write | Resolve a camera's RTSP URI over ONVIF so it can be registered without hand-typing it. Stores nothing; the password is never logged. 503 if the optional ONVIF engine is not installed. |
GET | /events | event:read | List events, most recent first. Filter by camera_id, type, since, until, limit. |
POST | /events | event:write | Record an event and immediately run the alert rule engine against it. |
POST | /events/ingest | HMAC | Edge-node ingest. Signed with X-Pharoah-Signature, not a bearer token. Refuses everything unsigned. |
GET | /alerts | alert:read | List alerts, most recent first. Filter by status, severity. |
POST | /alerts/{id}/ack | alert:ack | Acknowledge an alert. Audit-logged. |
GET | /cameras/{id}/recordings | recording:read | List a camera's clips. |
GET | /cameras/{id}/recordings/{clip_id} | recording:read | One clip descriptor. |
POST | /recordings/{clip_id}/export | recording:export | Export a clip to the export store, painting in the camera's mask rectangles and optionally running face-blur. Hashes exactly the bytes handed over and writes an attestation naming the recipient. 409 when the camera's profile requires masking but none is configured — it is never exported unmasked. |
GET | /recordings/{clip_id}/playback | recording:read | Governed playback descriptor carrying a short-lived clip-scoped token. |
GET | /recordings/{clip_id}/playback/media | playback token | The governed media itself, gated by the descriptor's token rather than a bearer header. |
GET | /streams/{camera_id} | stream:view | Stream descriptor. The RTSP URL is always credential-redacted. Returns an HLS URL only when a stream gateway is configured; otherwise gateway_enabled: false. |
GET | /streams/{camera_id}/hls/{path} | stream token | Token-gated HLS proxy to the localhost-bound gateway. The token is the auth because a <video> element cannot send headers. |
GET | /streams/gateway/config | camera:write | The generated gateway config. The one surface that emits raw RTSP credentials, so it carries the strictest scope. |
POST | /streams/gateway/sync | camera:write | Reconcile the live gateway against the camera registry. |
GET | /attestations | audit:read | The tamper-evidence ledger: export and capture hashes with their external-ledger status. |
POST | /attestations/{id}/submit-proof | recording:export | Re-submit a pending attestation to the external evidence ledger. 503 when no ledger is configured. |
GET | /compliance/dpia | audit:read | GDPR Article 35(7) DPIA built live from the camera inventory and runtime configuration. format=json|markdown. Audit-logged. |
GET | /compliance/matrix | audit:read | Per-jurisdiction requirement-to-control crosswalk, tender-annex ready. format=json|markdown. |
POST | /subject-rights | camera:read | Record a data-subject request. |
GET | /subject-rights | audit:read | List subject requests. |
GET | /subject-rights/{id} | audit:read | One subject request. |
POST | /subject-rights/{id}/fulfil | recording:export | Fulfil a right-of-access request, disclosing under the same masking rules the export pipeline enforces. |
GET | /audit | audit:read | Query the audit log by user_id, action, since, until. |
GET | /modules | audit:read | The add-on module catalogue with this deployment's enabled state. |
GET | /products | audit:read | Product profile overlays available to this deployment. |
GET | /products/{key} | audit:read | One product profile. |
GET | /products/{key}/features/{feature} | audit:read | Whether a named feature is active under that profile. |
GET | /nodes | camera:read | Edge-node roster with liveness. |
POST | /nodes/heartbeat | HMAC | Node heartbeat. Silence past the threshold flips the node offline and raises one alert per outage. |
POST | /wearables/register | camera:write | Register or update a wearable capture device, bound to a legal profile. |
GET | /wearables | camera:read | Wearable device roster and liveness. |
POST | /wearables/capture | HMAC | Signed capture ingest (multipart or JSON+base64). Persists the media, attests it, records an event, and audit-logs the device and content hash — never the media. |
GET | /wearables/captures/{event_id}/playback/media | playback token | Governed capture playback. Refuses any capture whose governance binding cannot be resolved. |
POST | /tscm/sweeps | camera:write | Open a counter-surveillance sweep. |
GET | /tscm/sweeps | camera:read | List sweeps. |
GET | /tscm/sweeps/{id}/report | audit:read | The generated sweep report. |
POST | /tscm/ingest/rf | HMAC | Signed RF capture ingest from sweep hardware. |
GET | /operator/location | camera:read | The operator location that resolves which jurisdiction governs this kit. May legitimately answer “unresolved”. |
PUT | /operator/location | camera:write | Set the operator location. |
GET | /nearby/privacy-notice | camera:read | Exactly what a nearby lookup would disclose, with zero egress, so consent is informed before anything is sent. |
GET | /nearby/surveillance | camera:read | Cameras mapped in OpenStreetMap around a point. Requires consent=true. Three outcomes, three codes: 200 we looked and found none, 400 you have not opted in and nothing left the device, 503 we could not look — never an all-clear. |
GET | /estate/brief | audit:read | Rolled-up estate posture brief. |
POST | /estate/brief/dispatch | audit:read | Dispatch the brief to the configured channel. |
Interactive schema
The generated OpenAPI schema and its browser are switched off on production deployments, so the full surface is not publicly enumerable. They can be enabled on an evaluation deployment on request; this page is the reference in the meantime.