Skip to main content

API Reference

The Wildfire Alarm API is plain HTTP and JSON. In Docker deployments the dashboard reaches it under /api/ on the same host, for example https://app.example.org/api/incidents. Locally it is served directly on http://localhost:3000.

Live API docs

A running server also serves interactive Swagger documentation at /api-docs. It is generated from the code and always matches the deployed version, including every query parameter and response field.

Authentication​

Protected endpoints take a token in the Authorization header:

Authorization: Bearer <jwt from /login>
Authorization: Bearer sk_<api key>
CredentialObtained fromCan do
JWTPOST /login, valid 24 heverything the user's role allows
User API keyPOST /api-keys with scope: "user"acts as the owning user, for scripts
Ingest keyPOST /api-keys with scope: "ingest"only POST /ingest/notehub

Endpoints marked admin also require the user's role to be admin. The role is read from the database on every request.


Account​

POST /register​

{ "username": "operator1", "email": "operator1@example.com", "password": "Str0ngPassw0rd!" }

Returns 201 with { message, user: { id, username, email, role }, token }. New accounts always get the role user; a role in the body is ignored.

POST /login​

{ "username": "operator1", "password": "Str0ngPassw0rd!" }

email works in place of username. Returns 200 with { message, user, token }.

GET /me​

The signed-in user with the role currently in the database: { id, username, email, role }.

API keys​

MethodPathNotes
POST/api-keysBody { name, description?, scope: "user" | "ingest" }. The key itself is returned once, in apiKey.key
GET/api-keysThe caller's keys with scope and lastUsedAt, never the key itself
DELETE/api-keys/:idRevokes a key

Notehub ingest​

POST /ingest/notehub​

Target of the Notehub HTTP route. Authenticate with an ingest key. The body is the Notehub event envelope; the API dispatches on its file field:

fileBody from the nodeResult action
device.qo{ deviceId, name, type }device_created / device_updated
data.qo{ requests: [{ deviceId, readings: [{ readingType, unit, value, timestamp }], anomaly: { severity, score } }] }readings_stored
alert.qo{ deviceId, event: "raised" | "updated" | "cleared", severity, score, signals, readings, baseline, timestamp }incident_opened / incident_updated / incident_closed / duplicate_ignored
anything elsesystem notes such as _health.qoignored (last seen, location and voltage are still refreshed)

A redelivered event (same Notehub event UUID) is acknowledged with 200 and not stored again. A malformed body returns 400.


Devices​

MethodPathNotes
GET/devicesAll nodes, with position, last severity, last seen and voltage
GET/devices/:idOne node, by database id or deviceId
POST/devicesRegister a node by hand: { deviceId, name, type, status? }. Nodes normally register themselves through device.qo
PATCH/devices/:id/status{ status: "active" | "inactive" | "maintenance" | "hibernation" }. Owner or admin
DELETE/devices/:idOwner or admin

Readings​

GET /readings​

Readings grouped by batch, newest first, with each batch's anomaly severity and score.

Query parameterMeaning
deviceIdOne node, by database id, deviceId (e.g. biobot-001) or Notecard UID (dev:…). An unknown id returns an empty page
readingTypee.g. pm25, gasResistance, temperature, humidity
startDate, endDateISO 8601 time range
minValue, maxValue, unitValue filters
sortBy, sortOrderSorting, default timestamp DESC
page, limitPaging

Incidents​

MethodPathNotes
GET/incidentsNewest first. Filters: status=open|closed, deviceId, limit
GET/incidents/:idOne incident with its full event timeline
GET/incidents/:id/contextCurrent wind, spread envelope (open incidents only) and nearest satellite hotspot
POST/incidents/:id/acknowledgeRecord the drive-out: { outcome: "fire" | "false_alarm" | "sensor_fault" | "unknown", notes? }

Live map​

GET /map​

Everything the live map draws, in one call: devices, openIncidents (each with weather, spread and satellite), and satellite.hotspots within 25 km of any node over the last 48 hours. satellite.status is not_configured until FIRMS_MAP_KEY is set. Weather and satellite lookups are cached, so polling every 15 seconds is fine.

Demo fire (admin)​

MethodPathNotes
POST/demo/fireOptional { latitude, longitude }. Starts a scripted fire on five demo nodes, using the live wind there. No emails are sent
GET/demoState of the demo
DELETE/demoStops the demo and deletes every demo node and its data

Users (admin)​

MethodPathNotes
GET/usersAll users, with counts of ingest keys, other keys and owned devices
PATCH/users/:id/role{ role: "admin" | "user" }. You cannot demote yourself or the last admin
DELETE/users/:idRevokes the user's keys and keeps incident outcomes they recorded. You cannot delete yourself or the last admin

Deleting a user who owns an ingest key stops every Notehub route that uses that key.

API health​

GET /api/health/stats, /api/health/endpoints, /api/health/latency-trends and /api/health/errors report request counts, latency and recent errors, as shown on the dashboard's API Health page.

Older endpoints​

These came before the Notehub ingest path and are kept for compatibility:

MethodPathNotes
GET / POST / DELETE/sensordataDirect reading submission and query, with server-side threshold checks
GET/alerts, POST /alerts/:id/acknowledgeThreshold alerts. Node alerts also appear here, mapped to low/medium/high/critical
GET/power-statusPower events

Errors​

Errors are JSON with an error field:

{ "error": "Incident not found" }

Validation failures add the offending fields:

{ "error": "Validation failed", "details": [{ "field": "name", "message": "name is required" }] }
StatusMeaning
400Malformed body or missing fields
401Missing, invalid or expired token or key
403Authenticated, but not allowed (for example, not an admin)
404Not found
409Conflict, for example demoting the last admin or starting a second demo
422Validation failed
503The database is reconnecting; try again shortly