PHAROAH / Security Core

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.

Base URL issued per deployment Transport HTTPS only Bodies JSON

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

CodeMeaning
200/201Success.
204Deleted.
400Malformed request, or a required consent flag not given.
401Missing, malformed, expired or revoked credentials. Every protected route answers this unauthenticated.
403Authenticated but the role lacks the scope, or the add-on module is not enabled for this deployment.
404No such object — or no such route.
409A governance control refused the operation. The masking-required-but-unconfigured export is the one to know.
422Body failed validation (bad mask geometry, unknown legal profile, missing device binding).
502A downstream the request depends on answered badly.
503A 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.

MethodPathScopeBehaviour
GET/healthnoneLiveness. The only route reachable without a token. Returns {status, version}.
POST/auth/loginnoneExchange {email, password} for a bearer token. 400 if either is missing, 401 on bad credentials.
GET/auth/whoamiany tokenThe calling principal: id, email, role.
GET/camerascamera:readList cameras.
POST/camerascamera:writeCreate 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:readOne camera. 404 if absent.
PUT/cameras/{id}camera:writeUpdate a camera, re-validating profile defaults and retention. Also where privacy-mask rectangles are set.
DELETE/cameras/{id}camera:writeDelete a camera. Audit-logged. 204.
GET/cameras/{id}/signagecamera:readThe statutory video-surveillance notice for this camera's jurisdiction, carrying the deployment's controller identity, purpose and rights contact.
POST/cameras/onvif-discovercamera:writeResolve 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/eventsevent:readList events, most recent first. Filter by camera_id, type, since, until, limit.
POST/eventsevent:writeRecord an event and immediately run the alert rule engine against it.
POST/events/ingestHMACEdge-node ingest. Signed with X-Pharoah-Signature, not a bearer token. Refuses everything unsigned.
GET/alertsalert:readList alerts, most recent first. Filter by status, severity.
POST/alerts/{id}/ackalert:ackAcknowledge an alert. Audit-logged.
GET/cameras/{id}/recordingsrecording:readList a camera's clips.
GET/cameras/{id}/recordings/{clip_id}recording:readOne clip descriptor.
POST/recordings/{clip_id}/exportrecording:exportExport 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}/playbackrecording:readGoverned playback descriptor carrying a short-lived clip-scoped token.
GET/recordings/{clip_id}/playback/mediaplayback tokenThe governed media itself, gated by the descriptor's token rather than a bearer header.
GET/streams/{camera_id}stream:viewStream 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 tokenToken-gated HLS proxy to the localhost-bound gateway. The token is the auth because a <video> element cannot send headers.
GET/streams/gateway/configcamera:writeThe generated gateway config. The one surface that emits raw RTSP credentials, so it carries the strictest scope.
POST/streams/gateway/synccamera:writeReconcile the live gateway against the camera registry.
GET/attestationsaudit:readThe tamper-evidence ledger: export and capture hashes with their external-ledger status.
POST/attestations/{id}/submit-proofrecording:exportRe-submit a pending attestation to the external evidence ledger. 503 when no ledger is configured.
GET/compliance/dpiaaudit:readGDPR Article 35(7) DPIA built live from the camera inventory and runtime configuration. format=json|markdown. Audit-logged.
GET/compliance/matrixaudit:readPer-jurisdiction requirement-to-control crosswalk, tender-annex ready. format=json|markdown.
POST/subject-rightscamera:readRecord a data-subject request.
GET/subject-rightsaudit:readList subject requests.
GET/subject-rights/{id}audit:readOne subject request.
POST/subject-rights/{id}/fulfilrecording:exportFulfil a right-of-access request, disclosing under the same masking rules the export pipeline enforces.
GET/auditaudit:readQuery the audit log by user_id, action, since, until.
GET/modulesaudit:readThe add-on module catalogue with this deployment's enabled state.
GET/productsaudit:readProduct profile overlays available to this deployment.
GET/products/{key}audit:readOne product profile.
GET/products/{key}/features/{feature}audit:readWhether a named feature is active under that profile.
GET/nodescamera:readEdge-node roster with liveness.
POST/nodes/heartbeatHMACNode heartbeat. Silence past the threshold flips the node offline and raises one alert per outage.
POST/wearables/registercamera:writeRegister or update a wearable capture device, bound to a legal profile.
GET/wearablescamera:readWearable device roster and liveness.
POST/wearables/captureHMACSigned 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/mediaplayback tokenGoverned capture playback. Refuses any capture whose governance binding cannot be resolved.
POST/tscm/sweepscamera:writeOpen a counter-surveillance sweep.
GET/tscm/sweepscamera:readList sweeps.
GET/tscm/sweeps/{id}/reportaudit:readThe generated sweep report.
POST/tscm/ingest/rfHMACSigned RF capture ingest from sweep hardware.
GET/operator/locationcamera:readThe operator location that resolves which jurisdiction governs this kit. May legitimately answer “unresolved”.
PUT/operator/locationcamera:writeSet the operator location.
GET/nearby/privacy-noticecamera:readExactly what a nearby lookup would disclose, with zero egress, so consent is informed before anything is sent.
GET/nearby/surveillancecamera:readCameras 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/briefaudit:readRolled-up estate posture brief.
POST/estate/brief/dispatchaudit:readDispatch 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.