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.
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.
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);
});
| type | fires when | payload |
|---|---|---|
classify | every classified window, about 1 to 4 s | {inf_ms, db, classes:[{name,idx,score}…]}, top 5 |
alert | a detection passes the active profile | {class_id,label,conf,brg,mesh} |
filtered | the filtered stream matches a label | {label,score} |
mesh_alert | an alert arrives from another node | the peer's alert payload |
tdoa | a new direction estimate is produced | {doa,conf,kdoa,kconf,t12,t13,t23,c12,c13,c23,el,elconf} |
scene | a scene fires at pipeline layer 10 | {scene,priority,window_sec} or {scene,similarity,learned:true} |
learned_sound | a user-taught sound matches | {name,similarity}, with src:"emb" on embedding matches |
cluster | a class cluster boosts a detection | {target,hits,boost,fires} |
compass | about every 5 s | {heading,mx,my,mz} in degrees, with the raw magnetometer |
status | every 5 s | {batt_v,batt_pct,charging,full,heap,uptime_s} |
gps | periodically | {fix,lat,lon,sats,hdop,alt,pwr} |
net | periodically | {ip,gate} |
mesh | periodically | {backend,tx,rx,drops} |
node | a peer relays its position | {id,fix,lat,lon,brg,conf,rssi} |
fleet | the set of known nodes changes | {changed:true} |
audio | about 1 Hz | {m1,m2,m3,m4,g1,g2,g3,floor}, per-mic dBFS with gains and the room floor |
security | an 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.
| endpoint | method | what |
|---|---|---|
/api/status | GET | Snapshot: version, build, boot slot, uptime, heap, gps{fix,sats,lat,lon}, heading, last classify, mesh backend, wifi, floor_db, tinyear self-test |
/api/history | GET | Last 400 classifications (RAM ring): time, dB, inference ms, top-5 |
/api/gate | GET / POST | The 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/status | GET / POST / GET | Network onboarding and current state. |
/api/mqtt | GET / POST | Home Assistant broker config {enabled,host,port,user,pass,prefix}; GET reports connected |
/api/raw-mic | GET | A 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-bg | POST | Mic gain calibration (~5 s) / room-background profile (blocks while sampling, {"seconds":60}) |
/api/learn | POST | Post {"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/reboot | POST | Restart the device. |
/api/factory-reset | POST | DESTRUCTIVE. Post {"dry":true} to list what would be wiped, and {"confirm":true} to perform the wipe and reboot. |
/api/last-window | GET | The exact audio window the model last saw, for debugging. |
/api/coredump | GET | Stream the last panic coredump as an ELF. |
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.
| endpoint | method | what |
|---|---|---|
/api/alerts/recent | GET | The last 64 alerts with their age. Poll it instead of the socket. |
/api/broadcast/log | GET | The recent broadcast history across triggers, scenes, sounds, and filtered alerts. |
/api/broadcast/test | POST | Test-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"}}]}
| endpoint | method | what |
|---|---|---|
/api/store/<name> | GET / POST | The 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. |
| endpoint | method | what |
|---|---|---|
/api/mesh | POST | Switch the backend with {"backend":"lora"} or "off". |
/api/mesh/channel | GET / POST | View or set the private LoRa channel, which is protected with an AES-256 PSK. |
/api/mesh/nodes | GET | The Meshtastic nodes it has heard, with id, name, RSSI, and age, plus the current alert target. |
/api/mesh/target | POST | Set 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/node | GET / POST | This node's name and role. |
/api/fleet | GET | The multi-node fleet table when the node is a master. |
| endpoint | method | what |
|---|---|---|
/api/ota | POST | Post 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/url | POST | Give it {"url":"https://…/ironear.bin"} and the device downloads and flashes itself. It accepts RSA-signed images only. |
/api/ota/storage | POST | Replace the web and asset storage image. User stores are preserved across it. |
| endpoint | method | what |
|---|---|---|
/api/notes | GET / POST | Field notes stored on the device (jsonl) |
/api/notes/export · /api/notes/clear | GET / POST | Download or wipe the notes. |
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.
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.