IRONEAR DEVICE API

Every IronEar node is a fully open device with REST API and a WebSocket event stream over your local network. The base URL is http://<node-ip>, and the node also answers to ironear.local over mDNS.

Authentication

Every route and the WebSocket require a session token, and there are no default credentials because the device password is set on first boot. You get a token by posting your password to /api/auth/login.

curl -X POST http://ironear.local/api/auth/login -d '{"pass":"your-device-password"}'
{"ok":true,"token":"<32-char token>"}

Send the token on each REST call in the X-Auth-Token header, or let the browser use the ie_tok cookie that the login also sets. On the WebSocket you pass it as a query parameter, as in ws://<node-ip>/ws?tok=<token>. Tokens live in RAM only, so they expire after 24 hours of inactivity and are cleared on reboot.

WebSocket events

The socket lives at ws://<node-ip>/ws?tok=<token>. Open it once and leave it open, and the device pushes every event the instant it happens. The alert profile gates only the outbound channels, never this stream. Each message has the form {"type":"...","data":{...}}.

const ws = new WebSocket(`ws://${host}/ws?tok=${token}`);
ws.addEventListener("message", (e) => {
  const { type, data } = JSON.parse(e.data);
  if (type === "alert") console.log("ALERT:", data);
});
typefires whenpayload
classifyevery classified window, about 1 to 4 s{inf_ms, db, classes:[{name,idx,score}…]}, top 5
alerta detection passes the active profile{class_id,label,conf,brg,mesh}
filteredthe filtered stream matches a label{label,score}
mesh_alertan alert arrives from another nodethe peer's alert payload
tdoaa new direction estimate is produced{doa,conf,kdoa,kconf,t12,t13,t23,c12,c13,c23,el,elconf}
scenea scene fires at pipeline layer 10{scene,priority,window_sec} or {scene,similarity,learned:true}
learned_sounda user-taught sound matches{name,similarity}, with src:"emb" on embedding matches
clustera class cluster boosts a detection{target,hits,boost,fires}
compassabout every 5 s{heading,mx,my,mz} in degrees, with the raw magnetometer
statusevery 5 s{batt_v,batt_pct,charging,full,heap,uptime_s}
gpsperiodically{fix,lat,lon,sats,hdop,alt,pwr}
netperiodically{ip,gate}
meshperiodically{backend,tx,rx,drops}
nodea peer relays its position{id,fix,lat,lon,brg,conf,rssi}
fleetthe set of known nodes changes{changed:true}
audioabout 1 Hz{m1,m2,m3,m4,g1,g2,g3,floor}, per-mic dBFS with gains and the room floor
securityan auth or security event occurs{msg}

Every JSON endpoint sends Access-Control-Allow-Origin: *, so browser clients work from any origin. Write endpoints validate JSON and return 400 on malformed bodies.

Device

endpointmethodwhat
/api/statusGETSnapshot: version, build, boot slot, uptime, heap, gps{fix,sats,lat,lon}, heading, last classify, mesh backend, wifi, floor_db, tinyear self-test
/api/historyGETLast 400 classifications (RAM ring): time, dB, inference ms, top-5
/api/gateGET / POSTThe energy gate. A GET returns {gate_db(legacy), effective_db, floor_db, margin_db}. The gate is floor-relative and tracks the room, so the margin is the only knob, which you set with {"margin_db":7}.
/api/wifi/scan · /api/wifi/connect · /api/wifi/statusGET / POST / GETNetwork onboarding and current state.
/api/mqttGET / POSTHome Assistant broker config {enabled,host,port,user,pass,prefix}; GET reports connected
/api/raw-micGETA raw 48 kHz capture of about 340 ms, returned as two base64 int16 channels so you can run your own DSP.
/api/calibrate · /api/calibrate-bgPOSTMic gain calibration (~5 s) / room-background profile (blocks while sampling, {"seconds":60})
/api/learnPOSTPost {"name":"gate","seconds":10} to teach one of up to five user sounds on the device. It blocks while recording and stores a neural embedding prototype that is matched on every window.
/api/rebootPOSTRestart the device.
/api/factory-resetPOSTDESTRUCTIVE. Post {"dry":true} to list what would be wiped, and {"confirm":true} to perform the wipe and reboot.
/api/last-windowGETThe exact audio window the model last saw, for debugging.
/api/coredumpGETStream the last panic coredump as an ELF.

Alerts and broadcast

Everything that leaves the box goes through the broadcast router. Its configuration, which holds the triggers, the filtered-stream alerts, the scenes, the learned sounds, and the rate caps, lives in the broadcast_config store below. Each entry carries its own routing switches for LoRa, Home Assistant, GPS, and bearing.

endpointmethodwhat
/api/alerts/recentGETThe last 64 alerts with their age. Poll it instead of the socket.
/api/broadcast/logGETThe recent broadcast history across triggers, scenes, sounds, and filtered alerts.
/api/broadcast/testPOSTTest-fire a route with {"kind":"trigger","name":"Dog"}. The kinds are trigger, scene, sound, and filtered.
curl -H "X-Auth-Token: $TOK" http://ironear.local/api/alerts/recent
{"alerts":[{"age_s":12,"e":{"class_id":420,"label":"Shatter","conf":94,"brg":214,"mesh":"sent"}}]}

Pipeline

endpointmethodwhat
/api/store/<name>GET / POSTThe single persistence route for the whitelisted JSON documents. A GET reads a store and a POST overwrites it, which auto-reloads the affected engine. The stores are pipeline_config, layers, bg_profile, learned_sounds, learned_scenes, broadcast_config, and scenes. Post the whole document, and unknown keys are preserved.

Mesh (LoRa / Meshtastic)

endpointmethodwhat
/api/meshPOSTSwitch the backend with {"backend":"lora"} or "off".
/api/mesh/channelGET / POSTView or set the private LoRa channel, which is protected with an AES-256 PSK.
/api/mesh/nodesGETThe Meshtastic nodes it has heard, with id, name, RSSI, and age, plus the current alert target.
/api/mesh/targetPOSTSet the primary alert DM target with {"node":"<hex-id>"}, add extra recipients with {"slot":1..3,"node":"…"}, and use "broadcast" to send channel-wide.
/api/nodeGET / POSTThis node's name and role.
/api/fleetGETThe multi-node fleet table when the node is a master.

Firmware

endpointmethodwhat
/api/otaPOSTPost the raw app image in the body to flash it and reboot, with automatic rollback if the image is bad. Unsigned images are rejected because the device requires an RSA-3072 signature.
/api/ota/urlPOSTGive it {"url":"https://…/ironear.bin"} and the device downloads and flashes itself. It accepts RSA-signed images only.
/api/ota/storagePOSTReplace the web and asset storage image. User stores are preserved across it.

Notes

endpointmethodwhat
/api/notesGET / POSTField notes stored on the device (jsonl)
/api/notes/export · /api/notes/clearGET / POSTDownload or wipe the notes.

Home Assistant

The node supports MQTT discovery, so once you set your broker it publishes its entities to Home Assistant. Those entities are the mode and filtered alerts, the scenes, the detected sound, GPS, compass, and battery, and they use the same alert profile as every other channel.

Which surface to use

Use the REST API when you want to fetch or change something on demand. Use the WebSocket when you want to react the instant something happens. Use MQTT when you want the node to push into Home Assistant or another broker.

The API is pre-1.0 and may change until the first hardware release. This page is generated from the actual firmware routes.