Atrium Gateway Developer Documentation
Platform 2.5.3

Atrium Gateway REST API

Pull live sensor readings, alerts, and gateway state out of your Atrium Gateway and into any external system — a CMMS, ERP, historian, SCADA platform, or your own application.

Audience
Integration developers
Endpoints
138 documented
Applies to
EG5120 & EG5100
Last updated
2026-09-08

1. Overview

Every Atrium Gateway exposes the same HTTP API that its own web interface uses. There is no separate integration product to license, no cloud account to create, and no data leaves your network unless you choose to send it.

Atrium is a self-contained platform. NCD wireless sensors report over a sub-GHz DigiMesh radio to a modem on the gateway; the gateway decodes, stores, alerts on, and visualizes that telemetry locally. The API is simply the same interface the built-in web app consumes, available to you on equal terms.

🔒
Local first
The API is served by the gateway itself. No cloud dependency, no vendor relay, no per-call fees.
📜
Plain JSON
Standard HTTP verbs, bearer-token auth, and JSON bodies. Any HTTP client works.
🔊
Pull or push
Poll the REST API, or have the gateway publish every reading to your MQTT broker.
🔧
Full surface
Not a cut-down read API — sensor config, alerting, and gateway administration are all reachable.

What you can do with it

  • Read sensor telemetry — current values and full history, per sensor and per metric.
  • Read the sensor inventory — device IDs, types, names, locations, assets, battery, signal, and online/offline health.
  • Read and manage alerts — active alert state, complete alert history, threshold rules, and multi-condition automations.
  • Write sensor metadata — push your own asset names, locations, and install dates from your system of record into Atrium.
  • Administer the gateway — configuration, network settings, scheduled reports, and firmware updates.

Two services behind one door

Internally the gateway runs two Node services. You do not need to care about this — nginx listens on port 80 and routes each path to the correct service, so every endpoint in this guide is reachable at the same base URL. It is documented only because the service labels appear throughout this guide and in error messages.

ServiceInternal portOwns
atrium-api3001Authentication, all telemetry and inventory reads, most CRUD, gateway configuration
atrium-ingest3002The radio itself — sensor configuration push, mesh mapping, FFT requests, MQTT, and report delivery

2. Base URL and Access

All endpoints are relative to a single base URL: your gateway’s address. How you reach it depends on where your client runs.

From the same network (recommended)

If your integration runs on the same LAN as the gateway, use its IP address or hostname over plain HTTP on port 80:

textBase URL forms
http://192.168.1.50           # by IP address (most reliable)
http://ncd-b00e.local         # by mDNS hostname, if your network resolves it

The gateway’s hostname follows the pattern ncd-xxxx, where xxxx is the last four characters of its LAN MAC address — the same code printed on the physical label. You can confirm the address on the gateway’s own Settings page.

From outside the network (Remote Access)

Cloud-hosted platforms such as ServiceNow cannot reach a private LAN address. For those, enable Remote Access under Settings → Network → Services. This opens a secure outbound Cloudflare tunnel and publishes the gateway at a stable HTTPS hostname:

textRemote Access base URL
https://b00e.iolight.com      # <label-code>.iolight.com, HTTPS only

The exact hostname for your unit is shown on the Settings page once Remote Access is enabled. It requires no inbound firewall rule and no port forwarding, because the tunnel is established outbound from the gateway.

Settings, Network, Services panel showing the Remote Access toggle enabled with the gateway's public remote address, and the Node-RED service toggle below it.
Settings → Network → Services. With Remote Access on, the gateway’s public address is shown under Remote Address — that value, with https://, is your API base URL. The same panel carries the optional Node-RED toggle (Section 13).

Health check

One endpoint needs no authentication at all, which makes it useful as a liveness probe from a monitoring system:

GET /api/_health api No auth

Returns immediately if the API service is up.

bashRequest
curl http://192.168.1.50/api/_health
jsonResponse 200
{ "ok": true, "service": "atrium-api" }

3. Authentication

Atrium uses session tokens. You exchange a username and password for a bearer token, then send that token on every subsequent request. There is no OAuth flow and no separate API-key store.

Step 1 — Log in

POST /api/auth/login api No auth

Exchanges credentials for a session token. This is the same login the web interface uses, so any Atrium user account works.

Request body

FieldTypeRequiredDescription
usernamestringYesAtrium account username
passwordstringYesAccount password
bashRequest
curl -X POST http://192.168.1.50/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"ncdio","password":"your-password"}'
jsonResponse 200
{
  "token": "e407a4ba1f73b881c6d7bc819e0e2f6e640b75a1ccabb35eb4c3c1de2dade81b",
  "username": "ncdio",
  "role": "admin",
  "allowed_pages": null,
  "enabled": true
}

Response fields

FieldDescription
token64-character hex session token. Send this as your bearer credential.
roleadmin or viewer.
allowed_pagesnull for an admin. For a viewer, the array of interfaces an admin has granted.
enabledWhether the account is active.

Error responses

StatusBodyCause
400{"error":"Username and password required"}Either field missing or blank
401{"error":"Invalid credentials"}Wrong username or password
403{"error":"This account is disabled…"}Viewer account currently disabled by an admin

Step 2 — Send the token

Every other endpoint requires the token in an Authorization header using the Bearer scheme:

bashAuthenticated request
curl http://192.168.1.50/api/sensors \
  -H 'Authorization: Bearer e407a4ba1f73b881c6d7bc819e0e2f6e640b75a1ccabb35eb4c3c1de2dade81b'

Token lifetime and renewal

The correct pattern is to treat 401 as “log in again and retry”, rather than tracking expiry dates:

  1. 1
    Cache the token
    Log in once, keep the token in memory or a credential store, and reuse it across requests. Do not log in on every call — each login inserts a new session row on the gateway.
  2. 2
    Watch for 401
    On any 401 response, discard the cached token, call /api/auth/login again, and retry the original request once.
  3. 3
    Optionally refresh early
    If you prefer not to fail even once, re-authenticate on a fixed schedule — daily is comfortable and well inside the 7-day window.

Checking a token

GET /api/auth/verify api

Confirms a token is still valid and reports the account it belongs to. Useful as a cheap pre-flight after a long idle period.

jsonResponse 200
{ "valid": true, "username": "ncdio", "role": "admin",
  "allowed_pages": null, "enabled": true }
POST /api/auth/change-credentials api

Changes the authenticated account’s username and/or password. Requires the current password.

jsonRequest body
{
  "current_password": "old-password",
  "new_password": "new-password",
  "new_username": "integrations"   // optional; omit to keep the username
}

Recommended: a dedicated account

Rather than embedding the admin credentials in an external system, create a second account for the integration. Atrium supports one admin and one viewer account, and the viewer can be restricted to a granted set of interfaces — see Section 13.

4. Conventions

These rules hold across every endpoint. Read this section once and the rest of the guide becomes predictable.

Requests and responses

AspectBehavior
Content typeapplication/json for both requests and responses. Send Content-Type: application/json on any request with a body.
Empty POST bodiesAllowed. An empty body on a JSON POST is parsed as {}, so action endpoints can be called with no payload.
ParametersPath parameters are shown as :name. Query parameters are documented per endpoint. Bodies are JSON objects.
CORSEvery response carries Access-Control-Allow-Origin: *, and OPTIONS /api/* returns 204. Browser-based clients work without a proxy.
Unknown fieldsIgnored on write. Sending extra keys is harmless.
Partial updatesPUT and PATCH endpoints update only the fields you send. Omitted fields are left unchanged.

Timestamps

Two conveniences to be aware of:

  • Some objects also carry a pre-formatted ISO 8601 companion field — the sensor object, for example, has both last_seen_epoch and last_seen_utc.
  • The gateway’s display timezone (a Settings preference) affects the web interface and report rendering only. The API always speaks UTC. Convert on your side.
GET /api/system/time api

Returns the gateway’s current clock. Use it to detect clock skew between your system and the gateway before building time-window queries — if the two disagree, a “last 15 minutes” query can silently return nothing.

jsonResponse 200
{ "epoch_ms": 1788874900454, "iso": "2026-09-08T13:41:40.454Z" }

Status codes

CodeMeaning
200Success.
201Created. Returned by most resource-creating POST endpoints.
204No content. Returned for OPTIONS preflight.
400Bad request — missing or invalid parameters. The body explains what.
401Missing, invalid, or expired token. Re-authenticate.
402An installed app requires a license.
403Forbidden — admin-only endpoint, unsupported hardware, or a scoped token out of bounds.
404Resource not found, or the endpoint does not exist on this platform version.
409Conflict — the resource already exists, or an operation is already running.
500Server error.
502An underlying system helper failed — seen on network-configuration endpoints.
503A required subsystem is unavailable — e.g. MQTT not configured, or a helper script missing.

Error body shape

Errors return a JSON object with an error key. Authentication failures add a message; some validation failures add an errors array or a machine-readable code.

jsonRepresentative errors
// 401 — no token supplied
{ "error": "Authentication required", "message": "No authorization token provided" }

// 401 — token expired or unknown
{ "error": "Authentication failed", "message": "Invalid or expired session token" }

// 400 — validation
{ "error": "whitelist parameter must be 0 or 1" }

// 400 — multi-field validation
{ "error": "Validation failed", "errors": ["Label 1: metric is required"] }

// 503 — subsystem unavailable, with a code to branch on
{ "error": "wifi helper not installed", "code": "helper_missing" }

There is no realtime channel

Polling guidance

  • Match your poll interval to the sensor report interval, not to your dashboard’s refresh rate. Most NCD sensors report every 10 minutes by default; polling every 30 seconds for a sensor that reports hourly just burns cycles on an embedded gateway.
  • Poll incrementally. Track the newest timestamp you have already ingested and pass it as start_time on the next request, rather than re-fetching a fixed window.
  • Prefer one call per sensor+metric over broad queries. The readings table grows without bound, and the database index is built for device_id + metric + ts lookups.
  • Be gentle. The EG5100 is a single-core device with no swap. A tight polling loop across dozens of sensors is felt by the whole system.

5. Quick Start

A complete round trip — log in, list sensors, and pull readings — in three calls. Every response below was captured from a live gateway.

  1. 1
    Get a token
    bashLog in and capture the token
    GATEWAY="http://192.168.1.50"
    
    TOKEN=$(curl -s -X POST "$GATEWAY/api/auth/login" \
      -H 'Content-Type: application/json' \
      -d '{"username":"ncdio","password":"your-password"}' \
      | python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')
    
    echo "$TOKEN"
  2. 2
    Find your sensors
    bashList the sensor inventory
    curl -s "$GATEWAY/api/sensors" \
      -H "Authorization: Bearer $TOKEN"

    Note the device_id of a sensor you care about — it is the 64-bit radio MAC, and it is the key for every other call.

  3. 3
    Discover its metrics, then read them
    bashMetrics, then the 10 newest temperature readings
    DEVICE="00:13:a2:00:42:41:65:28"
    
    # Which metrics does this sensor actually report?
    curl -s "$GATEWAY/api/sensors/$DEVICE/metrics" \
      -H "Authorization: Bearer $TOKEN"
    # → {"device_id":"00:13:a2:00:42:41:65:28",
    #    "metrics":["battery_pct","battery_v","counter","humidity","rssi","temperature"]}
    
    # Pull the 10 most recent temperature readings
    curl -s "$GATEWAY/api/sensors/$DEVICE/telemetry?metric=temperature&limit=10" \
      -H "Authorization: Bearer $TOKEN"

6. Sensors and Inventory

The sensor object is the backbone of the API. It carries identity, your own asset metadata, the latest health readings, and a computed status — enough to build an asset list in an external system without any further calls.

GET /api/sensors api

Lists all whitelisted sensors, newest-reporting first. Returns a bare JSON array, not a wrapper object.

Query parameters

ParameterTypeDefaultDescription
qstringCase-insensitive substring search across device ID, sensor name, location, asset, and custom name. Also matches a device ID typed without separators.
limitinteger1000Maximum rows. Clamped to the range 1–5000.
bashRequest
curl -s "$GATEWAY/api/sensors?q=office&limit=50" \
  -H "Authorization: Bearer $TOKEN"
jsonResponse 200 — one element of the array
[
  {
    "device_id": "00:13:a2:00:42:41:65:28",
    "sensor_type": 1,
    "sensor_name": "1 - Temperature/Humidity",
    "firmware": 10,
    "last_seen_epoch": 1788874772898,
    "last_seen_utc": "2026-09-08T13:39:32.000Z",
    "last_rssi": 40,
    "last_battery_v": 3.29,
    "last_battery_pct": 99.64,
    "location": "Travis' Office",
    "asset": "Jesse's Desk",
    "name": "Travis Office Temperature/Humidity",
    "install_date": "2026-06-02",
    "report_interval": 600,
    "last_error": null,
    "last_error_ts": null,
    "first_seen_ts": 1788536218806,
    "is_online": true,
    "has_active_alert": false,
    "status": "healthy"
  }
]

The sensor object

FieldTypeDescription
device_idstringThe sensor’s 64-bit DigiMesh MAC. Immutable, and the key for every per-sensor endpoint.
sensor_typeintegerNCD numeric sensor type (e.g. 1 = Temperature/Humidity). See /api/sensor-types.
sensor_namestring|nullFactory-reported type name.
firmwareinteger|nullSensor firmware revision.
last_seen_epochinteger|nullLast packet received, epoch ms.
last_seen_utcstring|nullThe same instant as an ISO 8601 string, for convenience.
last_rssinumber|nullSignal strength from the last packet. Lower is better — this is a positive dBm magnitude, so 40 is a strong link and 90 is marginal.
last_battery_vnumber|nullBattery volts.
last_battery_pctnumber|nullBattery percentage, 0–100.
locationstring|nullOperator-assigned location. Writable — see Section 8.
assetstring|nullOperator-assigned asset. Use this to carry your CMMS asset ID.
namestring|nullOperator-assigned friendly name, overriding sensor_name in the UI.
install_datestring|nullYYYY-MM-DD.
report_intervalintegerExpected seconds between reports. Defaults to 600. Drives the offline calculation.
last_errorstring|nullMost recent sensor-reported error, if any.
last_error_tsinteger|nullWhen that error arrived, epoch ms.
first_seen_tsinteger|nullFirst time the gateway ever saw this device. Present on the list endpoint only.
is_onlinebooleantrue when the last packet is newer than twice report_interval.
has_active_alertbooleantrue if any enabled trigger or alert template is currently firing for this device. Present on the list endpoint only.
statusstringComputed health. See the table below.

How status is computed

Evaluated in this order, first match wins:

StatusCondition
offlineis_online is false — no packet within twice the report interval.
errorA last_error is present, or battery ≤ 30%, or RSSI ≥ 75 (weak signal).
healthyEverything else.
GET /api/sensors/:id api

A single sensor, keyed by device_id. Returns the same object as the list endpoint, minus first_seen_ts and has_active_alert.

bashRequest
curl -s "$GATEWAY/api/sensors/00:13:a2:00:42:41:65:28" \
  -H "Authorization: Bearer $TOKEN"

Returns 404 with {"error":"Sensor not found"} for an unknown device ID.

GET /api/sensor-types api

Every sensor type present on this gateway with a count of devices, most common first. A cheap way to learn what hardware is deployed before iterating the full inventory.

jsonResponse 200
[
  { "type": 111, "count": 449 },
  { "type": 115, "count": 209 },
  { "type": 1,   "count": 60  }
]
GET /api/sensors/:id/status-history api

Health transitions for one sensor, newest first — the audit trail behind the current status. Useful for reliability reporting and for confirming an intermittent sensor really is intermittent.

Query parameters

ParameterTypeDefaultDescription
limitinteger50Maximum transitions returned.
jsonResponse 200
[
  {
    "id": 4127,
    "device_id": "00:13:a2:00:42:41:65:28",
    "old_status": "healthy",
    "new_status": "offline",
    "reason": "no report within 2x interval",
    "ts": 1788861600000
  }
]

7. Telemetry Readings

This is the endpoint most integrations spend all their time in. Readings are stored as one row per (timestamp, device_id, metric), so a single sensor reporting temperature and humidity produces two rows per report.

GET /api/sensors/:id/metrics api

Lists the metric names this sensor has actually reported. Always call this before hard-coding metric names — the available metrics depend on sensor type, firmware, and configuration, not on a fixed catalog.

jsonResponse 200
{
  "device_id": "00:13:a2:00:42:41:65:28",
  "metrics": ["battery_pct","battery_v","counter","humidity","rssi","temperature"]
}

Note that battery_pct, battery_v, rssi, and counter appear alongside the real measurements — the gateway stores sensor health as ordinary metrics, so you can trend battery life exactly like temperature.

GET /api/sensors/:id/telemetry api

Returns readings for one sensor, optionally filtered to a single metric and time window.

Query parameters

ParameterTypeDefaultDescription
metricstringall metricsRestrict to one metric name. Strongly recommended — without it you get every metric interleaved in one list.
start_timeintegerInclusive lower bound on timestamp, epoch ms.
end_timeintegerInclusive upper bound on timestamp, epoch ms.
limitinteger1000Maximum rows, capped at 10000. Applies to raw queries only.
bucket_msintegerRequests time-bucketed aggregation. See the warning below before using this.
bashThe three requests you will actually make
# 1. Current value of one metric
curl -s "$GATEWAY/api/sensors/$DEVICE/telemetry?metric=temperature&limit=1" \
  -H "Authorization: Bearer $TOKEN"

# 2. Everything new since the last time you polled (incremental — do this)
curl -s "$GATEWAY/api/sensors/$DEVICE/telemetry?metric=temperature&start_time=1788874000000&limit=5000" \
  -H "Authorization: Bearer $TOKEN"

# 3. A fixed historical window
curl -s "$GATEWAY/api/sensors/$DEVICE/telemetry?metric=temperature&start_time=1788790000000&end_time=1788876400000" \
  -H "Authorization: Bearer $TOKEN"
jsonResponse 200 — raw readings
{
  "device_id": "00:13:a2:00:42:41:65:28",
  "metric": "temperature",
  "bucket_ms": null,
  "count": 3,
  "readings": [
    {
      "timestamp": 1788874772898,
      "device_id": "00:13:a2:00:42:41:65:28",
      "metric": "temperature",
      "value": 25.2,
      "counter": 14,
      "sensor_type": 1,
      "firmware": 10,
      "rssi": 40,
      "tags": "{\"nodeId\":0}"
    },
    {
      "timestamp": 1788874742279,
      "device_id": "00:13:a2:00:42:41:65:28",
      "metric": "temperature",
      "value": 25.26,
      "counter": 13,
      "sensor_type": 1,
      "firmware": 10,
      "rssi": 40,
      "tags": "{\"nodeId\":0}"
    }
  ]
}

The reading object

FieldTypeDescription
timestampintegerWhen the gateway recorded the reading, epoch ms UTC.
device_idstringSource sensor.
metricstringMetric name.
valuenumberThe measurement, in the sensor’s native unit.
counterinteger|nullSensor’s own transmission counter. Gaps indicate packets that never arrived — useful for link-quality auditing.
sensor_typeinteger|nullType as reported in that packet.
firmwareinteger|nullFirmware as reported in that packet.
rssiinteger|nullSignal strength for that packet.
tagsstring|nullA JSON-encoded string, not an object — e.g. "{\"nodeId\":0}". Parse it a second time if you need the contents.

Bucketed aggregation

Passing bucket_ms switches the endpoint into an aggregating mode that groups readings into fixed time buckets and returns an average per bucket, plus min_v, max_v, and a sample count n. Accepted values are clamped to between 1 second and 24 hours, and the aggregated result is capped at 5000 buckets.

jsonAggregated response shape (fields, not values, are reliable)
{
  "device_id": "00:13:a2:00:42:41:65:28",
  "metric": "temperature",
  "bucket_ms": 3600000,
  "count": 250,
  "readings": [
    {
      "timestamp": 1788867578278,   // bucket start (currently NOT aligned — see warning)
      "device_id": "00:13:a2:00:42:41:65:28",
      "metric": "temperature",
      "value": 24.71,               // mean of the bucket
      "min_v": 24.71,
      "max_v": 24.71,
      "n": 1,                       // samples in the bucket
      "counter": 35,
      "sensor_type": 1,
      "firmware": 10,
      "rssi": 40
    }
  ]
}

Note that bucketed results are ordered ascending by bucket, the opposite of raw results.

GET /api/sensors/:id/fft api

Lists stored raw vibration captures for a vibration sensor, newest first. Returns capture metadata — sample rate, sample count, full-scale range, temperature, RPM, and a confidence figure — not the waveform itself.

ParameterTypeDefaultDescription
start_timeintegerLower bound, epoch ms.
end_timeintegerUpper bound, epoch ms.
limitinteger50Maximum captures, capped at 200.
jsonResponse 200
{
  "device_id": "00:13:a2:00:5a:00:00:07",
  "count": 1,
  "fft_readings": [
    { "id": 91, "timestamp": 1788870000000, "device_id": "00:13:a2:00:5a:00:00:07",
      "sensor_type": 110, "odr": 3200, "total_samples": 4096, "fsr": 16,
      "temperature": 31.4, "rpm": 1780, "fft_confidence": 0.93,
      "created_at": 1788870001200 }
  ]
}
GET /api/sensors/:id/fft/:fftId api

Retrieves one stored capture by its id. To compute a spectrum from it, see Section 15.

8. Metadata, Labels and Units

Atrium stores operator-supplied context alongside each sensor. These endpoints are writable, which makes the gateway a downstream consumer of your system of record rather than a second place to maintain asset names by hand.

GET /api/sensors/:id/meta api

The raw metadata row for one sensor. Returns {} if no metadata has ever been saved.

jsonResponse 200
{
  "device_id": "00:13:a2:00:42:41:65:28",
  "location": "Travis' Office",
  "asset": "Jesse's Desk",
  "name": "Travis Office Temperature/Humidity",
  "install_date": "2026-06-02",
  "report_interval": 600,
  "offline_alert_enabled": 1,
  "offline_alert_emails": "travis@ncd.io"
}
POST /api/sensors/:id/meta api

Creates or replaces the metadata row and returns the merged result.

Request body

FieldTypeNotes
locationstringFree text. Blank or omitted becomes null.
assetstringFree text. The natural home for an external asset ID.
namestringFriendly name. sensor_name is accepted as an alias.
install_datestringYYYY-MM-DD. A longer date string is truncated to that form.
report_intervalintegerSeconds. Defaults to 600 if omitted or unparseable. Drives offline detection.
offline_alert_enabledbooleanEmail when this sensor stops reporting.
offline_alert_emailsstringRecipient list for the offline alert.
bashPush your CMMS asset ID onto a sensor
curl -s -X POST "$GATEWAY/api/sensors/$DEVICE/meta" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "location": "Plant 2 / Line 4",
        "asset": "PUMP-00417",
        "name": "Line 4 Feed Pump — Bearing",
        "install_date": "2026-06-02",
        "report_interval": 600,
        "offline_alert_enabled": true,
        "offline_alert_emails": "maintenance@example.com"
      }'

The response is the stored row, so you can confirm exactly what was persisted.

GET /api/sensors/:id/metric-labels api

Custom display labels for this sensor’s metrics.

jsonResponse 200
{ "labels": [ { "metric": "temperature", "label": "Bearing Temp" } ] }
POST /api/sensors/:id/metric-labels api

Sets labels in bulk. Send {"labels":[{"metric":"...","label":"..."}]}. An empty-string label deletes that label.

The core metrics battery_pct, rssi, and counter cannot be relabeled; attempting it returns 400 with a per-item errors array.

GET /api/sensors/:id/metric-conversions api

Unit-conversion formulas configured for this sensor’s metrics. Important for integrators: these are applied by the web interface for display. Stored telemetry — and therefore what the API returns — is always unconverted. If a customer has configured a conversion, read it here and apply the same arithmetic so your figures match what they see on screen.

jsonResponse 200
{
  "conversions": [
    { "metric": "temperature", "formula": "value * 9 / 5 + 32",
      "unit": "°F", "label": "Bearing Temp", "visible": true }
  ]
}

The formula is a JavaScript expression in a single variable, value.

POST /api/sensors/:id/metric-conversions api

Sets conversions in bulk via {"conversions":[…]}. Each formula is validated by evaluation against sample inputs before it is stored, so a malformed expression is rejected with 400 rather than silently breaking the customer’s dashboard. Core metrics cannot be converted.

GET /api/sensors/:id/chart-order api

The operator’s preferred chart ordering for this sensor’s metrics, as an array of metric names. Purely a display preference.

POST /api/sensors/:id/chart-order api

Sets it. Body is {"order":["temperature","humidity"]}; maximum 100 entries.

Synthetic variables

Synthetic variables are computed metrics — a formula over readings from one or more real sensors, stored back into the readings table under the device ID synthetic. Use them to expose a derived value (a differential, an efficiency figure, a total) to your integration without recomputing it downstream.

GET /api/synthetic-variables api

Lists all synthetic variables as {synthetic_variables:[…], count}. Each carries id, name, formula, unit, label, and its inputs array.

GET /api/synthetic-variables/:id api

One synthetic variable. 404 if unknown.

POST /api/synthetic-variables api

Creates one. Requires name, formula, and a non-empty inputs array in which every entry has var, device_id, and metric. The formula is validated before saving. Returns 201 with the generated id.

jsonRequest body
{
  "name": "supply_return_delta",
  "formula": "supply - return_",
  "unit": "°C",
  "label": "Supply/Return Δ",
  "inputs": [
    { "var": "supply",  "device_id": "00:13:a2:00:42:41:65:28", "metric": "temperature" },
    { "var": "return_", "device_id": "00:13:a2:00:42:41:65:99", "metric": "temperature" }
  ]
}

Read the resulting values back from the readings table using device ID synthetic and the variable’s name as the metric.

POST /api/synthetic-variables/test api

Dry-runs a formula against literal values without saving anything. Body is {"formula":"a+b","inputs":[{"var":"a","value":1},{"var":"b","value":2}]}; returns {"ok":true,"result":3,…}, or 400 with the evaluation error.

DELETE /api/synthetic-variables/:id api

Deletes the variable and purges its computed readings. Not reversible.

9. Device Management

Atrium separates devices it has heard from devices it is watching. A gateway in a busy RF environment often sees hundreds of neighboring NCD sensors; only whitelisted ones appear in the sensor list and generate alerts.

GET /api/devices api

Lists devices on one side of the whitelist.

Query parameters

ParameterTypeRequiredDescription
whitelist0 or 1Yes1 for whitelisted devices, 0 for those seen but not whitelisted.
jsonResponse 200 — bare array
[
  { "device_id": "00:13:a2:00:42:38:62:c6", "sensor_type": 115,
    "sensor_name": null, "is_whitelisted": false, "last_seen_ts": 1780690645050 }
]
POST /api/sensors/:id/whitelist api

Whitelists or un-whitelists a single device. Body is {"is_whitelisted": true}. Anything other than boolean true is treated as false.

POST /api/devices/bulk-whitelist api

Whitelists or un-whitelists many devices at once — the efficient way to commission a batch of sensors.

jsonRequest body
{
  "device_ids": ["00:13:a2:00:42:41:65:28", "00:13:a2:00:42:41:65:99"],
  "is_whitelisted": true
}

Returns {"ok":true,"updated_count":2,"is_whitelisted":true}. An empty or missing array returns 400.

POST /api/devices/bulk-delete api

Removes devices and all associated data — every reading, raw ingest row, metadata row, and alert condition — in a single transaction.

Body is {"device_ids":[…]}; returns {"ok":true,"deleted_count":n}.

POST /api/purge-all-data api

Deletes all telemetry, devices, metadata, and alert rules, then compacts the database. Configuration, users, and sessions survive.

10. Alerts and Alert History

For a CMMS integration, alert history is often more valuable than raw telemetry: it is already a list of discrete events with a device, a metric, a value, and a trigger/clear state — the natural shape of a work order.

GET /api/alerts/history api

The alert event log, newest first. This is the endpoint to poll for CMMS work-order creation.

Query parameters

ParameterTypeDefaultDescription
qstringSubstring match against device ID, sensor name, or metric.
limitinteger100Page size.
offsetinteger0Rows to skip, for paging.
bashRequest
curl -s "$GATEWAY/api/alerts/history?limit=50" \
  -H "Authorization: Bearer $TOKEN"
jsonResponse 200
{
  "history": [
    {
      "id": 338,
      "device_id": "00:13:a2:00:42:41:65:28",
      "sensor_name": "Temperature/Humidity",
      "metric": "automation",
      "alert_type": "automation",
      "event_type": "triggered",
      "value": null,
      "threshold_value": null,
      "condition": "High Temperature Alarm Email",
      "timestamp": 1788190592479,
      "recipient_email": "travis@ncd.io",
      "created_at": 1788190592000,
      "sensor_type": 1
    }
  ],
  "count": 1,
  "limit": 50,
  "offset": 0,
  "search": null
}

The alert-history object

FieldTypeDescription
idintegerMonotonically increasing event ID. Use this as your high-water mark when polling incrementally.
device_idstringSensor that caused the event.
sensor_namestring|nullDisplay name at the time of the event.
metricstringThe metric that breached — or the literal "offline" or "automation" for those event kinds.
alert_typestringthreshold, offline, template, or automation.
event_typestringtriggered or cleared. Both are logged — match them up to compute a duration.
valuenumber|nullReading that caused it. null for offline and automation events.
threshold_valuenumber|nullThe limit that was crossed.
conditionstring|nullabove or below for thresholds. For an automation event this carries the automation’s name instead — a quirk worth handling.
timestampintegerWhen the event occurred, epoch ms.
recipient_emailstring|nullWho was emailed, if anyone.
created_atintegerWhen the row was written, epoch ms.
sensor_typeinteger|nullJoined from the device record.
GET /api/alerts api

Current alert-rule state — one row per rule condition, with active rules first. This is a state view (“what is wrong right now”), where alert history is an event view (“what happened”).

jsonResponse 200
{
  "alerts": [
    { "id": 12, "device_id": "00:13:a2:00:42:41:65:28",
      "sensor_name": "Travis Office Temperature/Humidity",
      "metric": "temperature", "threshold_value": 30, "condition": "above",
      "swing_value": 1.0, "recipient_email": null,
      "alert_active": true, "enabled": true, "created_at": 1780499419019 }
  ],
  "count": 1
}

An empty fleet returns {"alerts":[],"count":0}.

GET /api/sensors/:id/thresholds api

Threshold rules scoped to one sensor, including live alert_active state and the email recipients attached to each rule.

jsonResponse 200
{
  "device_id": "00:13:a2:00:42:41:65:28",
  "thresholds": [
    { "id": 12, "device_id": "00:13:a2:00:42:41:65:28", "metric": "temperature",
      "threshold_value": 30, "condition": "above", "enabled": true,
      "swing_value": 1.0, "alert_active": false,
      "last_alert_time": null, "created_at": 1780499419019,
      "recipient_email": "maintenance@example.com" }
  ]
}
POST /api/sensors/:id/thresholds api

Creates or replaces a simple threshold on one metric.

Request body

FieldTypeRequiredDescription
metricstringYesMetric to watch.
threshold_valuenumberYesThe limit.
conditionstringYesabove or below. Any other value returns 400.
swing_valuenumberNoHysteresis band, default 1.0. Must be ≥ 0. Prevents an alert flapping on a value hovering at the limit.
enabledbooleanNoDefaults to true.
recipient_emailstringNoNotification recipient.
DELETE /api/thresholds/:threshold_id api

Deletes an alert rule by ID. Despite the legacy path name, :threshold_id is an alert-rule ID — the id from GET /api/alerts. Returns {"ok":true,"deleted_id":n}.

Alert templates

A template applies one threshold rule to every sensor matching a pattern — all sensors of a type, or everything in a location or asset group — instead of configuring each device individually. Newly discovered matching sensors inherit it automatically.

GET /api/alert-templates api

All templates, each annotated with matched_device_count so you can see the blast radius before changing one.

jsonResponse 200
{
  "templates": [
    { "id": 3, "name": "Cold room over-temp",
      "match_type": "location", "match_value": "Cold Room A",
      "metric": "temperature", "threshold_value": 8, "condition": "above",
      "swing_value": 0.5, "recipient_email": "ops@example.com",
      "enabled": true, "matched_device_count": 12,
      "created_at": 1780499419019, "updated_at": 1788190592479 }
  ],
  "count": 1
}
GET /api/alert-templates/match-preview api

Previews which devices a match rule would cover, without creating anything. Call this before POSTing a template.

ParameterTypeDefaultDescription
match_typestringallall, sensor_type, location, or asset.
match_valuestringThe value to match. Ignored when match_type is all.
bashRequest
curl -s "$GATEWAY/api/alert-templates/match-preview?match_type=location&match_value=Cold%20Room%20A" \
  -H "Authorization: Bearer $TOKEN"

Returns {"devices":[{device_id, sensor_name, sensor_type, display_name, location, asset}], "count":n}.

POST /api/alert-templates api

Creates a template. Requires name, match_type, metric, and threshold_value; match_value is required unless match_type is all. condition defaults to above, swing_value to 1.0. Returns 201.

PUT /api/alert-templates/:id api

Partial update — send only the fields you want changed.

DELETE /api/alert-templates/:id api

Deletes a template. Returns {"ok":true,"deleted_id":n}.

11. Triggers and Automations

Triggers and automations are the modern, fully manageable alerting path. A trigger is a named condition on one sensor metric. An automation combines one or more triggers with one or more actions. This is the pair to use when your integration needs to create alerting that the customer can also see and edit in the web interface.

GET /api/triggers api

All triggers, each enriched with the sensor’s display name and its current live value for that metric — so one call tells you both the rule and how close it is to firing.

ParameterTypeDescription
device_idstringRestrict to one sensor.
jsonResponse 200
{
  "triggers": [
    { "id": 14, "name": "High Temperature Alarm",
      "device_id": "00:13:a2:00:42:41:65:28", "metric": "temperature",
      "threshold_value": 23.888424038887024, "condition": "above",
      "swing_value": 1, "condition_met": true, "enabled": true,
      "sensor_display_name": "Travis Office Temperature/Humidity",
      "current_value": 25.2,
      "created_at": 1780499419019, "updated_at": 1788190592479 }
  ],
  "count": 1
}

condition_met is the live evaluated state, including hysteresis — not a recomputation of current_value against threshold_value. Trust condition_met.

POST /api/triggers api

Creates a trigger. Requires name, device_id, metric, and threshold_value. condition defaults to above (any value other than below becomes above), swing_value to 1.0. Returns 201.

jsonRequest body
{
  "name": "Bearing over-temp",
  "device_id": "00:13:a2:00:42:41:65:28",
  "metric": "temperature",
  "threshold_value": 85,
  "condition": "above",
  "swing_value": 2
}
PUT /api/triggers/:id api

Partial update of name, threshold_value, condition, swing_value, or enabled. Note that device_id and metric cannot be changed — delete and recreate to re-point a trigger.

DELETE /api/triggers/:id api

Deletes a trigger.

GET /api/automations api

All automations with their triggers and actions nested inline — a complete picture of the customer’s alerting logic in one call.

jsonResponse 200
{
  "automations": [
    {
      "id": 13,
      "name": "High Temperature Alarm Email",
      "logic": "AND",
      "enabled": true,
      "active": true,
      "last_triggered_time": 1788190592479,
      "created_at": 1780499419019,
      "updated_at": 1788190592479,
      "triggers": [
        { "id": 14, "name": "High Temperature Alarm",
          "device_id": "00:13:a2:00:42:41:65:28", "metric": "temperature",
          "threshold_value": 23.888424038887024, "condition": "above",
          "swing_value": 1, "condition_met": true,
          "sensor_display_name": "Travis Office Temperature/Humidity" }
      ],
      "actions": [
        { "id": 13, "action_type": "email",
          "config": { "recipients": "travis@ncd.io", "subject": "", "body": "" },
          "enabled": true, "fires_on": "trigger" }
      ]
    }
  ]
}
FieldDescription
logicHow multiple triggers combine. Currently always AND — every trigger must be met.
activeWhether the automation is currently in its triggered state.
enabledWhether it is being evaluated at all.
actions[].action_typeemail or relay_command. There is no HTTP or webhook action type.
actions[].fires_ontrigger, clear, or both — which edge runs the action.
POST /api/automations api

Creates an automation from existing triggers. Requires name, a non-empty trigger_ids array, and a non-empty actions array. Returns 201 with the new id.

jsonRequest body
{
  "name": "Bearing over-temp → maintenance",
  "trigger_ids": [14],
  "actions": [
    { "action_type": "email",
      "config": { "recipients": "maintenance@example.com",
                  "subject": "Bearing over-temperature",
                  "body": "" },
      "fires_on": "trigger" }
  ]
}
PUT /api/automations/:id api

Updates an automation. Supplying trigger_ids or actions replaces the existing sets rather than adding to them.

DELETE /api/automations/:id api

Deletes an automation. Its triggers survive and can be reused.

12. Event Bus

The inter-app event bus lets installed Atrium apps publish events and expose actions, which an operator wires together on the Automations page. Most external integrations will not need it — but two of its read endpoints are genuinely useful as a diagnostic feed.

How it works, briefly

The bus is level-triggered. Producers publish the current state of an entity rather than an edge event, the broker retains the last value per topic and entity, and a binding fires only when its match result changes. The practical consequence for you: a value you read from /api/bus/state is the current truth, not a transient you might have missed.

GET /api/bus/state api

The retained current value for every (topic, entity) pair — “what does the bus believe right now”. Filter with topic, source, or a free-text q.

jsonResponse 200
{
  "state": [
    { "topic": "atrium/alert", "entity_key": "00:13:a2:00:42:41:65:28:temperature",
      "source": "atrium", "seq": 8814, "ts": 1788190592479,
      "payload": { "kind": "threshold", "state": "triggered", "active": true,
                   "device_id": "00:13:a2:00:42:41:65:28",
                   "metric": "temperature", "value": 25.2, "threshold": 23.88,
                   "condition": "above" } }
  ],
  "count": 1
}
GET /api/bus/events api

The recent event log, newest first. Roughly a week of traffic is retained.

ParameterTypeDefaultDescription
topicstringExact topic match, e.g. atrium/alert.
sourcestringPublishing source — atrium or an app ID.
entity_keystringExact entity match.
sinceintegerEpoch-ms floor on ts. Use the gateway’s clock, from /api/system/time.
qstringFree-text contains across topic, entity, and payload.
limitinteger100Capped at 500.

Each event carries seq, topic, entity_key, source, a parsed payload object, ts, origin, and delivered.

GET /api/bus/catalog api

Everything wireable on this gateway: sources, publishable events with their payload schemas, and invocable actions. This is how you discover what the installed apps expose without reading their manifests.

GET /api/bus/bindings api

Configured event→action bindings. Filter by source_topic or target_app.

GET /api/bus/bindings/:id api

One binding, plus its per-entity match state and its 50 most recent dispatch runs — the first place to look when a customer says an automation “didn’t fire”.

POST /api/bus/bindings api

Creates a binding. Requires name, a source_topic of the form <source>/<name>, and target_kind of app or email; app additionally requires target_app and target_action. fires_on is enter, exit, or both (default). match, params, and clear_params are JSON. Returns 201 with the created binding.

PATCH /api/bus/bindings/:id api

Partial update. Re-enabling or re-pointing a binding also clears its failure counter and circuit breaker.

DELETE /api/bus/bindings/:id api

Deletes a binding; its state and run history cascade away with it.

GET /api/bus/runs api

Dispatch history across all bindings. Filter by binding_id, ok (true/false), edge (enter/exit/manual), since, and free-text q; limit defaults to 100, capped at 500.

POST /api/bus/publish api

Publishes a diagnostic event into the atrium namespace. Body is {"topic":"my-event","key":"entity-1","payload":{…}}; topic must be an unqualified name and is published as atrium/<topic>.

This lets an external system inject an event that the customer can then wire to an Atrium action — the one place the API can push into gateway logic. Delivery is asynchronous, handled by the daemon within about a second.

GET /api/bus/status ingest

Broker counters plus which declared actions are actually bound. The answer to “why is my binding reporting action not available“.

POST /api/bus/bindings/:id/run ingest

Dispatches a binding immediately against its current retained state, bypassing the circuit breaker — an end-to-end wiring test. Body may set {"edge":"exit"}; the default is enter.

13. Gateway Configuration and System

Read the gateway’s identity and settings, manage user accounts, and drive firmware updates.

GET /api/gateway/config api

The full configuration as a flat key/value object. Call this first in any integration — it tells you the platform version and hardware model, which determine which endpoints exist.

jsonResponse 200 — representative keys
{
  "gateway_name": "Gateway-D8FC",
  "platform_version": "2.5.3",
  "gateway_model": "EG5120",
  "display_timezone": "America/Chicago",
  "data_retention_days": "30",
  "temperature_unit": "C",
  "dashboard_refresh_interval": "30",
  "new_sensor_handling": "whitelist",
  "default_home_page": "/sensors",
  "remote_access_enabled": true,
  "remote_access_provisioned": true,
  "remote_access_hostname": "d8fc.iolight.com",
  "nodered_enabled": false,
  "proxy_enabled": false,
  "proxy_scheme": "http",
  "proxy_auth_type": "none"
}

Keys worth knowing

KeyTypeDescription
gateway_namestringDisplay name. Also substituted into MQTT topic paths as {gateway_id}.
platform_versionstringAtrium version, e.g. 2.5.3. Gate your feature use on this.
gateway_modelstringEG5120 or EG5100. The EG5100 has no cellular modem and hides Remote Access.
data_retention_daysstringHow long telemetry is kept before nightly deletion. Your integration must poll often enough to stay inside this window, or you will lose data permanently.
temperature_unitstringC or F. A display preference only — the API always returns native units.
display_timezonestringIANA zone for the UI and reports. The API always returns UTC.
remote_access_enabledbooleanWhether the tunnel is intended to be up.
remote_access_provisionedbooleanWhether this unit actually has a tunnel provisioned. If enabled is true but this is false, remote access will never come up.
remote_access_hostnamestring|nullThe public hostname, with no scheme. Prefix with https://.
POST /api/gateway/config api

Writes the settings keys. gateway_name is required on every call; omitting it returns 400.

Toggling remote_access_enabled starts or stops the tunnel immediately. If the unit was never provisioned, the response includes a warning and the setting is stored but has no effect.

GET /api/users api

Admin username plus viewer-account state. Admin only — a viewer token gets 403 {"error":"Admin access required"}.

jsonResponse 200
{
  "admin_username": "ncdio",
  "viewer": { "exists": true, "username": "integrations",
              "enabled": true, "allowed_pages": ["/sensors","/monitor"] }
}
POST /api/users/viewer api

Creates or updates the single viewer account — the recommended way to provision a dedicated login for an integration. Admin only.

jsonRequest body
{
  "username": "integrations",
  "password": "a-strong-unique-password",
  "enabled": true,
  "allowed_pages": ["/sensors", "/monitor"]
}
  • A blank password preserves the existing one, so you can change grants without knowing it.
  • A password is required when creating the account for the first time.
  • The viewer username must differ from the admin’s.
  • Setting enabled: false immediately invalidates that account’s active tokens.
DELETE /api/users/viewer api

Removes the viewer account and its sessions. Admin only.

Firmware updates

Atrium updates itself over the air from NCD’s public release repository. These endpoints drive that process; all four are on atrium-api.

POST /api/system/check-updates api

Queries the release repository and caches the result. Returns {"ok":true,"update_available":bool,"available_version":string|null}. A transient network failure still returns 200 with ok:false and an error string, so branch on ok rather than on the status code.

POST /api/system/execute-update api

Applies the update found by the last check. Returns immediately with {"ok":true,"status":"running"}; the work happens in the background. 400 if no update is pending.

POST /api/system/force-update api

Installs a specific version regardless of what the check found. Body is {"version":"2.5.3"}; required. Returns 404 if no such release or package exists.

GET /api/system/update-progress api

Poll this during an update. Returns {step, message, percent, status, timestamp}, with status of idle, running, or error. When nothing is running you get {"step":"none","status":"idle","percent":0}.

Sensor library

The decoder library that turns radio packets into named metrics is versioned separately from the platform and can be updated without a full firmware update.

GET /api/system/sensor-library api

Returns {package, installed_version, latest_version, update_available, last_checked, updating, progress}.

POST /api/system/sensor-library/check api

Queries the npm registry for the newest published version and caches it. Like the firmware check, registry failures return 200 with ok:false.

POST /api/system/sensor-library/update api

Installs a version — {"version":"2.0.4"}, or the cached latest if omitted — then restarts the ingest daemon. Returns 409 if an update is already running, 400 for a malformed version or when nothing is cached.

Because this restarts the daemon, the radio stops briefly and radio-coupled endpoints will fail for a few seconds.

GET /api/system/sensor-library/update-progress api

Progress of an in-flight library update, in the same shape as the firmware progress endpoint.

Email and outbound proxy

POST /api/gateway/smtp-test ingest

Sends a single test email using credentials supplied in the body, without saving them. Returns 200 on success; 400 for a validation failure and 500 for a send failure, with a stage field indicating where it went wrong.

GET /api/gateway/proxy api

Outbound HTTP proxy settings, for gateways behind a corporate proxy. Secrets are never returned — instead you get has_password, has_client_cert, has_client_key, and has_ca_cert booleans.

POST /api/gateway/proxy api

Saves proxy settings. When proxy_enabled is true, proxy_host is required and proxy_port must be 1–65535. Any supplied PEM material is validated before it is persisted. Supports none, basic, and digest authentication, plus client certificates.

POST /api/gateway/proxy-test api

Tests a proxy configuration before committing it, optionally against your own test_url. Returns the observed HTTP status and a stage on failure.

Node-RED

Node-RED ships installed but dormant. A customer can enable it to run their own flows against the gateway’s raw-frame TCP feed on port 2101. It is never in the /api path and never owns the radio.

GET /api/system/nodered api

Returns {enabled, running, registered, status}, separating the user’s intent (enabled) from live process state.

POST /api/system/nodered api

Body {"enabled": true|false}. Starts or removes the process and persists the choice across reboots. Once running, the Node-RED editor is on port 1880.

14. Network Configuration

Atrium manages the gateway’s own WAN, LAN, WiFi, cellular, and NTP settings. These endpoints exist so the customer never has to visit the underlying router dashboard.

The commit-confirm pattern

  1. 1
    Apply the change
    POST to /api/network/ipv4, /api/network/lan, or /api/network/wifi. The gateway applies it and arms a rollback timer.
  2. 2
    Re-establish contact
    Reach the gateway again at its new address. If you cannot, do nothing — the watchdog reverts the change on its own.
  3. 3
    Confirm
    POST /api/network/confirm to make the change permanent. Miss the window and the gateway returns to its previous configuration.
GET /api/network/status api

Complete network state — interfaces, addresses, cellular, NTP, and the available timezone list. Read-only and always safe.

On a gateway that cannot report (helper not installed) it returns 200 with {"ok":true,"available":false,"code":"helper_missing"} rather than an error, so treat available as the gate. On an EG5100, cellular is null and cellular_supported is false — that is the hardware fact, not a missing configuration.

POST /api/network/confirm api

Confirms a pending network change, cancelling the rollback. Returns {"ok":true,"confirmed":true}. Takes no body.

POST /api/network/ipv4 api

Sets the IPv4 configuration of one interface. Requires target (wifi or ethernet) and connection_type (dhcp or manual); a manual configuration also takes address, gateway, dns1, and dns2. Arms the watchdog.

POST /api/network/lan api

Sets the LAN address and DHCP server pool: address, dhcp_enable, pool_start, pool_end, dns1, dns2, lease_time. Subnet and pool arithmetic is validated before anything is applied. Always requires the watchdog.

GET /api/network/wifi/status api

Current WiFi mode, SSID, link state, IP, and the reason for any previous rollback. Also reports watchdog — if false, a client-mode switch will be refused.

GET /api/network/wifi/scan api

Scans for visible networks and returns {"ok":true,"networks":[…]}. Takes up to 30 seconds — set your client timeout accordingly.

POST /api/network/wifi api

Switches WiFi mode. Requires mode of ap or client; client mode also takes ssid, password, security, hidden, and bssid. A client switch requires the watchdog, so a wrong password reverts to the access point instead of stranding the unit.

POST /api/network/cellular api

Configures the cellular modem: primary_sim (sim1/sim2), apn, username, password, auth_type (none/chap/pap), and optional extra_at_cmd. A username is required when authentication is CHAP or PAP.

EG5120 only. On an EG5100 this returns 403 with {"code":"unsupported_hardware"} — enforced server-side, not merely hidden in the UI.

POST /api/network/ntp api

Sets time synchronization: primary_server, optional secondary_server, and timezone. Servers must be a hostname or IPv4 address.

15. Radio, Mesh and Sensor Configuration

These endpoints reach the DigiMesh radio itself. They live on the ingest daemon because only that process owns the serial port — and unlike a database read, they have physical consequences.

GET /api/sensors/config-options/:type ingest

The configuration schema for a sensor type — every option, its valid range, and its meaning. Call this before attempting to write a sensor configuration, so you send only fields the hardware accepts.

ParameterTypeDefaultDescription
:typeintegerPath parameter: the numeric sensor type.
firmwareinteger1Firmware revision, since options vary by revision.

Returns 400 with an error string for a type the sensor library does not recognize.

GET /api/sensors/:id/sensor-config ingest

One sensor’s configuration: reported_configs (what the sensor last told us), desired_configs (what is queued for it), plus report_interval_info explaining the expected cadence and diagnostics for read-only counters. A device that has never checked in returns {"reported_configs":{},"desired_configs":{}}.

Diagnostic values arrive on the sensor’s check-in packet rather than with telemetry, so they refresh on the sync interval (default one hour). Present them as “as of last check-in”, not as live values.

POST /api/sensors/:id/sensor-config ingest

Queues a configuration change. The body is an array, not an object: [{"addr":"<device_id>","configs":{…},"type":<sensor_type>}].

GET /api/mesh-topology api

The whole mesh as {nodes, edges, routes} — a graph you can render directly. Nodes carry a role of gateway, repeater, or sensor plus their hop depth; edges carry link-test results including retry counts and min/max/average RSSI. A pure database read, safe to poll.

GET /api/sensors/:id/mesh-map api

The stored route for one sensor: status, route array, hop_count, and timing. Returns {"exists":false,"device_id":"…"} if no mapping has ever been requested.

POST /api/sensors/:id/mesh-map ingest

Queues a route-discovery request. Returns {"success":true,"device_id":"…","status":"pending"}. Poll the GET above for the result.

POST /api/sensors/:id/request-fft ingest

Flags a one-shot raw vibration capture, transmitted on the sensor’s next check-in. Body may set {"probe":"1"}. Returns {"success":true,"device_id":"…","probe":"1","status":"pending"}.

POST /api/sensors/:id/fft/:fftId/analyze ingest

Runs spectral analysis on a stored capture and returns the computed spectrum. No radio involved — it processes data already on the gateway — but it is CPU-intensive Python. On an EG5100 (single core, no swap) expect it to be slow and avoid running several at once. Returns 503 if the analysis subsystem is unavailable.

DigiMesh peer registry

Non-sensor radio hardware — relay controllers and similar — that the operator has authorized apps to command. Deliberately separate from the sensor inventory. Requires platform 2.5.0 or newer.

GET /api/digimesh-peers api

Registered peers as an array of {address, name, notes, enabled, created_at, updated_at}.

POST /api/digimesh-peers api

Registers a peer. address is required — a 64-bit DigiMesh MAC. Upper case and unseparated hex are accepted and normalized to lower-case colon form. Returns 201, or 409 if already registered, or 400 for a malformed address.

PUT /api/digimesh-peers/:address api

Updates name, notes, or enabled. 404 if unknown.

DELETE /api/digimesh-peers/:address api

Removes a peer, revoking app authority to command it.

16. Monitor Views and Widgets

The Monitor page is a per-user dashboard of tabs (views) containing widgets. These endpoints exist mainly so you can provision dashboards programmatically — handy when commissioning many gateways to a house standard.

GET /api/dashboard/views api

The caller’s views, ordered by position. A user with none gets a default “Dashboard” view created automatically, so this never returns an empty array.

POST /api/dashboard/views api

Creates a view. name is required; position defaults to the end of the tab strip.

PUT /api/dashboard/views/:id api

Renames or reorders a view via name and position.

DELETE /api/dashboard/views/:id api

Deletes a view and all its widgets. A user must always keep at least one view, so deleting the last returns 400.

GET /api/dashboard/widgets api

The caller’s widgets, with config parsed into an object. Pass view_id to scope to one tab.

POST /api/dashboard/widgets api

Creates a widget. widget_type is required and must be one of:

textValid widget_type values
line_graph   metric_card   bar_chart   gauge   area_chart
sparkline    stat_card     table       tank_level

width is clamped to 1–4 (default 2) and height to 1–6 (default 2). config must be an object. If view_id is omitted the widget lands in the caller’s default view; a view_id the caller does not own returns 403.

PUT /api/dashboard/widgets/:id api

Partial update. Setting view_id moves the widget to another of the caller’s views.

DELETE /api/dashboard/widgets/:id api

Deletes a widget.

17. Scheduled Reports and Saved Queries

Atrium can generate PDF reports on a schedule and email them. If your requirement is “a summary in someone’s inbox every Monday”, this is cheaper to set up than building it in your own system.

GET /api/scheduled-reports api

All configured reports, with config parsed and last_sent populated.

POST /api/scheduled-reports api

Creates a report. name, report_type, schedule, and recipient_email are required.

FieldAccepted values
report_typefleet_summary, sensor_group, alert_digest, oee_summary, custom_query, productivity_summary
scheduledaily, weekly, monthly
schedule_dayInteger day-of-week or day-of-month, for weekly and monthly schedules
schedule_hourHour of day, default 8
configFree-form object, stored as JSON — the report type determines what it needs

An invalid report_type or schedule returns 400 listing the accepted values. Returns 201 on success.

PUT /api/scheduled-reports/:id api

Partial update. Sending no updatable fields returns 400.

DELETE /api/scheduled-reports/:id api

Deletes a report.

POST /api/scheduled-reports/:id/send-now ingest

Generates and emails a report immediately, ignoring its schedule — useful for verifying a report before trusting it to a schedule. Returns {"success":true,"id":n}, or 404 if the report is unknown or disabled.

This runs PDF generation synchronously, so it can take a while on constrained hardware. Set a generous client timeout.

Saved queries

Named SQL snippets stored on the gateway, used by the SQL Query tool and by custom_query reports.

GET /api/saved-queries api

Returns {queries:[{id, name, sql_text, created_at}], count}.

POST /api/saved-queries api

Creates one. name and sql_text are both required. Returns 201 with the new id.

DELETE /api/saved-queries/:id api

Deletes one.

18. Apps and App Center

Atrium features such as AssetPulse machine health, OEE, and productivity monitoring ship as installable apps. Each app can add its own API endpoints, which is where you look if the data you want is not in the platform surface.

GET /api/apps api

Installed apps with version, enabled state, declared capabilities, model support, and license status. Also returns gateway_model and gateway_id at the top level.

jsonResponse 200 — one app
{
  "apps": [
    { "id": "machine-health", "name": "AssetPulse", "version": "1.4.2",
      "description": "Vibration-based machine health monitoring",
      "enabled": true, "model_supported": true, "available": true,
      "path": "/apps/machine-health", "ui": {},
      "capabilities": ["readings:read","events:publish"],
      "license": { "required": true, "state": "licensed", "reason": null,
                   "reason_text": null, "customer": "Example Mfg",
                   "expires_at": 1820000000000 } }
  ],
  "count": 1,
  "gateway_model": "EG5120",
  "gateway_id": "0013a200424165 28"
}

available is the field to check before calling an app’s endpoints: it is true only when the app is enabled, supported on this hardware, and licensed. A license.state of unlicensed means the app’s routes return 402.

GET /api/apps/:id/license api

License status for one app, including the gateway ID a license must be bound to. 404 if the app is not installed.

POST /api/apps/:id/license api

Activates a license. Body is {"license":"<token>"} (token is accepted as an alias). The license is verified against this gateway before being saved, so a license issued for another unit is rejected with 400 and a machine-readable reason. On success both services restart.

409 means the gateway ID is not yet known because the radio has not reported in — retry shortly.

DELETE /api/apps/:id/license api

Removes a stored license and restarts services. Intended for support and testing.

POST /api/apps/install api

Installs a signed app package uploaded as raw bytes — Content-Type: application/gzip or application/octet-stream, up to 200 MB. Optional query parameters expect_id pins the target app and allow_downgrade=1 permits installing an older version.

POST /api/apps/:id/uninstall api

Uninstalls an app and restarts services. Returns {"ok":true,"id":"…","purged_data":bool,"restarting":true}. Purging also deletes the app’s database.

POST /api/apps/:id/enable api

Enables an app. Revokes its existing scoped tokens and restarts services. 404 if not installed.

POST /api/apps/:id/disable api

Disables an app without uninstalling it. Same token revocation and restart.

POST /api/apps/:id/token api

Mints a scoped token for an app’s sandboxed UI. Returns {token, expires_at, capabilities}.

GET /api/apps/catalog api

The App Center catalog, annotated with what is installed and what this gateway can run. Pass refresh=1 to bypass the cache. If the catalog is unreachable you still get 200, with unavailable:true and an error string.

GET /api/apps/catalog/:id api

One catalog app in detail: all versions with their platform and model requirements, release dates, sizes, changelogs, an install_blocked_reason per version, and the README as sanitized HTML or raw markdown.

POST /api/apps/install-from-catalog api

Downloads and installs an app by ID. Body is {"id":"machine-health","allow_downgrade":false}. Returns 502 if the App Center is unreachable.

GET /app-ui/:appId/* api No auth

Static frontend assets for an installed app’s UI. Unauthenticated, exactly as nginx would serve any static bundle — the app’s data still requires a token. Not useful to an integration; documented for completeness.

19. Direct SQL Access

Atrium exposes its SQLite database through an HTTP endpoint. It is an administrative tool that powers the built-in SQL Query page — occasionally the right escape hatch, and easy to misuse.

GET /api/sqlite/tables api

Lists table names: {"tables":["readings","devices",…]}.

GET /api/sqlite/schema api

Column definitions for one table. table is a required query parameter and must be a valid identifier; anything else returns 400. Each column reports cid, name, type, notnull, dflt_value, and pk.

POST /api/sqlite/query api

Executes a statement. Body is {"query":"SELECT …"}. A SELECT returns rows; a non-returning statement executes and yields an empty rows array. A SQL error returns 400 with the engine’s message.

bashExample — daily averages, correctly bucketed
curl -s -X POST "$GATEWAY/api/sqlite/query" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"query":"SELECT (ts/86400000)*86400000 AS day, AVG(value) AS avg_c, MIN(value) AS min_c, MAX(value) AS max_c, COUNT(*) AS n FROM readings WHERE device_id = '\''00:13:a2:00:42:41:65:28'\'' AND metric = '\''temperature'\'' GROUP BY day ORDER BY day DESC LIMIT 30"}'
jsonResponse 200
{
  "success": true,
  "rows": [
    { "day": 1788872400000, "avg_c": 24.93, "min_c": 24.1, "max_c": 25.8, "n": 86 }
  ],
  "count": 1,
  "query": "SELECT (ts/86400000)*86400000 AS day, …"
}

Tables you are most likely to want

TableContents
readingsAll telemetry: ts, device_id, metric, value, counter, sensor_type, firmware, rssi, tags. Always filter by device_id, metric, and ts — the index is built for exactly that, and the table grows without bound.
devicesDevice registry including whitelist state and last-seen values.
sensor_metaOperator metadata — location, asset, name, install date.
alert_historyAlert events, the same rows served by /api/alerts/history.
status_historyDevice health transitions.
gateway_configConfiguration key/value pairs.

20. MQTT Push — the Streaming Alternative

The REST API is poll-only. If you want the gateway to push data to you as it arrives, configure MQTT instead. For continuous ingestion this is almost always the better architecture.

No polling lag
Readings arrive as the gateway decodes them, rather than up to one poll interval later.
🔒
Outbound only
The gateway connects out to your broker. No inbound firewall rule, no Remote Access tunnel needed.
📦
Nothing missed
Published at QoS 1, so a brief broker outage does not silently drop readings.
📈
Lighter on the gateway
One persistent connection instead of repeated authenticated HTTP queries against a growing table.

When to choose which

RequirementUse
Continuously stream every reading into a historian, data lake, or message busMQTT
React to alerts promptly — open a ticket when something tripsMQTT (alerts topic)
Backfill history, or query a past time windowREST (telemetry endpoint)
Read or write configuration, metadata, and alert rulesREST
Your platform cannot subscribe to MQTT (many SaaS ITSM tools cannot)REST polling
On-demand lookup — “what is this sensor reading right now?”REST

The two are not exclusive, and the most robust integrations use both: MQTT for the live stream, REST to backfill anything missed during an outage and to read metadata.

Configuring it in the interface

MQTT is set up under Settings → MQTT Client. It takes two steps that are easy to conflate: first point the gateway at your broker, then separately grant the Atrium publisher so sensor telemetry, alerts, and status changes are actually published.

Settings, MQTT Client panel showing the remote broker connection, the three default topic patterns, the Publishers list, and the Test Publish tool.
Settings → MQTT Client. Broker connection, the three topic patterns, the publisher grant list, and the Test Publish tool. Note the state captured here: a broker is configured, but the Atrium publisher is switched off while an installed app is switched on — so apps use the connection and no sensor data is published. That is exactly the condition described in the warning below.

Topics

Topic patterns are configurable. {gateway_id} is replaced with the gateway name and {device_id} with the sensor’s device ID:

FeedDefault patternQoSRetained
Telemetry{gateway_id}/telemetry/{device_id}1No
Alerts{gateway_id}/alerts/{device_id}1No
Status{gateway_id}/status/{device_id}1Yes

Status messages are retained, so a subscriber that connects late still learns each device’s current online/offline state without waiting for the next transition. Subscribe with a wildcard such as Gateway-D8FC/telemetry/# to receive every sensor.

Payloads

All three payloads are JSON, with epoch-millisecond timestamps, exactly as in the REST API.

jsonTelemetry payload
{
  "device_id": "00:13:a2:00:42:41:65:28",
  "timestamp": 1788874772898,
  "sensor_type": 1,
  "data": { "temperature": 25.2, "humidity": 41.8 },
  "battery": 99.64,
  "rssi": 40
}

Note that data is an object of all metrics from that one packet, whereas the REST telemetry endpoint returns one row per metric. The MQTT shape is usually the more convenient of the two for ingestion.

jsonAlert payload
{
  "device_id": "00:13:a2:00:42:41:65:28",
  "sensor_name": "Line 4 Feed Pump — Bearing",
  "alert_type": "threshold",
  "metric": "temperature",
  "value": 88.4,
  "threshold": 85,
  "condition": "above",
  "timestamp": 1788874772898
}

alert_type is threshold or offline; for an offline alert, metric is the literal string "offline".

jsonStatus payload (retained)
{
  "device_id": "00:13:a2:00:42:41:65:28",
  "old_status": "healthy",
  "new_status": "offline",
  "reason": "no report within 2x interval",
  "timestamp": 1788874772898
}

Security

The broker connection supports username/password authentication, TLS, broker-certificate verification with an uploaded CA, and mutual TLS with a client certificate. Private keys and passphrases are encrypted at rest on the gateway. Use the Test Publish tool after saving to confirm the connection before relying on it.

MQTT endpoints

Everything in the interface is also reachable over the API. All MQTT endpoints are on the ingest daemon, and all return 503 if the MQTT subsystem is unavailable.

GET /api/mqtt/status ingest

Whether Atrium publishing is on, the resolved topic paths, and the publisher grant list. Verified live:

jsonResponse 200
{
  "publishing_enabled": false,
  "topics": [
    "Article_Channel/Gateway-D8FC/telemetry/<device_id>",
    "Article_Channel/Gateway-D8FC/alerts/<device_id>",
    "Article_Channel/Gateway-D8FC/status/<device_id>"
  ],
  "publishers": [
    { "id": "atrium", "name": "Atrium (sensor telemetry, alerts & status)",
      "granted": false }
  ]
}

The resolved topics array is the quickest way to confirm exactly what to subscribe to — the placeholders are already substituted. The gateway above has a custom prefix (Article_Channel/) configured ahead of the default pattern, which is why its topics are longer than the defaults listed earlier — another reason to read the resolved list rather than assume the default.

GET /api/mqtt/config ingest

Broker settings. The password is returned masked, never in clear text, and certificate material is reported only as has_ca_cert, has_client_cert, has_client_key booleans.

POST /api/mqtt/config ingest

Saves broker settings and reconnects immediately. host is required and must be a valid hostname; port must be 1–65535. Topic patterns are validated. PEM material is checked before it is stored.

Submitting a blank password — or the mask returned by GET — preserves the stored password, so you can change topics without knowing the credential.

POST /api/mqtt/publishers ingest

Sets the publisher grant list — the switch that actually starts the sensor-data feed. Grant atrium to enable automatic publishing.

POST /api/mqtt/toggle ingest

Turns publishing on or off without discarding the broker configuration.

POST /api/mqtt/test-publish ingest

Publishes one message immediately, bypassing the grant list — the fastest way to prove host, port, credentials, and TLS are correct before enabling the real feed. A broker host must be saved first.

21. Worked Example: Feeding a CMMS

A concrete pattern for pulling Atrium data into a maintenance management system such as ServiceNow, Maximo, Fiix, or Limble. The specifics below are generic HTTP; adapt them to your platform’s integration framework.

Design decisions to make first

  1. 1
    Decide what actually triggers work
    Most CMMS integrations do not want a firehose of temperature readings — they want events. Poll /api/alerts/history and create work orders from alert events; pull telemetry only for the affected sensor, as evidence attached to the record. This keeps the volume low and the mapping obvious.
  2. 2
    Establish how the gateway is reachable
    A cloud-hosted CMMS cannot reach a LAN address. Either enable Remote Access (Section 2) for a public HTTPS hostname, or run a small on-premises connector that polls the gateway locally and forwards to your platform. The connector approach is generally preferable: it keeps the gateway off the public internet entirely.
  3. 3
    Map sensors to your assets
    Write your CMMS asset identifier into the sensor’s asset field with POST /api/sensors/:id/meta (Section 8). Every subsequent alert and reading then carries a device ID you can resolve to an asset in one lookup, with no mapping table to maintain on your side.
  4. 4
    Create a dedicated login
    Provision a viewer account for the integration rather than embedding admin credentials (Section 13). Store its password in your platform’s credential store and plan to rotate it.

The polling loop

A complete, idempotent event-ingestion loop. The high-water mark on id is what makes it safe to re-run.

pythonReference implementation — alert events to work orders
import requests

GATEWAY = "https://b00e.iolight.com"     # or http://192.168.1.50 on-premises
USER, PASSWORD = "integrations", "•••"

_token = None

def login():
    """Exchange credentials for a session token."""
    global _token
    r = requests.post(f"{GATEWAY}/api/auth/login",
                      json={"username": USER, "password": PASSWORD},
                      timeout=15)
    r.raise_for_status()
    _token = r.json()["token"]
    return _token

def call(path, **params):
    """GET with automatic re-auth on 401. Tokens expire after 7 days."""
    global _token
    if _token is None:
        login()
    for attempt in (1, 2):
        r = requests.get(f"{GATEWAY}{path}",
                         headers={"Authorization": f"Bearer {_token}"},
                         params=params, timeout=30)
        if r.status_code == 401 and attempt == 1:
            login()                       # token expired — get a new one and retry
            continue
        r.raise_for_status()
        return r.json()

def poll_alerts(last_seen_id):
    """Return new alert events oldest-first, plus the new high-water mark.

    The endpoint returns newest-first and has no 'since' parameter, so we
    walk the page and stop at the first id we have already processed.
    """
    page = call("/api/alerts/history", limit=100)
    fresh = [e for e in page["history"] if e["id"] > last_seen_id]
    if not fresh:
        return [], last_seen_id
    fresh.sort(key=lambda e: e["id"])     # process in chronological order
    return fresh, fresh[-1]["id"]

def sensor_context(device_id, metric):
    """Fetch the asset mapping and recent readings to attach as evidence."""
    sensor = call(f"/api/sensors/{device_id}")
    readings = call(f"/api/sensors/{device_id}/telemetry",
                    metric=metric, limit=20)
    return sensor, readings["readings"]

# ── main loop ────────────────────────────────────────────────────────────────
last_seen_id = load_high_water_mark()     # persist this in your own store

events, last_seen_id = poll_alerts(last_seen_id)

for ev in events:
    if ev["event_type"] != "triggered":
        resolve_work_order(ev)            # a 'cleared' event — close the ticket
        continue

    sensor, readings = sensor_context(ev["device_id"], ev["metric"])

    create_work_order(
        asset_id    = sensor["asset"],            # your CMMS asset ID
        location    = sensor["location"],
        summary     = f"{sensor['name']}: {ev['metric']} {ev['condition']} "
                      f"{ev['threshold_value']} (read {ev['value']})",
        occurred_at = ev["timestamp"],            # epoch ms, UTC
        evidence    = readings,
        source_ref  = f"atrium:{ev['id']}",       # idempotency key
    )

save_high_water_mark(last_seen_id)

Notes on the pattern

  • Use source_ref for idempotency. Alert event ids are stable and unique, so keying your work orders on them makes a duplicate poll harmless. This matters more than it looks: a network timeout after a successful fetch is the normal case, not the exception.
  • Handle cleared events. Atrium logs both edges. Ignoring cleared leaves tickets open after the condition resolves; using it lets you auto-close, or at least annotate.
  • Do not poll faster than the sensors report. Every 5 minutes is ample for alert events. Alerts are evaluated as readings arrive, so a shorter interval buys you nothing but load.
  • Persist the high-water mark outside the process. If it resets to zero, the loop replays the entire alert history — which the source_ref key will absorb, but noisily.
  • Watch the retention window. If data_retention_days is 30 and your integration is down for 40 days, that data is gone for good. Note that retention purges alert history on the same schedule as telemetry — missed alert events are not recoverable either. Do not treat the gateway as your archive of record.
  • Expect update outages. A firmware update restarts the services. Retry with backoff rather than treating a run of failures as a fault.

22. Complete Endpoint Index

All 138 platform endpoints on Atrium 2.5.3, grouped by purpose. Every path is served from the same base URL; the Service column tells you which internal service answers, which matters only for interpreting errors.

MethodPathServicePurposeDetails
Authentication
POST/api/auth/change-credentialsapiChange the authenticated account's username/password§
POST/api/auth/loginapiExchange credentials for a session token§
GET/api/auth/verifyapiValidate a token and report its account§
Sensors
GET/api/sensor-typesapiSensor types present, with device counts§
GET/api/sensorsapiList whitelisted sensors with health and metadata§
GET/api/sensors/:idapiOne sensor by device ID§
GET/api/sensors/:id/status-historyapiHealth transition history for one sensor§
Telemetry
GET/api/sensors/:id/fftapiStored raw vibration captures§
GET/api/sensors/:id/fft/:fftIdapiOne vibration capture§
GET/api/sensors/:id/metricsapiMetric names this sensor reports§
GET/api/sensors/:id/telemetryapiReadings, filterable by metric and time window§
Metadata
GET/api/sensors/:id/chart-orderapiPreferred chart ordering§
POST/api/sensors/:id/chart-orderapiSet chart ordering§
GET/api/sensors/:id/metaapiOperator metadata for one sensor§
POST/api/sensors/:id/metaapiReplace operator metadata (full replace)§
GET/api/sensors/:id/metric-conversionsapiUnit-conversion formulas§
POST/api/sensors/:id/metric-conversionsapiSet unit conversions in bulk§
GET/api/sensors/:id/metric-labelsapiCustom metric display labels§
POST/api/sensors/:id/metric-labelsapiSet metric labels in bulk§
GET/api/synthetic-variablesapiList computed/synthetic metrics§
POST/api/synthetic-variablesapiCreate a synthetic variable§
DELETE/api/synthetic-variables/:idapiDelete a synthetic variable and its readings§
GET/api/synthetic-variables/:idapiOne synthetic variable§
POST/api/synthetic-variables/testapiDry-run a formula without saving§
Devices
GET/api/devicesapiList devices by whitelist state (param required)§
POST/api/devices/bulk-deleteapiDelete devices and all their data (destructive)§
POST/api/devices/bulk-whitelistapiWhitelist or un-whitelist many devices§
POST/api/purge-all-dataapiDelete all telemetry and devices (destructive)§
POST/api/sensors/:id/whitelistapiWhitelist or un-whitelist one device§
Alerts
GET/api/alert-templatesapiPattern-matching alert templates§
POST/api/alert-templatesapiCreate an alert template§
DELETE/api/alert-templates/:idapiDelete an alert template§
PUT/api/alert-templates/:idapiUpdate an alert template§
GET/api/alert-templates/match-previewapiPreview which devices a match rule covers§
GET/api/alertsapiCurrent alert-rule state§
GET/api/alerts/historyapiAlert event log — poll this for CMMS events§
GET/api/sensors/:id/thresholdsapiThreshold rules for one sensor§
POST/api/sensors/:id/thresholdsapiWrite a threshold (see round-trip caveat)§
DELETE/api/thresholds/:threshold_idapiDelete an alert rule by ID§
Triggers
GET/api/automationsapiAutomations with nested triggers and actions§
POST/api/automationsapiCreate an automation§
DELETE/api/automations/:idapiDelete an automation§
PUT/api/automations/:idapiUpdate an automation (replaces sets)§
GET/api/triggersapiTriggers with live values and match state§
POST/api/triggersapiCreate a trigger condition§
DELETE/api/triggers/:idapiDelete a trigger§
PUT/api/triggers/:idapiUpdate a trigger§
Event bus
GET/api/bus/bindingsapiConfigured event to action bindings§
POST/api/bus/bindingsapiCreate a binding§
DELETE/api/bus/bindings/:idapiDelete a binding§
GET/api/bus/bindings/:idapiOne binding with state and recent runs§
PATCH/api/bus/bindings/:idapiUpdate a binding§
POST/api/bus/bindings/:id/runingestDispatch a binding now (real side effects)§
GET/api/bus/catalogapiWireable events and actions on this gateway§
GET/api/bus/eventsapiRecent event log§
POST/api/bus/publishapiPublish a diagnostic event into the atrium namespace§
GET/api/bus/runsapiBinding dispatch history§
GET/api/bus/stateapiRetained current value per topic and entity§
GET/api/bus/statusingestBroker counters and bound actions§
Gateway
GET/api/gateway/configapiFull configuration — call this first§
POST/api/gateway/configapiWrite configuration (unsent keys are reset)§
GET/api/gateway/proxyapiOutbound proxy settings (secrets masked)§
POST/api/gateway/proxyapiSave outbound proxy settings§
POST/api/gateway/proxy-testapiTest a proxy configuration§
POST/api/gateway/smtp-testingestSend a test email with supplied credentials§
POST/api/system/check-updatesapiCheck for a firmware update§
POST/api/system/execute-updateapiApply the pending firmware update§
POST/api/system/force-updateapiInstall a specific version§
GET/api/system/noderedapiNode-RED intent and process state§
POST/api/system/noderedapiEnable or disable Node-RED§
GET/api/system/sensor-libraryapiSensor decoder library version state§
POST/api/system/sensor-library/checkapiCheck for a newer decoder library§
POST/api/system/sensor-library/updateapiInstall a decoder library version§
GET/api/system/sensor-library/update-progressapiLibrary update progress§
GET/api/system/timeapiGateway clock, for skew detection§
GET/api/system/update-progressapiFirmware update progress§
GET/api/usersapiAdmin and viewer account state (admin only)§
DELETE/api/users/viewerapiDelete the viewer account (admin only)§
POST/api/users/viewerapiCreate or update the viewer account (admin only)§
Network
POST/api/network/cellularapiConfigure the cellular modem (EG5120 only)§
POST/api/network/confirmapiConfirm a pending change, cancelling rollback§
POST/api/network/ipv4apiSet interface IPv4 (arms watchdog)§
POST/api/network/lanapiSet LAN address and DHCP pool (arms watchdog)§
POST/api/network/ntpapiSet NTP servers and timezone§
GET/api/network/statusapiFull network state (read-only, safe)§
POST/api/network/wifiapiSwitch WiFi AP/client mode (arms watchdog)§
GET/api/network/wifi/scanapiScan for visible networks (slow, up to 30s)§
GET/api/network/wifi/statusapiWiFi mode, SSID, link and rollback reason§
Radio
GET/api/digimesh-peersapiRegistered non-sensor radio peers§
POST/api/digimesh-peersapiRegister a radio peer§
DELETE/api/digimesh-peers/:addressapiRemove a radio peer§
PUT/api/digimesh-peers/:addressapiUpdate a radio peer§
GET/api/mesh-topologyapiWhole mesh as nodes, edges and routes§
POST/api/sensors/:id/fft/:fftId/analyzeingestCompute a spectrum from a stored capture§
GET/api/sensors/:id/mesh-mapapiStored route for one sensor§
POST/api/sensors/:id/mesh-mapingestQueue a route-discovery request§
POST/api/sensors/:id/request-fftingestQueue a raw vibration capture§
GET/api/sensors/:id/sensor-configingestReported and desired sensor configuration§
POST/api/sensors/:id/sensor-configingestQueue a sensor configuration change§
GET/api/sensors/config-options/:typeingestConfiguration schema for a sensor type§
Monitor
GET/api/dashboard/viewsapiCaller's dashboard views§
POST/api/dashboard/viewsapiCreate a view§
DELETE/api/dashboard/views/:idapiDelete a view and its widgets§
PUT/api/dashboard/views/:idapiRename or reorder a view§
GET/api/dashboard/widgetsapiCaller's widgets, optionally by view§
POST/api/dashboard/widgetsapiCreate a widget§
DELETE/api/dashboard/widgets/:idapiDelete a widget§
PUT/api/dashboard/widgets/:idapiUpdate or move a widget§
Reports
GET/api/saved-queriesapiSaved SQL snippets§
POST/api/saved-queriesapiCreate a saved query§
DELETE/api/saved-queries/:idapiDelete a saved query§
GET/api/scheduled-reportsapiConfigured scheduled reports§
POST/api/scheduled-reportsapiCreate a scheduled report§
DELETE/api/scheduled-reports/:idapiDelete a scheduled report§
PUT/api/scheduled-reports/:idapiUpdate a scheduled report§
POST/api/scheduled-reports/:id/send-nowingestGenerate and email a report immediately§
Apps
GET/api/appsapiInstalled apps with license and model state§
POST/api/apps/:id/disableapiDisable an app§
POST/api/apps/:id/enableapiEnable an app§
DELETE/api/apps/:id/licenseapiRemove a stored license§
GET/api/apps/:id/licenseapiLicense status for one app§
POST/api/apps/:id/licenseapiActivate a license§
POST/api/apps/:id/tokenapiMint an app-scoped token (not an API key)§
POST/api/apps/:id/uninstallapiUninstall an app§
GET/api/apps/catalogapiApp Center catalog§
GET/api/apps/catalog/:idapiOne catalog app, all versions and README§
POST/api/apps/installapiInstall an uploaded app package§
POST/api/apps/install-from-catalogapiInstall an app from the catalog§
GET/app-ui/:appId/*apiApp UI static assets (no auth)§
Direct SQL
POST/api/sqlite/queryapiExecute arbitrary SQL (use with care)§
GET/api/sqlite/schemaapiColumn definitions for one table§
GET/api/sqlite/tablesapiList database tables§
MQTT
GET/api/mqtt/configingestBroker settings (password masked)§
POST/api/mqtt/configingestSave broker settings and reconnect§
POST/api/mqtt/publishersingestSet the publisher grant list§
GET/api/mqtt/statusingestPublishing state and resolved topics§
POST/api/mqtt/test-publishingestPublish one test message, bypassing grants§
POST/api/mqtt/toggleingestTurn publishing on or off§

23. Limits, Gotchas and Known Issues

Collected here so you can scan them before writing code rather than discovering them in production. Each of these has caught a real integrator.

Design constraints

ConstraintWhat it means for you
Tokens expire after 7 days Not extended by use. Handle 401 by re-authenticating and retrying. There is no refresh-token flow and no non-expiring API key.
No webhooks or websockets The gateway never calls you over HTTP. Poll, or use MQTT for push. Alert actions are limited to email and relay_command — there is no HTTP action type.
Telemetry is deleted on a retention schedule data_retention_days (default 100, often lowered) governs a nightly purge at 03:00 gateway time that deletes readings, raw ingest rows, and alert history older than the window. Nothing older survives. The gateway is not an archive — poll often enough to stay inside the window.
All times are UTC epoch milliseconds Never seconds. The display timezone and °C/°F preference affect the UI only.
Readings are per metric, not per packet A sensor reporting temperature and humidity produces two rows per report. MQTT telemetry payloads, by contrast, group a packet’s metrics into one data object.
Values are native units Conversions and labels are display metadata. Fetch them separately and apply them yourself if your figures must match the customer’s screen.
Hardware is modest The EG5120 has 4 cores and ~2 GB RAM; the EG5100 has one core, ~1 GB, and no swap. Wide queries and tight polling loops are felt. Prefer narrow, indexed requests.
Result caps /api/sensors: 5000 max. Telemetry raw: 10,000 max. Telemetry aggregated: 5000 buckets. Bus events and runs: 500 max. FFT list: 200 max.
Firmware gates endpoints /api/bus/* needs 2.4.0+; /api/network/* and /api/digimesh-peers need 2.5.0+. Older firmware returns 404. Check platform_version first.

Known issues on platform 2.5.3

Easy mistakes

  • Forgetting ?whitelist= on /api/devices — the parameter is mandatory and its absence is a 400, not a default.
  • Treating POST /api/sensors/:id/meta as a merge — it is a full replace. Read, modify, write back.
  • Treating POST /api/gateway/config as a merge — same trap, with more collateral damage: a partial POST resets SMTP, retention, timezone, and temperature unit.
  • Assuming raw and bucketed telemetry share an order — raw comes back newest-first, bucketed oldest-first.
  • Parsing tags as an object — it is a JSON-encoded string, so it needs a second parse.
  • Reading rssi as a conventional negative dBm — Atrium reports a positive magnitude where lower is better. 40 is strong; 90 is marginal.
  • Expecting a trigger to alert on its own — it needs an automation referencing it before anything happens.
  • Assuming an error status means a broken sensor — it also covers low battery and weak signal. Inspect the underlying fields.
  • Paging alert history with offset alone — rows shift as events arrive. Track the highest id instead.
  • Expecting a radio write to take effect immediately — sensors sleep. "status":"pending" means queued, not applied.
  • Creating dashboards with the integration’s account — views and widgets are per-user, so the customer will not see them.
  • Renaming the gateway after wiring MQTT — it rewrites every topic path and silently orphans subscribers.

Security checklist

  1. 1
    Change the default credentials
    Before anything else, and certainly before enabling Remote Access.
  2. 2
    Give the integration its own account
    Do not embed admin credentials in an external platform. Remember that the viewer role is a usability boundary, not a hardened permission model — the account still needs a strong, unique, rotated password.
  3. 3
    Prefer an on-premises connector to public exposure
    A small local service that polls the gateway and forwards to your platform keeps the gateway off the public internet entirely. Reach for Remote Access only when that is not possible.
  4. 4
    Use TLS on MQTT
    Enable TLS with certificate verification, and mutual TLS if your broker supports it. Verify with Test Publish before trusting the feed.
  5. 5
    Keep the SQL endpoint out of production paths
    It executes arbitrary statements, including writes, and bypasses every validation rule. Use the purpose-built endpoints.