# API overview

> The public REST and WebSocket interfaces of the X-Edge Vision appliance, and the rules every route follows.

The appliance exposes a REST API under `/api/v1` and a WebSocket stream at
`/ws/v1/stream`, both on port `5080`. The operator console uses the same
interfaces.

The full REST contract, with every request and response body, is the OpenAPI
file [`/openapi/rest-v1.json`](/openapi/rest-v1.json). It is generated from the
code and checked against real responses.

## Public surfaces

| Surface | Use |
|---|---|
| `/api/v1/*` | REST routes, authenticated per route |
| `/ws/v1/stream` | Live detections, camera status and alarm events as JSON |
| `/whep/*` | WebRTC video signalling. The video itself travels from the relay to the browser, not through the API |
| `GET /health/live` | Anonymous liveness check. It answers `200` when the service is up |

## Rules every route follows

| Topic | Rule |
|---|---|
| Authentication | Send `Authorization: Bearer <access token>`. An access token is valid for 15 minutes. The refresh token is an HttpOnly cookie, `ev_refresh`, valid for 14 days. |
| Roles | `read` (Administrator, Engineer, Operator), `configure-inference` (Administrator, Engineer) and `administer` (Administrator). |
| Unclaimed device | Until an administrator is claimed, every path answers `403 NOT_PROVISIONED` except `/health/live`, `/api/v1/setup/*` and `/api/v1/system/info`. |
| Errors | `application/problem+json` with a stable `code`. Branch on `code`, never on `title` or `detail`, which are prose and can be reworded. Field validation is `422 VALIDATION_FAILED` with an `errors` map keyed by the JSON field name. |
| Concurrent edits | Reads of editable rows carry a weak `ETag`. Send it back as `If-Match` on a write. A missing `If-Match` gets `428`, and a stale one gets `412` with the current version in `current`. |
| Partial updates | In a `PATCH`, a missing field means leave it unchanged, and an explicit `null` means clear it. |
| Lists | Always an object with an `items` array. Offset paging returns `total`, `page` and `pageSize`. Keyset paging returns `total` and `nextCursor`. |
| Asynchronous work | `202 Accepted` means the work was requested, not finished. Read the outcome from a status route. |
| Audit | Every change that reaches its handler writes an audit entry named `resource.action`, for example `camera.update`. |

## Live stream

1. Request a ticket with `POST /api/v1/ws-tickets` and a bearer token. The reply
   is `{ ticket, expiresInSeconds }`. A ticket works once and lasts 30 seconds.
2. Open `GET /ws/v1/stream?ticket=<ticket>` as a WebSocket. A missing, expired or
   used ticket answers `401` before the handshake.
3. The first frame is `hello`. The socket starts with no cameras, so send
   `{ "type": "subscribe", "cameras": ["<camera id>"] }` for up to 64 cameras.

A **detection** arrives as a JSON object with no `type` field. Every other frame
has a string `type`: `hello`, `subscribed`, `clock`, `cameraStatus`, `event` and
`alertState`. Clients tell the two apart by whether `type` is present.

Up to 16 viewers can be connected at once by default. When the limit is reached
the server closes the socket with `1008 too many clients`. A client that does not
read fast enough is closed with `1008 too slow`, and should reconnect.

## Versioning

The version is in the path, `/api/v1` and `/ws/v1`. Within a version, new routes,
new optional request fields, new response fields and new error codes can be
added. Ignore fields you do not know, and handle an unknown error `code` by its
HTTP status class. Removing or renaming a route or field, changing a type or
meaning, or making a policy stricter is a breaking change. It goes through a
deprecation period or a new version path.

The REST API, the live stream and the detection payload are versioned
separately. The detection payload carries `schemaVersion`, currently `"1.0"`.
