All responses use JSON except static assets and the SSE stream. Mutation errors include an error message and, when available, a stable code.
| Status | Meaning |
|---|---|
400 |
Invalid JSON, field, timestamp, URL, query, status, or progress |
401 |
Required bearer token missing |
403 |
Incorrect token or wrong device identity |
404 |
Device, alert, command, or route not found |
409 |
State conflict, active OTA conflict, skipped transition, or progress regression |
413 |
JSON body exceeds 64 KiB |
429 |
Write rate limit exceeded; retry after Retry-After seconds |
500 |
Unexpected server or persistence failure |
503 |
Health check reports a persistence failure |
Every response has X-Request-Id. A safe client-supplied ID is retained; otherwise one is generated. Ordinary JSON responses below 500 also include requestId. Unexpected failures return generic errors without filesystem details or credentials.
The server binds to 127.0.0.1. AUTH_MODE=demo is the default. AUTH_MODE=protected requires OPERATOR_TOKEN and a JSON DEVICE_TOKENS map at startup.
| Routes | Protected-mode credential |
|---|---|
| Telemetry, device command polling, command ack/progress | Authorization: Bearer <token> for the specific device |
| OTA creation, alert acknowledge/resolve, demo injection | Operator bearer token |
GET /api/auth/operator |
Operator bearer token; returns { actor, authMode } for credential verification |
| Fleet list, alerts, event history, metrics | Operator bearer token |
| Device detail | Operator token or the specific device token |
| SSE | Operator bearer token or event-session cookie |
| Health and static dashboard files | Public; health details require operator token |
Protected mode protects reads and writes. Device tokens are never returned by an endpoint. In protected mode the alert actor is the authenticated operator, not the request body's actor. Anonymous health exposes ok, service, authMode, now, status and request identity, without filesystem recovery paths or persistence details. The server remains loopback-only without TLS or multi-user accounts.
POST /api/auth/session requires an operator bearer token and creates an opaque 30-minute event session (maximum 100 sessions per process). It returns expiresAt and an HttpOnly, SameSite=Strict cookie scoped to /api. The cookie authorizes only GET /api/events; it cannot authorize REST reads or mutations. DELETE /api/auth/session revokes that cookie's session, closes its streams and clears the cookie. Replacement login revokes the old cookie. Session mutations reject foreign Origin headers. Query-string tokens are never accepted. Sessions are memory-only and disappear on server restart. Expiry is checked before event delivery and on heartbeat; expired streams receive auth.expired then close. Slow clients are disconnected when stream backpressure is detected and must reconnect to refresh state.
GET /api/health returns ok, status, persistence, schemaVersion, recovery, and authMode. Failed persistence changes status to degraded and HTTP 503; successful persistence restores status to ok. Backup recovery includes source path and index. GET /api/metrics returns device/command/alert counts, SSE client count and HTTP request/error/rate-limit counters.
All API requests have an additional default limit of 1,000 per 60 seconds per IP, including failed authentication, command polling and progress. Write limits also apply per category (telemetry or operatorWrite) and identity/IP. Expired limiter buckets are periodically removed.
GET /api/devicesreturns{ devices }with each device's latest telemetry.GET /api/devices/:idreturns{ device }withrecentTelemetry, activependingCommands, and completecommandHistory.POST /api/telemetryrequiresdeviceIdand ametricsobject containing finite numeric values.reportedStateis optional.- Unknown device IDs are registered automatically on first telemetry.
Optional delivery identity: provide bootId (1-100 ASCII letters/digits or ._:-), a nonnegative safe-integer sequence, and timestamp together. Deduplication is scoped to the device and its latest 240 retained samples. Matching identity and normalized timestamp/metrics/reportedState return HTTP 202 with the original telemetry, alerts: [], and duplicate: true. Object key order does not affect matching. Reused identity with different content returns 409 TELEMETRY_IDENTITY_REUSE. New samples return duplicate: false.
Duplicates do not refresh liveness, change shadow state, persist or emit events. The deduplication window survives snapshot restore because identities are stored with telemetry. Samples outside this window can be accepted again. Requests without identity retain legacy behavior. Sequence is an identity component, not an ordering guarantee; this version does not reject delayed/reordered new messages or retain retired boot sessions indefinitely.
GET /api/alertslists all alerts;?status=open|acknowledged|resolvedfilters them.POST /api/alerts/:id/acknowledgeaccepts{ "actor": "operator" }.POST /api/alerts/:id/resolveaccepts{ "actor": "operator", "reason": "Inspection complete" }.
Manual lifecycle:
open -> acknowledged -> resolved
Repeated identical acknowledgement or resolution is idempotent. Manual resolution requires prior acknowledgement. Connectivity alerts may be resolved directly by the system when telemetry resumes.
In v0.5 the Chinese UI uses “已关闭” for resolved, leaving the API unchanged. The dashboard requires a nonblank disposition note (up to 1,000 characters) and sends it as reason; API clients retain the existing optional-reason behavior. Latest-sample threshold condition is a separate presentation calculation, not a persisted active/cleared state. Manual closure does not guarantee the condition has cleared.
POST /api/ota-jobsacceptsdeviceId,targetVersion, and optionalartifactUrl,checksum,requestId, and eitherexpiresInSecondsorexpiresAt.GET /api/commands?deviceId=:idreturns active commands for device polling.- Add
scope=allto return active and terminal command history. POST /api/commands/:id/ackacknowledges a queued command.POST /api/commands/:id/progressaccepts numericprogressandstatus.
State machine:
queued -> acknowledged -> downloading -> installing -> success
\-> failed
Any active state -> expired (server-owned deadline)
Rules:
- Progress is between 0 and 100 and cannot regress.
- Success requires 100% and may only follow installing.
- Failed may only follow installing.
- Terminal commands cannot change.
expiresInSecondsmust be an integer from 1 to 2,592,000.expiresAtmust be a future date within 30 days. Default TTL is 24 hours (COMMAND_TTL_MS).- The server evaluates expiry on its timer and before create/ack/progress/poll operations. Devices cannot submit
expiredas a progress status. Expiry preserves reported firmware and the desired target, and adds one terminal audit entry. - Identical retries return the existing command without adding audit entries.
- A device may have only one active OTA command.
requestIdis an optional idempotency key. Reuse with a different device or normalized OTA payload returns409 IDEMPOTENCY_KEY_REUSE.
GET /api/events/history?limit=40returns recent compact audit events.GET /api/eventsopens the SSE stream.
Emitted event types are device.updated, telemetry.updated, alert.created, alert.updated, command.created, and command.updated.
The server sends SSE comment heartbeats while connections are open. The MVP does not yet implement replay with Last-Event-ID.
POST /api/demo/inject-alert accepts an optional deviceId and injects a high-temperature telemetry reading. It exists only for the public demonstration workflow.