API Reference
REST + MQTT + WebSocketShort links (Explorator)
GET
/api/short/:idResolve 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/:endpointTwo-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/contentDevice-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/contentResolved 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/:endpointPer-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/contentSpace-level content document (single-page form, kept for back-compat). New work should use /api/spaces/:space_id/pages instead.
GET
/image/:idSpace-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/devicesList all devices with current channel state, filtered by announced channels.
GET
/api/devices/:macSingle device with all channels and metadata. Anon read allowed when devices.public_access=true.
Commands
POST
/api/devices/:mac/cmdSend 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/paramsList 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/paramsSame shape as above, scoped to a multi-endpoint device. See docs/endpoints.md.
POST
/api/devices/:mac/params/:nameSet 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/:nameEndpoint-scoped variant.
Lua IDE
POST
/api/lua/compileCompile 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/scriptRead 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/scriptEndpoint-scoped variant of the script readback.
Space pages + files
GET
/api/spaces/:space_id/pagesMulti-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/filesSpace-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/filesMultipart 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/:idDelete a space file. Operator / owner only.
Tools
GET
/api/tools/buildingsPublic 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-associationiOS 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.jsonAndroid 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/eventsQuery 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>/announceDevice type and channel list (retained). Published by hub on boot.
SUB
<mac>/stateCurrent channel values (retained). No type field — type is on /announce.
PUB
<mac>/setSend 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>/scriptCompiled 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>/rssiSignal strength in dBm (retained). Updated on every ESP-NOW frame.
SUB
hub/stateHub online/offline (retained, LWT).
SUB
hub/logDebug log stream from hub (not retained). Shows command flow, ESP-NOW ACK/NACK, device frames.
WebSocket
WS
/wsLive 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/:idPromote 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/preflightTester 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_nameFire 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).