Reading device health, security, and inventory
Panel already collects a health, antivirus, and hardware picture for every device it manages. The API can hand that picture to your own dashboard, PSA, or reporting tool, so you are not re-collecting data you already have.
These are read-only calls. They add nothing to a device and change nothing on it.
Asking for the extra detail
By default a device comes back with its normal details — name, serial, site, who has it. The monitoring data is opt-in: add include and name the blocks you want.
GET /api/public/v1/assets/4821?include=health,security
{
"id": 4821,
"name": "LT-0412",
"site": { "id": 38, "name": "High School" },
"last_seen": "2026-07-24T14:02:11Z",
"health": {
"state": "warning",
"issue_count": 2,
"policy": { "id": 7, "name": "Staff Laptops" },
"cpu_avg_pct": 71.4
},
"security": {
"uncontained_threat_count": 0,
"realtime_product": "Microsoft Defender",
"definitions_status": "up_to_date",
"running_mode": "Normal",
"capability": "defender_active",
"monitoring_mismatch": false,
"inventory_captured_at": "2026-07-24T13:58:00Z"
}
}
There are three blocks — health, security, and inventory — and you can combine them. include works the same way on a single device, on the device list, and on the list of devices checked out to a person.
Leaving include off gives you exactly the response you got before this feature existed, so nothing you have already built changes.
Permissions
Each block has its own permission, so you can issue a key that reads hardware inventory without also exposing antivirus threats:
| Block | Permission needed |
|---|---|
health |
health:read |
security |
security:read |
inventory |
inventory:read |
Asking for a block your key does not carry returns a 403 insufficient_scope rather than silently omitting it, so a missing permission is never mistaken for a device with no data.
The same permission is required to filter or group by that data — see below.
What is in each block
health — Panel's PC Health rollup for the device.
| Field | Meaning |
|---|---|
state |
unmonitored, healthy, warning, critical, or unknown |
issue_count |
How many health issues are currently open |
policy |
The health policy that applies to this device |
cpu_avg_pct |
Average processor use since the last sample |
unmonitored does not mean healthy. It means no health policy covers this device, so nothing is being checked. A dashboard that colours it green is reporting a fleet as healthier than it is.
security — antivirus and Defender posture: how many uncontained threats are open, which real-time product is running, whether definitions are current, and whether monitoring is expected but Defender has gone passive (monitoring_mismatch).
Branch your logic on capability (defender_active, defender_passive, defender_absent, unknown). running_mode is passed through exactly as Defender reports it — useful to display, but the wording can change, so do not write code that tests it for equality.
inventory — hardware and patch state, in two halves:
"inventory": {
"hardware": { "ram_mb": 16384, "os_name": "Windows 11 Pro", "battery_health": 87, ... },
"windows_update": { "pending_count": 3, "pending_critical_count": 1, "reboot_pending": true, ... }
}
"No data" is null, not blank
Any monitoring field that has never been reported comes back as null. A Chromebook has no Windows Update stack, and a Windows device that has not yet run the collector has not reported one either — both give you null, never 0 and never "".
This matters for compliance reporting: null means unknown, which is not the same as clean. Panel keeps them apart, and so should anything counting them.
Filtering the device list
The same data can narrow a list. These work on GET /api/public/v1/assets:
| Filter | Example |
|---|---|
health_state |
health_state=critical,warning |
definitions_status |
definitions_status=out_of_date |
has_threats |
has_threats=true |
os_name, os_version |
os_name=Windows 10 Pro |
reboot_pending |
reboot_pending=true |
has_pending_critical |
has_pending_critical=true |
last_seen_before / _after |
last_seen_before=2026-07-01 |
last_polled_before / _after |
last_polled_before=2026-07-01 |
never_polled |
never_polled=true |
Filtering by a field counts as reading it, so health_state=critical needs health:read just as the health block does.
Dates accept either a plain date (2026-07-01, meaning midnight UTC) or a full timestamp. A date the API cannot read is rejected rather than ignored — a filter that quietly vanished would report your whole fleet as overdue.
"Last seen" and "last polled" are different questions
This distinction matters more than any other on this page.
| Term | Means | Covers |
|---|---|---|
last_seen |
Last agent check-in, or failing that the last time the record was updated | Every device, including Chromebooks kept current by sync |
last_polled |
Last agent check-in, full stop | Only devices running the agent |
Use last_seen for "is this device still around?" — it covers your whole fleet.
Use last_polled for "is the agent healthy?" — it is stricter, and it never reports a Chromebook, because a Chromebook has no agent to poll. Ask for last_polled_before= and devices that have never polled are left out entirely; ask never_polled=true to find them deliberately.
Reaching for last_polled when you meant last_seen is the easiest mistake to make here: it silently drops the largest group of devices in most districts.
Counting and grouping
Every filter above is also a dimension you can group by, using the same counting endpoint described in Counting devices for a dashboard:
GET /api/public/v1/assets/count?group_by=site,health_state
Fleet health per building, in one call. Other useful combinations:
group_by=health_state fleet health at a glance
group_by=site,has_pending_critical patch compliance per building
group_by=os_name what you would be replacing in a refresh
group_by=last_seen_bucket how much of the fleet has gone quiet
Monitoring dimensions report a value and a label instead of the id and name you get from sites and classes, because a health state is not a record you can look up:
{ "health_state": { "value": "critical", "label": "Critical" }, "count": 4 }
Filter on value; show label to people.
Grouped monitoring counts always come back complete. Every possible value is returned, including the ones at zero, so a tile can display "0 Critical" as a healthy result rather than failing on a missing entry. os_name is the exception — there is no fixed list of operating systems, so you only get the ones actually present.
Activity buckets
last_seen_bucket and last_polled_bucket group devices by how long it has been:
GET /api/public/v1/assets/count?group_by=site,last_seen_bucket&buckets=1,7,30
{ "site": { "id": 38, "name": "High School" },
"last_seen_bucket": { "value": "7d_30d", "label": "7 to 30 days" },
"count": 12 }
buckets takes ascending day thresholds and defaults to 1,7,30, giving lt_1d, 1d_7d, 7d_30d, and gt_30d.
last_polled_bucket adds one more: never, for devices the agent has never polled. On last_seen_bucket there is no never — every device has been seen at least once.
Looking at the underlying records
Everything above is a summary. The individual records behind those numbers have their own endpoints, each filtered the same way the device list is:
GET /api/public/v1/health-issues?severity=critical&site=38
GET /api/public/v1/detections?status=uncontained&since=2026-07-01
GET /api/public/v1/software?name=Zoom
GET /api/public/v1/service-cases?open=true
All four are org-wide and take an asset= filter, so "every critical issue in the district" and "everything wrong with this one device" are the same call with a different filter.
Two views are per-device, because "all of it for one device" is the whole question:
GET /api/public/v1/assets/{id}/health current reading for every check
GET /api/public/v1/assets/{id}/software everything installed
Health issues
/health-issues returns alerting issues only by default. Panel's health checks pass through intermediate states while a problem is being confirmed, and those are deliberately not shown in the UI either — surfacing them would have you reporting problems that are still being debounced. Add state=any if you genuinely want them.
Health check readings (/assets/{id}/health) carry a stale flag. A stale reading is real data that is no longer being refreshed, because the check no longer applies to that device. It is kept rather than deleted, so treat stale: true as "last known", never as current.
Detections and gap markers
/detections hides gap markers by default. A gap marker is not a threat — it records that catch-up was truncated, meaning detections before that point may be missing entirely. Pass include_gap_markers=true when you are auditing coverage rather than triaging, and check the is_gap_marker flag on the row.
Clearing noise
Two triage actions are available, and both are reversible:
POST /api/public/v1/detections/{id}/acknowledge { "reason": "false_positive", "note": "..." }
DELETE /api/public/v1/detections/{id}/acknowledge
POST /api/public/v1/health-issues/{id}/mute { "until": "2026-08-01T00:00:00Z" }
DELETE /api/public/v1/health-issues/{id}/mute
These need security:write and health:write respectively.
Acknowledging a detection removes it from the active threat count without claiming it was remediated — the detection's status is untouched. Muting a health issue suppresses it from the device's health state. Neither changes anything on the device itself.
Omit until when muting and the issue is silenced indefinitely; muted_until then comes back as null with muted: true.
Actions taken with a key are attributed to it: Panel records and displays APIKEY_<your key name> wherever it would show a person's name, so an API acknowledgement is never anonymous.
Software inventory
/software is a catalog, not a row per installation — a district can hold close to a million installation rows, and paging them to answer one question is not useful. Each row is a distinct title with its install count:
{ "app_name": "Zoom", "publisher": "Zoom Video Communications",
"install_count": 412, "version_count": 7 }
name is a partial, case-insensitive search, so ?name=zoo finds Zoom. To get the actual devices, add expand=assets — this requires name, because expanding the whole catalog is the row dump the endpoint exists to avoid.
Service cases
/service-cases is read-only and defaults to open cases. The detail view adds entries[], the note thread. Creating and updating cases is not available yet.
Staff members (owner, reporter, note author) appear as a name only — Panel does not expose staff email addresses through the API.