PiMonitor exposes a small, versioned REST API under /api/v1/... intended
both for its own web dashboard and for third-party consumers (e.g. home
automation systems such as openHAB, Home Assistant, or Node-RED).
Breaking changes to an existing version's response shape will not happen
in place — a new /api/v2/... path would be introduced instead, so
existing integrations against /api/v1/... keep working.
By default, no authentication is required — PiMonitor is meant to run on a
trusted local network. If you set api_key in config.yaml (or the
-api-key flag), every /api/v1/... request must include one of:
Authorization: Bearer <api_key>X-Api-Key: <api_key>
Requests without a valid key receive 401 Unauthorized. GET /healthz is
never gated by the API key, so external health checks keep working
regardless of authentication configuration.
The bundled web dashboard uses this same API: when an api_key is set it
shows an "API key required" prompt on first load, then stores the entered
key in the browser's localStorage and sends it as X-Api-Key on every
request. Setting a key therefore does not disable the dashboard — each
browser just has to unlock it once.
Plain-text liveness check, always 200 ok. Not versioned; intended for
systemd/monitoring tooling, not for metric data.
Returns the most recently collected snapshot of every metric. This is the main endpoint for third-party integrations: poll it on an interval and extract the fields you need (e.g. via JSONPath in openHAB's HTTP binding).
{
"timestamp": "2026-07-12T18:32:00Z",
"uptime_seconds": 372014.5,
"cpu": {
"overall_percent": 12.4,
"per_core_percent": [10.1, 14.8, 11.2, 13.5]
},
"cpu_frequency": [
{ "core": 0, "mhz": 600, "governor": "ondemand" },
{ "core": 1, "mhz": 1500, "governor": "ondemand" },
{ "core": 2, "mhz": 600, "governor": "ondemand" },
{ "core": 3, "mhz": 1500, "governor": "ondemand" }
],
"load_average": { "load1": 0.42, "load5": 0.38, "load15": 0.31 },
"cpu_count": 4,
"temperature": { "zone": "cpu-thermal", "celsius": 48.6 },
"gpu_temperature": { "celsius": 47.8 },
"throttled": {
"under_voltage_now": false,
"frequency_capped_now": false,
"throttled_now": false,
"soft_temp_limit_now": false,
"under_voltage_since_boot": true,
"frequency_capped_since_boot": false,
"throttled_since_boot": true,
"soft_temp_limit_since_boot": false,
"raw": "0x50000"
},
"memory": {
"total_bytes": 4137000000, "available_bytes": 2900000000, "used_percent": 29.9
},
"swap": { "total_bytes": 104857600, "used_bytes": 0, "used_percent": 0 },
"disks": [
{
"mountpoint": "/",
"device": "/dev/root",
"fstype": "ext4",
"total_bytes": 31000000000,
"used_bytes": 8000000000,
"used_percent": 25.8
}
],
"network": [
{ "name": "eth0", "rx_bytes_per_sec": 1240.5, "tx_bytes_per_sec": 302.1 }
],
"system": {
"kernel_version": "6.6.31+rpt-rpi-v8",
"distribution": "Raspberry Pi OS Bookworm (Debian 12)",
"pi_model": "Raspberry Pi 4 Model B Rev 1.4",
"cpu_model": "ARMv8 Processor rev 1 (v8l)"
},
"updates": {
"count": 3,
"packages": [
{
"name": "curl",
"new_version": "7.88.1-10+deb12u5",
"old_version": "7.88.1-10+deb12u4",
"arch": "arm64"
}
],
"cache_age_seconds": 1800,
"stale": false,
"checked_at": "2026-07-12T18:20:00Z"
}
}Notes:
timestampis the Pi's own clock at collection time (useful as the displayed device time), anduptime_secondsis the time elapsed since boot.system.cpu_modelis best-effort: it is empty on kernels whose/proc/cpuinfoomits amodel namefield (common on some Raspberry Pi kernels).disks[].used_percentfollowsdf's semantics: it is computed asused / (used + available), whereavailablecounts only blocks writable by unprivileged processes. Blocks reserved for root (typically 5% on ext4) therefore count as used capacity, and the value reaches 100 when services can no longer write — matchingdf'sUse%rather than a rawused / totalratio (which would still read ~95% on a full ext4 filesystem).total_bytesandused_bytesremain the raw filesystem totals, soused_percentcan slightly exceedused_bytes / total_bytes * 100.diskscontains at most one entry per mountpoint (the filesystem actually visible at that path when a mountpoint is overmounted), and network filesystems (NFS, CIFS/SMB, SSHFS, ...) are excluded — only local storage is reported.networkentries are sorted by interface name.cpu_frequencyis one entry per CPU core with a readable sysfscpufreqdirectory (scaling_cur_freq,scaling_governor), sorted bycoreindex. It is omitted entirely on systems without a cpufreq driver (e.g. many development machines, or a kernel built withoutCONFIG_CPU_FREQ), and a core that is offline or whose driver doesn't expose both files is simply left out rather than failing the whole reading.gpu_temperatureis only present ifvcgencmdis installed and responded successfully; otherwise the field is omitted.throttleddecodes the Raspberry Pivcgencmd get_throttledbitmask. The*_nowflags reflect the current state; the*_since_bootflags latch whether the condition has occurred at any point since boot. A setunder_voltage_*flag usually means an inadequate power supply or cable. Likegpu_temperature, the whole object is omitted whenvcgencmdis unavailable (e.g. off-Pi), andrawcarries the original hex bitmask.networkis omitted entirely when network monitoring is disabled (network_enabled: false).updates.staleistruewhen the underlying apt cache (refreshed by a separate root-privileged systemd timer, not by this process) is older than the configured staleness threshold — treat the update count as possibly outdated when this is set.- Fields may read as zero values (
0,"", empty arrays) briefly after process start, before the first collection tick completes, or permanently on non-Pi/non-Linux systems for hardware-specific fields liketemperatureorpi_model.
Returns the retained history (a rolling window, typically the last 30-60
minutes) for every time-series metric. When history persistence is enabled
(history_persist_enabled, on by default), history is periodically
snapshotted to disk and restored on startup, so the returned window may
span service restarts and reboots; points older than the configured window
are dropped on restore. With persistence disabled, history is in-memory
only and starts empty after every restart.
{
"cpu_percent": [{ "t": "2026-07-12T18:00:00Z", "v": 10.2 }],
"load1": [{ "t": "2026-07-12T18:00:00Z", "v": 0.4 }],
"load5": [{ "t": "2026-07-12T18:00:00Z", "v": 0.38 }],
"load15": [{ "t": "2026-07-12T18:00:00Z", "v": 0.31 }],
"temperature": [{ "t": "2026-07-12T18:00:00Z", "v": 48.1 }],
"memory_used_percent": [{ "t": "2026-07-12T18:00:00Z", "v": 29.9 }],
"swap_used_percent": [{ "t": "2026-07-12T18:00:00Z", "v": 0 }],
"disk_used_percent": {
"/": [{ "t": "2026-07-12T18:00:00Z", "v": 25.8 }]
},
"network_rx_bytes_per_sec": {
"eth0": [{ "t": "2026-07-12T18:00:00Z", "v": 1240.5 }]
},
"network_tx_bytes_per_sec": {
"eth0": [{ "t": "2026-07-12T18:00:00Z", "v": 302.1 }]
}
}disk_used_percent, network_rx_bytes_per_sec, and
network_tx_bytes_per_sec are keyed by mountpoint/interface name and are
omitted entirely if empty (e.g. network history when monitoring is
disabled).
Returns the server-side alert engine's current per-metric state plus a
rolling list of recent transition events. The engine maps each collected
snapshot against the configured thresholds into ok/warn/crit states,
applying a debounce (alerts.for_seconds) so a threshold crossing must
persist before it is reported — this suppresses short-lived spikes and
momentary dips. The states mirror the color-coding the dashboard already
shows; the events make sustained crossings actionable (e.g. an openHAB rule
polling this endpoint).
{
"enabled": true,
"states": [
{ "metric": "cpu", "level": "ok", "value": 12.4, "since": "2026-07-12T18:00:00Z" },
{ "metric": "disk", "resource": "/", "level": "warn", "value": 82.1, "since": "2026-07-12T18:25:00Z" },
{ "metric": "memory", "level": "ok", "value": 45.2, "since": "2026-07-12T18:00:00Z" },
{ "metric": "swap", "level": "ok", "value": 0, "since": "2026-07-12T18:00:00Z" },
{ "metric": "temperature", "level": "crit", "value": 78.5, "since": "2026-07-12T18:30:10Z" }
],
"events": [
{
"metric": "disk",
"resource": "/",
"kind": "fired",
"from": "ok",
"to": "warn",
"value": 82.1,
"at": "2026-07-12T18:25:00Z"
},
{
"metric": "temperature",
"kind": "fired",
"from": "warn",
"to": "crit",
"value": 78.5,
"at": "2026-07-12T18:30:10Z"
}
]
}Notes:
enabledisfalse(with emptystates/events) when alerting is disabled viaalerts.enabled: false.stateslists one entry per evaluated metric:cpu,temperature,memory,swap, and onediskentry per mounted filesystem (distinguished byresource, the mountpoint).resourceis omitted for non-per-device metrics.- A metric whose collection fails on a given tick is skipped rather than
evaluated against a bogus zero, so its state is left unchanged (or absent
if it has never been collected). In particular, on hardware without a
readable thermal zone (containers, non-Pi dev machines) there is no
temperatureentry at all — do not assume every metric is always present. - A per-filesystem
diskstate is dropped when its mountpoint disappears from the sample (e.g. an unplugged USB drive). If that filesystem was still alerting, a final syntheticclearedevent is emitted for it; that event'svalueis the last reading before the mount vanished (which may still be>=a threshold), so acleared/to: "ok"event carrying a highvalueon an unmount is expected, not a bug. levelis the debounced state actually reported;valueis the most recent reading andsinceis when the current level was entered.- Each
eventsentry is a confirmed transition:kindisfiredwhen the severity increased (e.g.ok→warn,warn→crit) andclearedwhen it decreased (e.g.crit→ok).from/tocarry the levels andatis the transition time. The list is bounded to the most recent transitions and is in-memory only (it starts empty after a restart). - The value cutoffs match the dashboard's card coloring: a level is
critwhenvalue >= *_crit,warnwhenvalue >= *_warn, otherwiseok. - The same
fired/clearedtransitions can also be pushed to external HTTP webhooks (Slack, Discord, Home Assistant, ntfy, ...). This is delivery-only and configured underalerts.webhooksin the config file — it adds no new API endpoint; seepackaging/pimonitor.example.yaml.
Returns non-sensitive runtime configuration, so the web dashboard (or a third-party client) doesn't need to hardcode values separately from the server:
{
"version": "1.2.3",
"poll_interval_seconds": 5,
"network_enabled": true,
"thresholds": {
"temperature_warn_c": 60,
"temperature_crit_c": 75,
"cpu_warn_percent": 80,
"cpu_crit_percent": 95,
"disk_warn_percent": 80,
"disk_crit_percent": 95,
"swap_warn_percent": 50,
"swap_crit_percent": 90,
"memory_warn_percent": 80,
"memory_crit_percent": 95
}
}Notes:
versionis the build-time version of the running binary, injected via-ldflags "-X main.version=...". Release builds report the release tag; a local build made without version injection reportsdev. The value may include a leadingvdepending on the build path (e.g. agit describestring likev1.2.3-5-gabc123); the dashboard strips that leadingvwhen it renders the version in its footer.
curl -s http://raspberrypi.local:8080/api/v1/metrics | jq '.cpu.overall_percent'With an API key configured:
curl -s -H "X-Api-Key: $PIMONITOR_API_KEY" \
http://raspberrypi.local:8080/api/v1/metrics | jq '.temperature.celsius'A Thing definition polling the temperature every 30 seconds:
Thing http:url:pimonitor "PiMonitor" [
baseURL="http://raspberrypi.local:8080/api/v1/metrics",
refresh=30
] {
Channels:
Type number : temperature "CPU Temperature" [
stateExtension="temperature/celsius",
stateTransformation="JSONPATH:$.temperature.celsius"
]
}