Skip to content
KestrelKestrel
Esc
↑↓navigate↵open⌘Jpreview
On this page

API

The server's HTTP API, which the web client reads.

The server’s API is plain JSON over HTTP, with no login of its own. Its OpenAPI document is at /api/openapi.json. Recorder times have no time zone: they’re the recorder’s own clock, as it keeps them, such as 2026-11-11T22:24:31.

It serves sources, such as the recorder, and their cameras. A camera’s id is its source’s id and its own, such as swann/3 for the recorder’s channel 3, and its routes take the two as path segments.

Endpoint Returns
GET /api/sources Each source: its kind, name and whether it’s connected
GET /api/sources/:source/details Its facts, clock, storage and each camera’s state
GET /api/sources/kinds Each kind of source the server runs and the settings it takes
GET /api/sources/settings Every source’s settings and the camera labels, no secrets
PUT /api/sources/settings Saves them and restarts the sources that changed
GET /api/attention What needs you, each an alert or passive, with its source
GET /api/cameras Each camera: its id, name and what it can do
GET /api/cameras/:source/:camera/recordings?date=… A camera’s recordings on one day, such as 2026-11-11
GET /api/cameras/:source/:camera/video?… Recorded video as MP4; see below
GET /api/cameras/:source/:camera/still A camera’s still picture now, for one with still
GET /api/recordings/days?month=… The days with recordings on any camera in a month
GET /api/events/ The last 200 events from each source, newest first
GET /api/events/live Events as server-sent events
GET /api/health 204 while the server is up

A camera in /api/cameras says what it can do: live has its go2rtc stream names, or is null for a camera with no live view; still says whether it has a still picture, which the page shows in place of video for a camera with no live view; recordings and events say whether it has them; signal is whether it has a picture now, or null when its source can’t tell. A source that isn’t connected yet leaves its cameras out until it knows them.

The settings read back carry a revision. A save sends back the one it read, and is refused with InvalidSettings when the settings have changed since, such as from another tab, so a page left open can’t undo a newer save.

A camera’s name is its source’s name for it, such as Channel 3, and its label the name you gave it on the Settings page, or null.

The settings are what sources.json keeps: each source’s id, kind, name and settings, its cameras for a kind you list cameras for yourself, and the camera labels. Each kind in /api/sources/kinds lists its fields, each text, number, secret or address. Reading them back leaves secret fields out, naming the ones that are set in secrets, and hides the password in an address as ********. A save keeps each secret it leaves out and each hidden password, checks the settings, and returns 400 with InvalidSettings saying what’s wrong, such as two sources with one id.

A source’s details are general, so any source can fill them in: facts are labelled values, such as the recorder’s name, firmware and channel count; clock is its own time, where it keeps one; storage lists each disk or card with its state; and cameras gives each camera’s signal, what it’s recording and whether there’s motion now. A source with nothing to show returns 404 with NotSupported.

/api/attention is the status source the CLI’s status, the page and push read. Each source works out its own. Alerts are what needs you: a source can’t be reached, a disk is bad or unformatted, or a camera that had a signal while the server ran has lost it. A full disk is passive, since the recorder records over its oldest recordings; the page shows it in the source’s Storage card.

An event in /api/events has its source, its camera where it’s about one, its kind (such as motion, PIR, or a camera’s own person, vehicle or animal detection), whether it started or ended, the source’s own time for it, and when the server received it.

Every endpoint that needs a source returns 503 with a SourceUnavailable error while it isn’t connected. An id that names no source or camera returns 404 with UnknownSource or UnknownCamera, and asking for something a source can’t do, such as a stream’s recordings, returns 404 with NotSupported.

Recorded video

GET /api/cameras/swann/1/video?begin=2026-11-11T21:46:00&end=2026-11-11T21:48:00 sends what a camera recorded between two recorder times as fragmented MP4, H.264 with no audio, which plays as it arrives. Add stream=sub for the small sub stream, and download=true to have a browser save it as a file rather than play it. speedUp=2, up to 4, shortens each frame so it plays that many times faster: browsers play no faster than 16 times.

every=1, up to 60, sends key frames only, one per that many seconds of recording, from the recorder’s own fast forward. The whole main stream only downloads at about 17 times real time, so it can’t keep up past 16, but its key frames come at about 58. So the page plays 32 times as every=1 with speedUp=2, at 16 times, in full resolution. The recorder runs one playback or download at a time; the server waits up to 15 seconds for it to let go of the last one.

  • It runs across recordings, one after another, with the gaps between them closed up, so a range over a run of motion clips plays them back to back.
  • It starts at the first key frame in the second at begin, and runs to the end of the key frame group at end, so about a second either side.
  • It comes faster than real time, around 17 times on the main stream, so a minute of 1080p takes a few seconds. Nothing is kept on the server.
  • A range with nothing recorded in it returns 404 with a NoRecordings error. Once video has started, a failure can only cut the response short.

The page plays it through Media Source Extensions. It sets the length from the range, asks for video from a new begin when you seek past what has loaded, and keeps only a few minutes around the playhead, as a full recording is about 250 MB.

Live video

The server serves go2rtc under /go2rtc/, its WebSocket included, so live video plays from the same address as the page, through a proxy or over HTTPS. Each camera’s live in /api/cameras names its streams, from its id: such as swann_1 for the main stream and swann_1_sub for the sub stream. Play one with /go2rtc/stream.html?src=swann_1_sub. subRatio is the width over height to draw the sub stream at when its frames squeeze the picture into another shape, as the DVR8-4575’s 352×288 sub stream does with a 16:9 picture, and null when it’s drawn at its own shape.

The DVR8-4575 offers an audio track on every stream but sends nothing on it, and an MSE player that asks for audio waits for it forever. The page plays video only, with go2rtc’s own video-rtc.js player and media set to video, so do the same in other players.

Push

The server pushes each alert from /api/attention once, when it starts, to every browser that subscribed. It follows Groundwork’s push contract, outside /api:

Endpoint Does
GET /push/vapid-key The VAPID public key, as { publicKey }
POST /push/subscriptions Keeps a subscription, replacing the client’s last one; 204
DELETE /push/subscriptions Drops the subscription with { endpoint }; 204

The page’s Notify me button subscribes. The keys and subscriptions are kept in push.json, readable only by you, under KESTREL_STATE, else $XDG_STATE_HOME/kestrel, else %LOCALAPPDATA%\kestrel on Windows, else ~/.local/state/kestrel.

Last updated on