Dashboard

API Reference

REST + MQTT + WebSocket

Short links (Explorator)

GET/api/short/:id
Resolve an 11-char base62 short id (10..12-char accepted for legacy + physical-MAC compat) to its canonical entity. No auth — public surface backing the explorator.pro / testing.explorator.pro inline viewer. CORS-enabled for the explorator origins; same-origin always works. Returns 404 when the id matches no space / device / device_meta.short_id and is not a literal devices.mac. See plan-explorator-short-domain.md §1.5 + the resolver at src/backend/app/api/explorator_shortlink.py.
GET/api/short/:id/:endpoint
Two-segment form. Endpoint id (0..255) from the path always overrides whatever endpoint_id the short_id row would otherwise carry — so one short_id can point at multiple endpoints by varying the suffix.

Content

GET/api/devices/:mac/content
Device-level content document (separate from any per-endpoint override). Returns {mac, content, content_enabled, public_access, ...}. Anonymous reads allowed when devices.public_access=true (D-RD10). Used by the web SPA + explorator inline viewer on bare /device/<mac> URLs.
GET/api/devices/:mac/ep/:endpoint/content
Resolved per-endpoint content document with style cascade. Resolution order: per-endpoint override → space template (most-specific selector first) → device-level content fallback (when devices.content is set, regardless of inherit_from_device — principle of least surprise) → autogenerated from declared capabilities. Always 200. Anonymous reads gated by the cascade `devices.public_access AND device_meta.public_access` (D-RD10) — both must be true; otherwise 404 to avoid existence enumeration. Authenticated requests fall through to the device-access role check.
PATCH/api/devices/:mac/ep/:endpoint
Per-endpoint settings PATCH. Owner / admin only. Each field independently optional; the body is the union of: content_enabled (bool), content_enabled_tile (bool), slug (string|null, space-scoped unique), public_access (bool, anon-read gate per D-RD10), inherit_from_device (bool, D-RD11). Returns the updated row.
GET/api/spaces/:space_id/content
Space-level content document (single-page form, kept for back-compat). New work should use /api/spaces/:space_id/pages instead.
GET/image/:id
Space-scoped image upload. The `id` is a globally-unique 11-char base62 — same shape as everywhere else in SHORTID-E5. Backend resolves owner + applies the space public_access / member auth check; proxy serves the bytes. /assets/* on canonical hosts returns Access-Control-Allow-Origin: * so the explorator branded shell can embed images cross-origin.

Devices

GET/api/devices
List all devices with current channel state, filtered by announced channels.
GET/api/devices/:mac
Single device with all channels and metadata. Anon read allowed when devices.public_access=true.

Commands

POST/api/devices/:mac/cmd
Send a command to a device. Payload keys are channel names, values are "on", "off", "toggle", or a number.

Capabilities (caps params)

GET/api/devices/:mac/params
List the typed params declared by the device (bool / float / integer / string / list / trigger). Wire shape per docs/capabilities.md — value omitted on triggers, options only on lists, min/max/step only on numerics, last_triggered_at only on fired triggers.
GET/api/devices/:mac/ep/:endpoint/params
Same shape as above, scoped to a multi-endpoint device. See docs/endpoints.md.
POST/api/devices/:mac/params/:name
Set a single param. Backend validates against the registered type — bool wants a JSON boolean, integer wants an int (not float), float wants any number, string a string, list a string in `options`, trigger null / {}. 400 on type mismatch with the validation message in `detail`. On success the backend publishes `{name: value}` to <mac>[/ep]/set; the device re-validates (clamps numerics) and re-publishes state which the bridge ingests back into device_params.
POST/api/devices/:mac/ep/:endpoint/params/:name
Endpoint-scoped variant.

Lua IDE

POST/api/lua/compile
Compile Lua source via the configured luac (lua54 or nodemcu). With a destination, also store source + bytecode in device_scripts and publish the bytecode to <mac>[/ep]/script retained QoS 1. Compile errors return 400 with the parsed {message, line}; timeout returns 504; MQTT not connected returns 503. See docs/lua-ide.md.
GET/api/devices/:mac/script
Read back the most recent stored Lua source for a device. Used by the IDE on mount so reopening the editor doesn't lose work even if browser localStorage is cleared. Bytecode column is omitted from the response — the dashboard only needs source for editing. 404 when the device is unknown OR no script has been uploaded.
GET/api/devices/:mac/ep/:endpoint/script
Endpoint-scoped variant of the script readback.

Space pages + files

GET/api/spaces/:space_id/pages
Multi-page space content. Each page has a slug (URL segment), kind, document JSON-AST, sort order. Pages are flat (no nesting), inherit the space style. See plan-space-pages.md.
GET/api/spaces/:space_id/files
Space-scoped file library — any MIME type, served from the FS at /var/www/hiot-files/<space-uuid>/<file-id>. Operator uploads via the SpaceDetail FILES tab.
POST/api/spaces/:space_id/files
Multipart upload of a single file. Operator / owner only. Returns the new file id (11-char base62) which can then be referenced in content documents via <image id="..."/>.
DELETE/api/spaces/:space_id/files/:id
Delete a space file. Operator / owner only.

Tools

GET/api/tools/buildings
Public R-tree-indexed radius lookup against the OSM buildings dataset (~28.9M rows). Returns up to N buildings within `radius_m` of `(lat, lng)`. Gated by `BUILDINGS_TOOL_ENABLED` so production (no DB file shipped) returns 404. Used by the typed-space onboarding picker.

Well-known (Universal Links + App Links)

GET/.well-known/apple-app-site-association
iOS AASA file served per-Host. hiot-proxy reads `<static_root>/well-known/<host>/apple-app-site-association` and returns it as application/json with no redirect (Apple's CDN won't follow redirects for AASA). On canonical hosts, the file claims the matching iOS bundle id; on `(testing.)explorator.pro`, it claims `4RHUR747N4.app.explorator[.testing]` with `/*` matching every path (DeepLinkRouter does the in-app filter). Source: src/web/aasa/<host>.json; install via src/environment/scripts/deploy/install-well-known.sh.
GET/.well-known/assetlinks.json
Android App Links verification file served per-Host. Same install path + serving layer as AASA above. Claims the matching Android package name + the SHA-256 cert fingerprint(s). On `(testing.)explorator.pro` it lists `app.explorator[.testing]` with both upload + debug keystore fingerprints so locally-built APKs verify on first install.

Device tab events

GET/api/devices/:mac/events
Query event history. Supports channel filter, time range, cursor pagination.
GET/api/devices/:mac/events (time range)
When both since and until are provided, returns ALL events in the range (no limit cap). Used by the chart.

MQTT Topics

SUB<mac>/announce
Device type and channel list (retained). Published by hub on boot.
SUB<mac>/state
Current channel values (retained). No type field — type is on /announce.
PUB<mac>/set
Send commands to a device via MQTT (same format as POST /cmd body). Caps params (docs/capabilities.md) ride the same topic — `{"led":true}`, `{"brightness":0.7}`, `{"reboot":null}`.
PUB<mac>/script
Compiled Lua bytecode (retained, QoS 1). Published by the backend's `POST /api/lua/compile` when a destination is set. Source is NOT on the broker — it stays in the device_scripts table; the IDE reads it back via GET /api/devices/:mac/script. The device-side handler that loads this into a Lua VM is deferred to lua-test/PLAN.md Phase 5.
SUB<mac>/rssi
Signal strength in dBm (retained). Updated on every ESP-NOW frame.
SUBhub/state
Hub online/offline (retained, LWT).
SUBhub/log
Debug log stream from hub (not retained). Shows command flow, ESP-NOW ACK/NACK, device frames.

WebSocket

WS/ws
Live device updates. On connect: receives full snapshot (with caps `params` per device when present). Then real-time events: device_state, device_announce, device_params_schema, device_rssi, hub_status, hub_log. The device_state event's `state.params` carries caps value updates; device_params_schema fires after each fresh announce so already-connected clients see schema deltas without refetching.

Admin · push notification testing (testing env only)

PATCH/api/admin/users/:id
Promote yourself (or anyone) to "tester" — orthogonal to system_role, unlocks /admin/push-test on testing envs. Body fields are both optional (partial PATCH). The Tester checkbox on the /admin/users page is the one-click UI for this.
GET/api/admin/push/test/preflight
Tester preflight: which platforms are configured on the backend (APNs / FCM), how many push_tokens are registered for the calling user, and a list of human-readable issues when the system isn't ready (missing creds, no token registered, etc.). The /admin/push-test page renders this as a green/red banner.
POST/api/admin/push/test/simulate/:type_name
Fire one of the v1 production triggers through the real dispatcher.notify() pipeline (prefs + rate-limit + audit all apply). Audience-scoped: the backend rejects with 403 if your user_id isn't in the audience the simulated event would target — testers can only simulate events that would fire for them anyway. Supported types: hub-offline, hub-online, sensor-threshold-high, sensor-threshold-low, security-new-signin, security-vault-unlock. Empty body falls back to sensible defaults from the caller's own scope (first space / first device).