§ API SPEC · v1
Connect the cameras you already have.
The most common buyer question after the demo is "how does it talk to my existing IP cameras?". Short answer: RTSP, ONVIF or SCS-1 directly off the wire — no proprietary SDK, no on-prem appliance, no rip-and-replace. The longer answer is below, sectioned by endpoint, with sample request/response, rate limits and the named edge region for each call.
This is a public spec, not the OpenAPI source-of-truth — that lives in the customer portal post-pilot. The shape is stable; the field names are stable. Want the OpenAPI YAML on the call? Ask at /book.
§ 01 · CAMERA INGESTION
RTSP, ONVIF, SCS-1. No proprietary SDK lock-in.
We tap the same stream your Milestone, Genetec or Hikvision NVR is already reading — read-only, no replacement. The edge node authenticates as a regular RTSP client per camera. The customer gives us one read-only username per estate; we never touch the recorder.
- RTSP
PRIMARY
RTSP/1.0 over TCP (or RTP over UDP where the LAN allows)
Most BTR, hotel and venue estates speak RTSP natively. We support H.264 main/high and H.265 main, 720p–4K, up to 30fps ingestion (the model samples at 6fps — higher source rates are throttled at the edge).
Setup: 4 lines of config per camera + one firewall rule for the LHR edge IP.
- ONVIF
DISCOVERY
ONVIF Profile S + Profile T
For estates that prefer not to hand us individual RTSP URLs, we speak ONVIF Profile S (streaming) and Profile T (advanced streaming + alerts) to a customer-side ONVIF gateway. The gateway sees Witness as a regular Profile S client; no special permissions needed.
Setup: customer points the gateway at our ONVIF endpoint; auto-discovery handles the rest.
- SCS-1
GOVERNMENT
Surveillance Camera Code — Standard 1
Required by some local-authority CCTV control rooms and JCNSS-scoped transport-hub estates. SCS-1 mandates a specific authentication + audit-log shape; we ship a dedicated SCS-1 ingest path that emits the required audit-log entries to the customer's existing audit sink.
Setup: the SCS-1 path is opt-in per site, configured on the call.
§ AUTHENTICATION
Bearer token, rotated quarterly, bound to ICO ZA-registration.
Every API call carries an
Authorization: Bearer <token>
header. Tokens are scoped per environment (pilot / production)
and per site, never global. Quarterly rotation is mandatory —
the previous token continues to work for a 72-hour overlap
window so deploys aren't rushed. Every issued token is bound at
creation to the customer's ICO ZA-registration; if either the
customer's or our own ZA lapses, the token is revoked at the
next rotation rather than left dangling.
- ZA
Our ICO ZA-registration:
ZA-pendingIn progress at the time of this spec. Tokens issued pre-registration carry a 30-day grace period after the ZA issues, during which we re-bind every active token. - ROTATE
Tokens rotate at the start of every UK calendar quarter (1 Jan, 1 Apr, 1 Jul, 1 Oct). Customers receive the new token 7 days ahead via the same channel they configured for webhook secrets.
§ 02 · EMBEDDING API
POST /v1/embeddings
Compute and store the vector.
Submit a batch of detected-person frames; receive 512-dim embedding vectors with content-addressable IDs.
Sample request
{
"site_id": "ste_btr_acme_block_a",
"camera_id": "cam_lobby_north",
"frames": [
{
"ts": "2026-06-15T14:23:01.842Z",
"detection_id": "det_01HXT9F2K",
"bbox": [120, 84, 380, 612],
"image": "<base64-encoded JPEG, max 96kB>"
}
]
} Sample response
{
"embeddings": [
{
"embedding_id": "emb_sha256_8c2e...a1f",
"detection_id": "det_01HXT9F2K",
"dim": 512,
"stored_at": "2026-06-15T14:23:02.118Z",
"expires_at": "2026-06-29T14:23:02.118Z"
}
],
"request_id": "req_01HXT9F30"
} Notes
- Embedding IDs are SHA-256 of the vector itself — identical embeddings collapse to one ID, so a person standing still for 5s doesn't bloat the store.
- The raw frame is held in a 30s rolling RAM buffer for operator playback, never written to durable storage.
- expires_at is always datePublished + 14 days. The store enforces hard expiry; there is no "keep it for the audit" flag.
§ 03 · MATCH API
POST /v1/matches
Search the recent embedding history.
Submit an embedding; receive a ranked list of candidate matches across the recent embedding history.
Sample request
{
"embedding_id": "emb_sha256_8c2e...a1f",
"site_scope": "ste_btr_acme_block_a",
"window_minutes": 30,
"threshold": 0.78,
"limit": 10
} Sample response
{
"matches": [
{
"embedding_id": "emb_sha256_3b41...c9d",
"camera_id": "cam_lift_lobby",
"similarity": 0.842,
"seen_at": "2026-06-15T14:08:44.290Z",
"dwell_seconds": 11
},
{
"embedding_id": "emb_sha256_07ad...e2b",
"camera_id": "cam_landing_3f",
"similarity": 0.811,
"seen_at": "2026-06-15T14:14:09.660Z",
"dwell_seconds": 4
}
],
"request_id": "req_01HXT9F31"
} Notes
- Cosine similarity, range 0.0–1.0. Operators tune the threshold per site — 0.78 is the v9 default that hits a reasonable precision/recall on the BTR cohort.
- dwell_seconds is the duration the same embedding was reported on that camera before the next detection ID — a useful proxy for tail-gating and pause-at-door signals.
- Results are ordered by similarity DESC, then by seen_at ASC (the older sighting wins ties — operators want the earliest evidence of the cross-camera trace).
§ 04 · ALERT WEBHOOK
POST <customer-configured URL>
Deliver real-time match events.
Outbound delivery of real-time match events crossing the configured similarity threshold.
Sample request
POST /your-webhook HTTP/1.1
Host: customer.example.co.uk
Content-Type: application/json
X-Witness-Signature: t=1718459789,v1=4f8a...c2e
X-Witness-Delivery: dlv_01HXT9F40
{
"event": "match.crossed_threshold",
"site_id": "ste_btr_acme_block_a",
"embedding_a": "emb_sha256_8c2e...a1f",
"embedding_b": "emb_sha256_3b41...c9d",
"camera_a": "cam_lobby_north",
"camera_b": "cam_lift_lobby",
"similarity": 0.842,
"seen_at_a": "2026-06-15T14:23:01.842Z",
"seen_at_b": "2026-06-15T14:08:44.290Z",
"ts": "2026-06-15T14:23:02.231Z"
} Sample response
HTTP/1.1 200 OK
Content-Length: 0
(Any 2xx response acknowledges delivery. Any 4xx other than 408/429
permanently fails the delivery — no retry. 5xx and 408/429 trigger the
5-attempt back-off.) Notes
- Verify X-Witness-Signature using the per-webhook secret: HMAC-SHA256(secret, `${t}.${raw_body}`). The signed timestamp guards against replay; reject deliveries where |now − t| > 300s.
- Idempotency: every delivery carries an X-Witness-Delivery header. Use it as the dedupe key — retries reuse the same value.
- Customers configure the threshold per camera or per site. Below-threshold matches are silently dropped; the webhook never fires for them.
§ 05 · RETENTION API
DELETE /v1/embeddings/{embedding_id}
Right-to-erasure on demand.
Right-to-erasure for an individual embedding. Bulk variant for batch retention enforcement.
Sample request
DELETE /v1/embeddings/emb_sha256_8c2e...a1f HTTP/1.1
Authorization: Bearer <token>
—— OR (bulk) ——
POST /v1/embeddings/bulk-delete HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
{
"embedding_ids": [
"emb_sha256_8c2e...a1f",
"emb_sha256_3b41...c9d"
],
"reason": "DSAR-2026-Q2-014"
} Sample response
{
"deleted": [
"emb_sha256_8c2e...a1f",
"emb_sha256_3b41...c9d"
],
"not_found": [],
"request_id": "req_01HXT9F32",
"ico_audit_ref": "ZA-pending-2026-06-15-001"
} Notes
- Deletion is hard — the embedding and all derived match events are tombstoned within 24 hours. There is no soft-delete window.
- The reason field is mandatory on bulk; we store it (as plain text) against the ICO audit ref so a DSAR auditor can trace every deletion back to its origin.
- The 14-day automatic expiry runs independently. You almost never need to call this — but the endpoint exists so a customer DPO can satisfy an immediate erasure request without waiting for the expiry.
Want the OpenAPI YAML?
The OpenAPI 3.1 source-of-truth lives in the customer portal, which goes live with the pilot agreement. We're happy to walk through the YAML on a call before that — schedule one at /book.
Schedule a 20-min call → The pipeline in four steps → The UK posture →