Skip to main content

The recorder's API for other systems

Another system talks to EverNVR through the same HTTP routes its own pages use, under the same permissions, signed in with an API token instead of a browser session. The full route list, generated from the running recorder, is at GET /api/openapi.json (OpenAPI 3.1; sign in to read it). This page is the guide to the parts an integration reaches for.

Tokens​

Settings → API tokens (an owner administrator). A token stands for one account: it sees that account's cameras and may do what that account's role may do, so choose the least account that will serve: a viewer granted only the cameras the other system needs, an operator only if it must move a camera, switch its light or siren, or press a button. A read-only token is refused every write (no PTZ, no button presses, no settings) before a route sees it. The secret is shown once; only a hash is kept. A token is revoked in Settings → API tokens without touching anyone's password, and revoking closes its open streams and feeds at once. Creation and revocation are in the account history.

A token may be given an expiry (up to five years). After it, the token is refused exactly as a revoked one is, with no job to run and nothing to remember. Give temporary work — a commissioning laptop, a trial integration — an expiry; leave it empty for a permanent integration.

Present the token one of three ways:

WhereHow
Any requestAuthorization: Bearer evk_…
A stream URL for a player or ffmpegHTTP basic auth with the token as the password: https://token:evk_…@recorder/api/cameras/2/stream.mjpg
Where a client cannot send a header?token=evk_… on a GET of stream.mjpg, frame.jpg, playback.mp4, /ws or /ws/video

A URL is written down: by proxies, by browser history, in a Referer. So the query parameter is accepted only on the routes whose client is an <img>, a player or a browser's WebSocket and has no way to send a header. Everywhere else, use the header or basic auth. An Authorization header this recorder did not issue is ignored rather than refused, so a client with a bearer token for something else can still reach the sign-in page.

Tokens are minted and revoked by a signed-in owner administrator on the API page. A token cannot mint or revoke tokens, whatever account it stands for; POST /tokens and DELETE /tokens/:id answer a token with 403.

A camera with privacy masks refuses /ws/video to a token (close code 1008). That socket carries the camera's own packets, and only the recorder's own page draws the masks over them. A token gets the same camera through stream.mjpg, frame.jpg and playback.mp4, which have the masks applied before the pixels leave; playback.mp4 of a masked camera is re-encoded and starts at the moment asked for.

Times are epoch milliseconds throughout. Camera ids are the id in /api/cameras.

What an NVR integration uses​

Administrators can read first-run wizard progress at GET /api/onboarding: {"step":"hailo","required":true}. PATCH /api/onboarding accepts a step of hailo, storage, ai, or complete; only complete makes required false. This records wizard progress, not hardware qualification or a successful configuration check. The wizard saves configuration through the existing PUT /api/detector, PATCH /api/storage/location, PATCH /api/storage/settings, and PUT /api/ai routes before advancing. GET /api/status includes onboarding for administrators and null for other callers. Existing installations without wizard progress are treated as complete; creating the first account through POST /api/setup starts at hailo.

PurposeRoute
Who am I, version, camera countGET /api/status — also timeZone, the recorder's own zone, which every time the recorder shows is in; null before signing in
Which recorder this isGET /api/identity — product, a stable id and name (the commissioned site name, else the host name). Answers without signing in and carries no version; it is what the mobile app uses to recognize a recorder whose address has changed
CamerasGET /api/cameras — id, name, enabled, onvif (PTZ possible), recordProfile (width, height, codec, fps), health.state, mode (intelligence_only means the recorder keeps no footage), cameraEvents (whether the recorder listens to the camera's own events), cameraEventsStatus (state starting, ok or failing, reason, lastEventAt, active kinds), outputs (the deterrence outputs last found on the camera, each id, kind: relay, light or siren, name and driver: onvif, dahua or tapo), outputsProbedAt (null until an administrator has looked), outputsNotes (what a driver could not do, in words), vendorAccount (whether a Tapo app account password is saved), talkback (whether an operator can talk through the camera's speaker: supported, codec, probedAt, null until the recorder has asked, and reason when not supported) and planPlacement (where the camera is on the site plan: planId, x, y, headingDeg, hfovDeg and calibrated, which is homography, ptz or null; null when the camera is on no plan)
PTZ capability and presetsGET /api/cameras/{id}/ptz, GET /api/cameras/{id}/ptz/presets
Live frameGET /api/cameras/{id}/still.jpg
Live videoGET /api/cameras/{id}/stream.mjpg — MJPEG, plays in any player or ffmpeg; the recorder drops frames rather than letting a slow client fall behind
Frame from the recordingGET /api/cameras/{id}/frame.jpg?at=<ms>&width=<px> — 404 when no footage is kept for that moment. A frame is a pull of footage: it holds one of the account's playback slots while it is cut, at most 30 a minute per account (429 too_many_frames), and a moment older than the 120-second review window needs the account's export permission and is audited as an export. Responses are never cached: a privacy mask drawn today has to cover a frame cut from last week, and a cached copy is a look at footage the recorder never saw
Recorded rangesGET /api/cameras/{id}/availability?from=<ms>&to=<ms> — ranges: [{start, end}] and bounds
Recorded ranges, every camera at onceGET /api/recordings/availability?from=<ms>&to=<ms> — a window of up to 31 days, both bounds required; cameras: {<id>: {cameraId, bounds, ranges}} for each camera the account may see, each entry what that camera's own route returns
Recorded videoGET /api/cameras/{id}/playback.mp4?start=<ms>&duration=<s> — download=1 needs the account's export permission
Boxes over the recordingGET /api/cameras/{id}/tracks?from=<ms>&to=<ms> — where each detected object was, frame by frame, at most 10 minutes per call; boxes are fractions of the frame, timestamps are on the recording's clock (the camera's box alignment already applied and returned as offsetMs: in auto the recorder's measurements nearest the requested window, in manual the camera's trackOffsetMs), and they are kept exactly as long as the footage they belong to
EventsGET /api/events?cameraId=&label=&who=&plate=&since=&until=&limit= — newest first; who is known, unrecognized, unsure or an identity id; plate is all or part of a license plate (letters and digits; spaces and punctuation are ignored). Where face or plate recognition is on, an event also carries identity_method (appearance, face or plate), face_state (known, unsure, unknown or unusable), and plate_text, plate_state (known, unsure or unknown) and plate_confidence
Event pictureGET /api/events/{id}/thumbnail.jpg
Activity over timeGET /api/events/density?from=<ms>&to=<ms>&buckets=<n> — how many events began in each of buckets equal slices (default 120, at most 500) of a window of up to 31 days, per camera the account may see: cameras: {<id>: [{bucket, total, firstAt, person, vehicle, animal, other}]} with empty slices left out, plus from and bucketMs to place them
What rules didGET /api/alerts?limit=
What cameras reported themselvesGET /api/camera-events?cameraId=&limit=&before=<ms> — newest first, events: [{camera_id, camera_name, kind, phase, topic, at, summary}]. kind is motion, line, intrusion, person, tamper or other, and phase is begin, end or pulse (happened once). Only cameras with cameraEvents on report anything; these are the camera's own ONVIF claims, not the recorder's detections
ButtonsGET /api/action-buttons, POST /api/action-rules/{id}/press (operator)
Rules (administrator)GET /api/action-rules, POST /api/action-rules, PATCH and DELETE /api/action-rules/{id} — a rule carries actions, a list of up to ten, each a type of notify (userIds, channels), webhook (url, write-only secret), ptz_preset (cameraId, preset, and optionally returnPreset with returnAfterSeconds 5..3600, 60 unless given), ptz_aim (cameraId of a PTZ whose heading is calibrated on the site plan, zoom 0..1 or null to keep, and the same optional return), output (cameraId, outputId from that camera's outputs, state: pulse, on or off, and seconds 1..300 for a pulse, 10 unless given), talk_message (cameraId of a camera whose talkback.supported is true, messageId of a saved message, repeat 1..3, 1 unless given), preserve (beforeSeconds, afterSeconds, up to an hour in all) or arm (armed, ruleIds, minutes: 0 for until changed, up to a week). action is the first of them, kept for older clients, and a body with action alone is still accepted. A webhook's answer says hasSecret, never the secret; a save that omits secret keeps the saved one for the n-th webhook, and secret: null removes it. temporary_until is when a timed arm or disarm ends, null otherwise; activeNow is whether the rule is listening right now
Alarm journalGET /api/alarms?source=&severity=&cameraId=&since=&until=
Site planGET /api/site-plans — the plans (id, name, width, height in pixels; the image is GET /api/site-plans/{id}/image) and placements keyed by camera id: x, y in plan pixels, headingDeg clockwise from up, hfovDeg, and calibration (kind homography for a fixed camera matched to the plan, with its points and residualPx, or ptz with headingDeg, height in plan pixels and residualDeg). The answer also carries limits (bytes, pixels, points and the placement ranges). Administrators upload a plan with POST /api/site-plans?name= (the JPEG or PNG as the body, at most 8 MB and 8000 pixels on a side), replace its image with PUT /api/site-plans/{id}/image (pins are scaled along, calibrations dropped), rename it with PATCH /api/site-plans/{id} {"name"} and remove it with DELETE /api/site-plans/{id}, which takes every camera off it. They place a camera with PUT /api/cameras/{id}/plan {"placement": {"planId", "x", "y", "headingDeg", "hfovDeg", "rangePx"}} in plan pixels, or {"placement": null} to take it off; match a fixed camera's picture with PUT /api/cameras/{id}/plan/points {"points": [{"picture": [u, v], "plan": [x, y]}]} (picture fractions, at least four, up to 24; the answer's fit carries residualPx and each point's errorsPx, and an empty list clears it); and measure a PTZ's heading with PUT /api/cameras/{id}/plan/samples {"samples": [{"position": {"pan", "tilt", "zoom"}, "plan": [x, y]}]} (the camera needs a calibrated or trained PTZ geometry; the answer's fit carries headingDeg, height and residualDeg, and an empty list clears it). GET /api/cameras/{id}/plan/looking answers the plan point under a calibrated PTZ's center and its position, or null when it looks at or above the horizon; POST /api/cameras/{id}/plan/aim {"x", "y", "zoom"?} sends a calibrated PTZ to look at a plan point (PTZ operator) and answers like ptz/aim plus bearingDeg and levelOnly (true when the camera's height is unknown and only its pan was set); 409 for a camera whose heading is not calibrated
HealthGET /api/health (administrator)
A camera's clock (administrator)GET /api/cameras/{id}/clock — reads the camera now and answers driftSeconds, the camera's clock minus the recorder's, offsetMinutes, the camera's local time minus its UTC time (null when it reports only one), and dateTimeType, NTP or Manual. POST /api/cameras/{id}/clock {"switchToManual": false} sets the camera's time as the signed-in administrator would from the camera's page, and with it the recorder's time zone and the recorder as its NTP server: verified, driftBeforeSeconds, driftAfterSeconds, timeZoneBefore, timeZoneSet (null when the camera already showed the recorder's offset and kept its own zone string), timeZonePreserved, zoneVerified (whether the camera's picture time now shows the recorder's offset; null when it cannot be told), ntpServerSet (the recorder address the camera now takes its time from), ntpError (the camera's answer when it refused an NTP server), dateTimeTypeBefore, dateTimeTypeSet, streamReopened, and ntpKept when the camera stayed on NTP and did not hold the time; switchToManual: true is the separately audited switch to manual time and sets no NTP server. 400 for a camera without ONVIF
Automatic clock correction (administrator)GET /api/clock-sync and PUT /api/clock-sync {"enabled": true} — whether the recorder itself sets a camera's clock, time zone and NTP server when it finds the time seconds out, the zone different from the recorder's, or the camera not taking its time from the recorder (on by default). Off, it still reads every camera's clock on its schedule; each correction it makes is in GET /api/cameras/{id}/writes with no performed_by
The recorder's own clock (administrator)GET /api/recorder/time — available (false, with error, when the system's clock service cannot be reached), zone, ntp (network time on), canNtp, synchronized, now (the recorder's time in ms), zones (the zones it can be set to), pinnedZone (the zone EVERNVR_TZ pins times to, otherwise null) and timeServer (whether the recorder is serving NTP to cameras). PUT /api/recorder/time with any of {"zone": "<from zones>", "ntp": true|false, "time": "YYYY-MM-DD HH:MM"} changes them and answers the same fields without zones. A time is read in the recorder's zone after any new zone is applied, and is refused unless network time is off or turned off in the same request. 400 for a value it refuses, 502 when the system's clock service refuses the change. Each change is recorded in the alarm journal, and every camera's clock is checked again at once

PTZ (operator or administrator; the account, not the token, decides):

VerbRoute and body
Continuous movePOST /api/cameras/{id}/ptz/move {"pan": -1..1, "tilt": -1..1, "zoom": -1..1, "durationMs": optional} — the recorder stops the move itself after a short timeout
StopPOST /api/cameras/{id}/ptz/stop
Aim at a point in the picturePOST /api/cameras/{id}/ptz/aim {"x": -1..1, "y": -1..1, "zoom": -1..1} — the answer says how it was done: method is absolute when the camera's calibrated or trained geometry worked out the pan and tilt and the recorder checked and corrected the result, or fov when the camera was aimed in its own terms; predictedError and achievedError are how far the point was expected to be and ended up from center, as a fraction of the frame; corrections is how many follow-up moves it took, and clamped is true when the camera could not reach the point and was turned as far as it goes
Recall a presetPOST /api/cameras/{id}/ptz/preset {"token": "<preset token>"}
Save a preset (administrator)POST /api/cameras/{id}/ptz/presets {"name": "…"}
Patrol and what it is doingGET /api/cameras/{id}/ptz/patrol (anyone who can see the camera) — patrol (steps: [{preset, presetName, dwellSeconds}], enabled, schedule, resumeAfterSeconds) or null, and status.state: running, paused (after another PTZ command, until status.until), held, waiting (status.reason) or off
Define the patrol (administrator)PUT /api/cameras/{id}/ptz/patrol {"steps": [{"preset": "<token>", "dwellSeconds": 10..3600}], "enabled": true, "schedule": {"days": [0..6], "from": "HH:MM", "to": "HH:MM"} or null, "resumeAfterSeconds": 30..3600} — every preset must be on the camera; DELETE removes it
Hold or resume the patrolPOST /api/cameras/{id}/ptz/patrol/pause, POST /api/cameras/{id}/ptz/patrol/resume — a hold lasts until resumed, across restarts
Auto tracking and what it is doingGET /api/cameras/{id}/ptz/follow (anyone who can see the camera) — follow (enabled, groups, maxFollowSeconds, returnPreset, returnPresetName, returnAfterSeconds, resumeAfterSeconds) or null; unavailable, a sentence saying what the camera lacks (trained geometry, absolute positioning) or null; and status.state: watching, following (status.label, status.since, status.hops), paused (until status.until) or off, with status.lastEnded (why: lost, time, operator, rule, moved, stopped or failed)
Set auto tracking (administrator)PUT /api/cameras/{id}/ptz/follow {"enabled": true, "groups": ["person", "vehicle"], "maxFollowSeconds": 10..600, "returnPreset": "<token>" or null, "returnAfterSeconds": 0..600, "resumeAfterSeconds": 10..3600} — switching it on needs the camera's PTZ geometry calibrated or trained and an absolute position space (409 otherwise); the return preset must be on the camera; DELETE removes it
Stop followingPOST /api/cameras/{id}/ptz/follow/stop — the camera stays where it is and auto tracking pauses for resumeAfterSeconds, as after any other PTZ command
Deterrence outputsGET /api/cameras/{id}/outputs (anyone who can see the camera) — the camera's relay, light and siren outputs as last found (items: [{id, kind, name, driver, on, until}], with on what the recorder last set and until when a timed switch ends, or null), plus probedAt and notes; POST /api/cameras/{id}/outputs/{outputId} (operator) with `{"state": "pulse"
Spoken messagesGET /api/talk/messages (anyone signed in) — the saved messages, `{id, name, source: "recorded"

Any PTZ command above pauses a running patrol for its resumeAfterSeconds, and ends a follow and pauses auto tracking for its own resumeAfterSeconds. A rule's PTZ action ends a follow without pausing auto tracking. The status socket sends {"type": "ptz-patrol", "cameraId", "patrol"} and {"type": "ptz-follow", "cameraId", "follow"} on every change, and {"type": "camera-output", "cameraId", "id", "on", "until"} each time an operator or a rule switches an output; a timed switch ending sends nothing, so read until. GET /api/cameras/{id}/writes?limit=<n> (administrator) returns a camera's newest audited writes, each patrol step and each output switch (operation output, with the output, the state asked for, the rule if one asked, and the camera's answer when it refused) included, as well as every aim at a plan point or a trigger (ptz_aim, with the plan point and bearing when the plan was used), every return a rule booked (ptz_return, with the preset and the rule), and one row per follow (ptz_follow, with what was followed, for how long, how many moves and why it ended) and per return after one (ptz_follow_return).

Deep links for a person: /playback?camera=<id>&at=<ms> opens the timeline at that moment; /events?event=<id> opens an event; /?camera=<id> opens the live view on a camera.

Hearing about things​

Three ways, and an integration usually wants more than one.

  • Webhooks, pushed. An action rule with a webhook action POSTs its trigger details to your URL, signed with the action's secret (HMAC in the X-EverNVR-Signature header); a rule may carry several actions, webhooks among them, and the body names the rule (rule, ruleId). Rules can fire on detections, departures, occupancy, absence, button presses, recorder faults, departures from the site's own normal and the camera's own ONVIF events (triggerType camera_event, with cameraEvent.kind and cameraEvent.topic). When the recorder knows who it was, the body carries identity (name, and method: appearance, face or plate); where recognition read a plate or a face, it carries plate (text, state) and face (the face reading). A detection carries region, the object's box as fractions of the frame [left, top, right, bottom]. See INSTALL.md.
  • The status feed, live. wss://recorder/ws with the token as a bearer header or ?token=. Messages: hello (cameras this token may see), camera-state, camera-activity, event, camera-event, alert, detections, ptz-patrol, ptz-follow, camera-output, site-plan-changed (a plan or a camera's place on it changed, with cameraId when it was one camera's pin; the client reloads /api/site-plans), license-changed (with the new state; the client reloads /api/license), heartbeat every 15 s. An account limited to some cameras gets each of these for the cameras it may see, output switches included; anything about a camera the account cannot see is never sent.
  • MQTT, published. An administrator can point the recorder at an MQTT broker (Settings → MQTT): detections, alerts, alarms and retained camera and recorder state under one topic prefix. Unlike the status feed it is not filtered per account. See the MQTT guide.

Polling GET /api/events?since=<last seen> every few seconds is the simplest correct client and is what the GEM driver does.

The license​

A recorder installed with the EverNVR installer is licensed; a source checkout or development server has no license and refuses nothing. The license never stops recording, a camera already added or a feature already switched on. It refuses growth: adding a camera past the licensed count, switching a licensed feature on, and new work in one.

GET /api/license (anyone signed in) answers {"managed": false} on a recorder with no license. On a licensed one it answers managed: true, state, entitlements (features, a list of feature ids, and limits, where cameras is a number, or null for no limit) and features (each feature id and its name). An administrator also gets the holder and the lease: customer, licenseKey (a hint, not the whole key), plan, termEnd, updatesUntil, leaseIssuedAt, leaseExpiresAt, pendingUntil or graceUntil where they apply, reason, installationId, keyId and cameras, the number added.

stateMeaning
pendingA new install that has not reached the license server yet; it has the trial's features for up to 14 days
trialA trial lease from the license server
licensedA license key is active
graceThe lease has run out and the server cannot be reached; the license's features continue until graceUntil
expiredThe license term has ended
unlicensedNo license, the trial has ended, or the lease could not be renewed
suspended, revokedSet on the license server

The feature ids are ai (the AI agent: event questions, Ask the recorder, briefings, commissioning proposals), recognition (face and plate recognition, new identities and enrolment), investigation (semantic search, trails, new incidents and notes) and mqtt (the MQTT integration).

An administrator manages the key with POST /api/license/activate {"licenseKey": "EVR-…", "move": false}, POST /api/license/refresh, POST /api/license/deactivate and POST /api/license/import {"lease": …} (a lease file issued for this installation). Each answers the new license state. A key in use on another recorder is refused with code no-free-seat and holders; send "move": true to take it. These routes answer 409 on a recorder with no license.

A recorder with no internet connection is licensed by file: GET /api/license/request (administrator) answers a license request, this installation's id and public key signed with its own key, which the supplier turns into a lease file for POST /api/license/import. The state then carries offline: true, and leaseExpiresAt is when that file runs out.

A release uploaded by hand (POST /api/update/upload) must be one the license covers: one signed on or before updatesUntil, or signed as a security fix; an unlicensed or ended license accepts only security fixes. Anything else is a 400 whose error starts Rejected: and gives the reason.

A refusal is a 403 with a code:

  • license_feature, with feature, for a route or a settings save the license does not include. Saving settings for a feature that is already on, or switching one off, is never refused.
  • license_camera_limit when adding a camera (POST /api/discovery/rtsp or POST /api/discovery/{id}/adopt) would pass the licensed count. It is refused before any of the camera's sessions is used. POST /api/discovery/adopt-all lists each camera refused this way under failed, with the sentence as its error.

GET /api/investigation/search?mode=auto falls back to basic search when the license does not include investigation; mode=semantic is refused.

Limits and conduct​

  • Live streams are capped per account; a client past the cap gets 429 with a sentence saying so. Recorded pulls — playback.mp4 and frame.jpg — share the account's playback slots.
  • Anything past the 120-second review window is an extraction: it needs the account's export permission and is audited (INSTALL.md).
  • A token bound to a temporary installer account cannot be minted.
  • Nothing here changes a camera's configuration, and the recorder never does. Its only writes to a camera are the audited ones above: PTZ, presets, a camera's deterrence outputs (a relay, light or siren switched on or off when an operator or a rule asks), and a camera's clock — its time, the recorder's time zone and the recorder as its NTP server — which the recorder also sets itself when clock-sync is on.