Repository navigation
API Reference
Every route the service exposes. All application routes are mounted under
/api/v1; / is the only unversioned path. A live OpenAPI document is served
at /docs and /openapi.json.
Response examples on this page are hand-written illustrations of the shape each endpoint returns, not captures from a running fleet.
Send one of X-API-Key, X-Client-Passphrase (or its alias
X-API-PASSPHRASE), X-Internal-Secret, or Authorization: Bearer <jwt>.
The required scope is derived from the request, not declared per route:
| Request | Scope required |
|---|---|
Any path containing /admin
|
admin |
GET / HEAD / OPTIONS
|
read |
POST |
ingest |
PUT / PATCH / DELETE
|
admin |
Legacy credentials (passphrase, internal secret, trusted Easy Auth principal)
carry all three scopes. API keys and OIDC tokens carry only what they were
granted. GET /api/v1/auth/config tells a client which of these a deployment
accepts, without a credential. See Security.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | / |
none | Service banner: name, version, endpoint hints |
| GET | /api/v1/health |
none | Status plus a database round-trip |
| GET | /api/v1/health/live |
none | Liveness; checks nothing |
| GET | /api/v1/health/ready |
none | Readiness; 503 when the database is unreachable |
| GET | /api/v1/auth/config |
none | Accepted credentials and the OIDC issuer and audience |
| GET | /api/v1/negotiate |
read |
Web PubSub client access token |
| GET | /api/v1/metrics |
read |
Prometheus exposition (hidden from OpenAPI) |
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/devices |
read |
Device list with lightweight inventory/OS/network summaries |
| GET | /api/v1/device/{serial_number} |
read |
One device with every module attached |
| GET | /api/v1/device/{serial_number}/info |
read |
Fast subset: inventory, system, hardware, management, security, network |
| GET | /api/v1/device/{serial_number}/modules/{module_name} |
read |
One module's raw JSONB |
| GET | /api/v1/device/{serial_number}/events |
read |
That device's events |
| GET | /api/v1/device/{serial_number}/installs/log |
read |
The installs module's full run log |
| GET | /api/v1/device/{serial_number}/logs/{tool} |
read |
One management-tool log root, tails included |
| GET | /api/v1/device/{serial_number}/applications/usage/history |
read |
Per-device daily usage time series |
| PATCH | /api/v1/device/{serial_number}/archive |
admin |
Soft delete |
| PATCH | /api/v1/device/{serial_number}/unarchive |
admin |
Restore |
| DELETE | /api/v1/device/{serial_number} |
admin |
Permanent delete; requires ?confirm=true
|
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/hardware |
read |
Hardware per device, with inventory and network context |
| GET | /api/v1/system |
read |
System module per device |
| GET | /api/v1/network |
read |
Network module per device |
| GET | /api/v1/security |
read |
Security module per device |
| GET | /api/v1/security/certificates |
read |
Certificate search across the fleet |
| GET | /api/v1/management |
read |
Management/MDM module per device |
| GET | /api/v1/management/logs/{tool} |
read |
Fleet-wide log-line sweep for one tool |
| GET | /api/v1/inventory |
read |
Inventory module per device |
| GET | /api/v1/peripherals |
read |
Peripherals module per device |
| GET | /api/v1/identity |
read |
Identity module per device |
| GET | /api/v1/profiles |
read |
Configuration profiles per device |
| GET | /api/v1/installs |
read |
Flattened managed-install records |
| GET | /api/v1/installs/full |
read |
Nested per-device install records |
| GET | /api/v1/installs/filters |
read |
Filter vocabulary plus stale-agent detection |
| GET | /api/v1/applications |
read |
Flattened installed applications, filterable |
| GET | /api/v1/applications/filters |
read |
Filter vocabulary for the applications view |
| GET | /api/v1/applications/distribution |
read |
Version distribution for named applications |
| GET | /api/v1/applications/usage |
read |
Fleet utilization rollup |
| GET | /api/v1/applications/usage/by-device |
read |
Utilization for one application, per device |
| GET | /api/v1/applications/collection-health |
read |
How fresh each device's application data is |
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/v1/events |
ingest |
Agent check-in. The whole ingest surface |
| GET | /api/v1/events |
read |
Fleet event list |
| GET | /api/v1/events/failures |
read |
Rejected and repaired check-ins |
| GET | /api/v1/events/{event_id}/payload |
read |
One event's details plus the module data around it |
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/dashboard |
read |
Devices, install stats, and recent events in one call |
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/inventory-state |
read |
Every device's asset-inventory state, keyed by serial |
| PUT | /api/v1/inventory-state |
admin |
Bulk write of inventory states from an asset inventory system |
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/settings |
read |
Org settings document |
| PUT | /api/v1/settings |
internal secret | Replace the org settings document |
| GET | /api/v1/settings/inventory/discover |
internal secret | Inventory keys present across the fleet, with samples |
PUT /api/v1/settings and the discovery route additionally require the
authenticated method to be internal_secret (or auth_disabled in
development). A fleet passphrase or managed identity is refused with 403 even
though it holds the admin scope.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/v1/admin/api-keys |
admin |
Mint a scoped API key |
| GET | /api/v1/admin/api-keys |
admin |
List key metadata |
| DELETE | /api/v1/admin/api-keys/{key_id} |
admin |
Revoke a key (soft delete) |
| GET | /api/v1/admin/usage-history/date-anomalies |
admin |
Usage rows with impossible dates |
| GET | /api/v1/admin/usage-history/integrity |
admin |
Invariant checks over recent usage rows |
| GET | /api/v1/admin/usage-history/export |
admin |
Stream a date range as CSV |
| POST | /api/v1/admin/usage-history/reset-baseline |
admin |
Archive and delete usage rows before a cutoff |
| DELETE | /api/v1/admin/usage-history/cleanup |
admin |
Delete usage rows past a retention window |
| DELETE | /api/v1/admin/installs/clear-errors |
admin |
Clear stale install error/warning state |
| POST | /api/v1/admin/installs/reclassify |
admin |
Re-run the install-item classifier over stored rows |
| GET | /api/v1/admin/orphans |
admin |
Module rows whose device row does not exist |
| DELETE | /api/v1/admin/orphans |
admin |
Delete the module and event rows of named orphaned device ids |
| GET | /api/v1/debug/database |
read |
Duplicate, orphan, and table-size diagnostics |
/api/v1/debug/database does not contain /admin in its path, so under the
scope rules above it is reachable with a read credential. Treat it as a
privileged endpoint when you decide which keys to issue.
Pagination. List endpoints that accept limit and offset emit
X-Total-Count, X-Offset, and — when a limit was requested — X-Limit and
an RFC 5988 Link header with next and prev relations. When no limit is
given the endpoint returns the full set and only the totals are emitted.
Archived devices. Every fleet and device-list endpoint takes
includeArchived (default false). Archived devices keep all their data but
are hidden from the default views.
Inventory state. When an asset inventory system has written a state for a
device (see PUT /api/v1/inventory-state), the device list, the device
record, the dashboard, and every per-device fleet report carry it as
inventoryState. Rows with no state omit the key on the fleet reports and
carry null on the device record and the dashboard. A device in storage is
expected to be silent: it stays listed and searchable, but the dashboard, the
stale-agent list, and the collection-health buckets count it apart.
{
"inventoryState": {
"state": "storage",
"storageLocation": "Store Room 2",
"leaseNumber": "L-0042",
"assetTag": "ACME-11482",
"updatedAt": "2026-01-15T06:00:00+00:00"
}
}Conditional GET. Successful /api/v1/ GETs carry a weak ETag. Send it
back as If-None-Match to get an empty 304.
Errors. Client errors return the handler's own detail. Server errors are
masked: the body carries a generic label and a reference that matches the
X-Request-ID header and the server log line.
{
"error": "Internal server error",
"detail": "Internal server error. Reference: 9f2c41ab7d03",
"reference": "9f2c41ab7d03",
"status_code": 500
}| Parameter | Type | Default | Notes |
|---|---|---|---|
limit |
int | none | 1–1000. Omitted returns everything. |
offset |
int | 0 |
|
includeArchived |
bool | false |
{
"devices": [
{
"serialNumber": "WS-0001",
"deviceId": "11111111-2222-3333-4444-555555555555",
"deviceName": "design-lab-01",
"name": "design-lab-01",
"hostname": "design-lab-01",
"lastSeen": "2026-01-15T08:55:02+00:00",
"createdAt": "2025-09-02T17:41:19+00:00",
"registrationDate": "2025-09-02T17:41:19+00:00",
"status": "online",
"archived": false,
"archivedAt": null,
"platform": "Windows",
"osName": "Windows 11 Enterprise",
"osVersion": "10.0.26100",
"lastEventTime": "2026-01-15T08:55:02+00:00",
"totalEvents": 0,
"assetTag": "ACME-11482",
"usage": "Lab",
"catalog": "Production",
"department": "Design",
"location": "Building A",
"owner": "acme-it",
"inventory": {
"deviceName": "design-lab-01",
"assetTag": "ACME-11482",
"usage": "Lab",
"catalog": "Production",
"location": "Building A",
"department": "Design",
"fleet": "Workstations"
},
"modules": {
"inventory": { "deviceName": "design-lab-01", "assetTag": "ACME-11482" },
"system": { "operatingSystem": { "name": "Windows 11 Enterprise", "version": "10.0.26100" } },
"network": { "hostname": "design-lab-01" }
}
}
],
"total": 412,
"message": "Successfully retrieved 1 devices",
"page": 1,
"pageSize": 1,
"hasMore": true
}Resolves by serial number or by the devices.id column, which holds the same
value. Returns every module row for the device. The installs module has its
runLog key removed and the management module has every log root's tails
stripped — both are fetched separately because they are large.
Returns 404 when the device does not exist.
{
"success": true,
"device": {
"serialNumber": "WS-0001",
"deviceId": "11111111-2222-3333-4444-555555555555",
"name": "design-lab-01",
"platform": "Windows",
"clientVersion": "2026.01.09.1420",
"lastSeen": "2026-01-15T08:55:02+00:00",
"createdAt": "2025-09-02T17:41:19+00:00",
"registrationDate": "2025-09-02T17:41:19+00:00",
"archived": false,
"archivedAt": null,
"modules": {
"system": { "operatingSystem": { "name": "Windows 11 Enterprise", "version": "10.0.26100" } },
"hardware": { "manufacturer": "Acme", "model": "Precision 5000" },
"inventory": { "deviceName": "design-lab-01", "assetTag": "ACME-11482" },
"network": { "hostname": "design-lab-01" },
"security": {},
"management": {},
"installs": {},
"applications": {},
"peripherals": {},
"identity": {}
}
}
}module_name must be one of applications, hardware, identity,
installs, inventory, management, network, peripherals, security,
system. Anything else is a 400; an unknown device is a 404. A device with no
row for that module returns "data": null rather than 404.
{
"success": true,
"module": "hardware",
"data": {
"manufacturer": "Acme",
"model": "Precision 5000",
"processor": { "name": "Acme Core 9", "cores": 16 },
"memory": { "totalGB": 64 }
}
}tool names a management-tool log root (for example installs, bootstrap,
reports, state, encryption, users, utilities). The root is selected
inside PostgreSQL rather than by shipping the whole management document to the
application. A tool with no root returns "root": null, not a 404.
{
"tool": "installs",
"root": {
"tool": "installs",
"path": "/var/log/managed/installs",
"files": [{ "name": "run.log", "sizeBytes": 184320 }],
"tails": { "run.log": ["2026-01-15 08:55:01 INFO session complete"] }
}
}| Parameter | Type | Default | Notes |
|---|---|---|---|
days |
int | 90 |
1–548 |
appName |
string | none | Exact application name |
activeSeconds is the reportable measure of human attention. totalSeconds
is summed process lifetime across concurrent processes and routinely exceeds
the wall clock, so it is not a duration you can present as one.
{
"serialNumber": "WS-0001",
"days": 90,
"filters": { "appName": null },
"data": [
{
"date": "2026-01-14",
"appName": "Example Editor",
"launches": 3,
"totalSeconds": 20340.0,
"totalHours": 5.65,
"activeSeconds": 9120.0,
"activeHours": 2.53,
"foregroundSeconds": 7440.0,
"foregroundHours": 2.07,
"users": ["asmith"]
}
],
"count": 1
}| Parameter | Type | Default | Notes |
|---|---|---|---|
includeArchived |
bool | false |
|
limit |
int | none | 1–5000 |
offset |
int | 0 |
Returns a bare JSON array — one object per device, not an envelope. Emits
pagination headers. Each storage entry is slimmed to its summary fields;
volumeLetters lists the volume letters on that disk where the Windows client
reports them, which is how the system disk is labelled. The network.onCampus and network.campusDns booleans
are computed from a hard-coded list of internal DNS servers and address
prefixes in routers/fleet.py; change those constants for your own network or
ignore the fields.
[
{
"deviceId": "11111111-2222-3333-4444-555555555555",
"serialNumber": "WS-0001",
"deviceName": "design-lab-01",
"lastSeen": "2026-01-15T08:55:02+00:00",
"collectedAt": "2026-01-15T08:54:40+00:00",
"manufacturer": "Acme",
"model": "Precision 5000",
"processor": { "name": "Acme Core 9", "cores": 16 },
"memory": { "totalGB": 64 },
"storage": [
{
"name": "Acme NVMe 2TB",
"type": "SSD",
"capacity": 2048408248320,
"freeSpace": 817112494080,
"health": "Healthy",
"interface": "NVMe",
"isInternal": true,
"volumeLetters": ["C:"]
}
],
"graphics": [{ "name": "Acme GX 4000" }],
"osName": "Windows 11 Enterprise",
"osVersion": "10.0.26100",
"architecture": "x86_64",
"assetTag": "ACME-11482",
"usage": "Lab",
"catalog": "Production",
"location": "Building A",
"department": "Design",
"area": "Design",
"fleet": "Workstations",
"displays": [{ "model": "Acme U27", "serialNumber": "DSP-0004" }],
"network": {
"ipAddress": "192.0.2.31",
"dnsServers": ["192.0.2.1"],
"onCampus": false,
"campusDns": false
}
}
]/api/v1/system, /api/v1/network, /api/v1/security, /api/v1/management,
/api/v1/inventory, /api/v1/peripherals, /api/v1/identity,
/api/v1/profiles, /api/v1/installs, and /api/v1/installs/full take the
same three parameters and likewise return a bare array. Their objects carry
the module's own fields plus the same inventory context, and inventoryState
where one is recorded.
/api/v1/system also carries installedUpdates: up to 100 installed OS
updates per device, each projected to id (the KB or hotfix id), title, and
installedOn, whichever spelling the client reported them in.
/api/v1/installs/full puts collectedAt on each device's installs object.
That is when the installs module was last ingested, which is not the device's
lastSeen: other modules check in more often, and the installs module only
refreshes after a managed-software session that did work.
Flattens each device's managed-install items into one record per item, from
both the Cimian (Windows) and Munki (macOS) sections. The source field says
which produced the row. Stored items also carry reportmateStatus, the state
ingest decided for them (see Ingest-Pipeline); this projection passes the
client's currentStatus through.
[
{
"id": "11111111-2222-3333-4444-555555555555_cimian_0",
"deviceId": "11111111-2222-3333-4444-555555555555",
"deviceName": "design-lab-01",
"serialNumber": "WS-0001",
"lastSeen": "2026-01-15T08:55:02+00:00",
"collectedAt": "2026-01-15T08:54:40+00:00",
"itemName": "ExampleEditor",
"currentStatus": "Installed",
"latestVersion": "4.2.1",
"installedVersion": "4.2.1",
"installDate": "2026-01-09T11:02:00+00:00",
"lastChecked": "2026-01-15T08:54:40+00:00",
"updateAvailable": false,
"usage": "Lab",
"catalog": "Production",
"location": "Building A",
"assetTag": "ACME-11482",
"fleet": "Workstations",
"platform": "Windows",
"source": "cimian"
}
]Builds the filter vocabulary for the installs view and, alongside it, finds
devices that are still checking in but whose management agent has stopped
running. Those carry no errors or warnings — their records simply froze — so
no per-item metric surfaces them. The threshold is one day between the
device's last check-in and its agent's last run. Devices an inventory reports
as in storage are left out of staleAgents and counted in
storageDeviceCount; every entry in devices carries inventoryState where
one is recorded.
{
"success": true,
"managedInstalls": ["Example Renderer", "ExampleEditor"],
"cimianInstalls": ["ExampleEditor", "Example Renderer"],
"munkiInstalls": ["ExampleEditor"],
"otherInstalls": [],
"usages": ["Lab", "Office"],
"catalogs": ["Production", "Testing"],
"rooms": ["Building A"],
"areas": ["Design"],
"fleets": ["Workstations"],
"platforms": ["Windows", "macOS"],
"softwareRepos": ["https://packages.example.com"],
"manifests": ["workstations/design"],
"devicesWithData": 412,
"staleAgentCount": 3,
"staleAgentThresholdDays": 1.0,
"staleAgents": [
{ "serialNumber": "WS-0002", "deviceName": "design-lab-02", "staleDays": 6.4 }
],
"storageDeviceCount": 7,
"devices": []
}| Parameter | Type | Default | Notes |
|---|---|---|---|
deviceNames, applicationNames, publishers, categories, versions
|
string | none | Comma-separated |
search |
string | none | Case-insensitive substring of the application name |
sizeMin, sizeMax
|
int | none | |
usages, catalogs, areas, fleets, rooms, locations, platforms
|
string | none | Comma-separated inventory filters |
includeArchived |
bool | false |
|
limit |
int | 500 |
1–5000 |
offset |
int | 0 |
|
deviceLimit |
int | 2000 |
1–5000; caps how many devices are scanned |
Returns a bare array of application records. installDateFrom,
installDateTo and loadAll are accepted for compatibility but do not filter
the result.
The fleet utilization rollup, aggregated from usage_history over the
lookback window and grouped by canonical application name.
| Parameter | Type | Default | Notes |
|---|---|---|---|
days |
int | 30 |
1–548 |
applicationNames |
string | none | Comma-separated |
usages, catalogs, locations, areas, fleets, rooms, platforms
|
string | none | Comma-separated |
minHours |
float | none | |
minLaunches |
int | none | |
includeArchived |
bool | false |
Application names are canonicalized at query time by an alias map in
dependencies.py, so version-suffixed editions of a product collapse into one
row. A vendor's launcher, updater, or embedded runtime keeps its own name
deliberately — folding one into the product makes the row a sum of two
different things, and the launcher runs at every logon while the product may
not.
{
"status": "ok",
"applications": [
{
"name": "Example Editor",
"totalSeconds": 1104000.0,
"totalHours": 306.67,
"activeSeconds": 412800.0,
"activeHours": 114.67,
"foregroundSeconds": 361200.0,
"foregroundHours": 100.33,
"activeRatio": 0.37,
"launchCount": 1841,
"deviceCount": 88,
"userCount": 63,
"activeDeviceCount": 74,
"activeUserCount": 55,
"installedDeviceCount": 120,
"firstUsed": "2025-12-16",
"lastUsed": "2026-01-14",
"devices": ["WS-0001"],
"users": ["asmith"],
"isSingleUser": false,
"isSingleActiveUser": false,
"activeDevices": ["WS-0001"],
"activeUsers": ["asmith"],
"aliasedFrom": ["Example Editor 4.2.1", "exeditor.exe"]
}
],
"topUsers": [
{
"username": "asmith",
"totalSeconds": 39600.0,
"totalHours": 11.0,
"launchCount": 62,
"appsUsed": 9,
"devicesUsed": 2
}
],
"singleUserApps": [],
"unusedApps": [],
"devicesAggregate": [
{
"serialNumber": "WS-0001",
"deviceName": "design-lab-01",
"usage": "Lab",
"catalog": "Production",
"location": "Building A",
"room": "Building A",
"department": "Design",
"area": "Design",
"fleet": "Workstations",
"totalSeconds": 20340.0,
"totalHours": 5.65,
"launchCount": 12
}
],
"summary": {
"totalAppsTracked": 213,
"totalActiveHours": 114.67,
"totalUsageHours": 306.67,
"totalLaunches": 1841,
"uniqueUsers": 63,
"uniqueDevices": 88,
"singleUserAppCount": 0,
"unusedAppCount": 0,
"noActiveUseAppCount": 41
},
"filters": {
"days": 30,
"applicationNames": [],
"usages": [],
"catalogs": [],
"locations": [],
"minHours": null,
"minLaunches": null
},
"lastUpdated": "2026-01-15T09:00:00+00:00"
}app is required and matched as a case-insensitive substring. Because the
fleet rollup groups on the canonical name, a query for an aliased product must
use a pattern that matches the raw stored names.
| Parameter | Type | Default | Notes |
|---|---|---|---|
app |
string | required | Substring, case-insensitive |
days |
int | 30 |
1–548 |
usages, catalogs, locations
|
string | none | Comma-separated |
includeArchived |
bool | false |
Each device entry carries its inventory dimensions, its hours and launches, the human users seen on it, and both the canonical and raw application-name variants that matched:
{
"status": "ok",
"appPattern": "example editor",
"days": 30,
"devices": [
{
"serialNumber": "WS-0001",
"deviceName": "design-lab-01",
"usage": "Lab",
"catalog": "Production",
"location": "Building A",
"room": "Building A",
"department": "Design",
"area": "Design",
"fleet": "Workstations",
"assetTag": "ACME-11482",
"totalSeconds": 20340.0,
"totalHours": 5.65,
"launchCount": 12,
"userCount": 1,
"users": ["asmith"],
"appVariants": ["Example Editor"],
"appVariantCount": 1,
"rawAppVariants": ["Example Editor 4.2.1", "exeditor.exe"],
"firstUsed": "2025-12-16",
"lastUsed": "2026-01-14"
}
],
"summary": {
"deviceCount": 1,
"totalUsageHours": 5.65,
"totalLaunches": 12,
"uniqueUsers": 1
},
"filters": { "usages": [], "catalogs": [], "locations": [] },
"lastUpdated": "2026-01-15T09:00:00+00:00"
}Usernames are filtered to human principals — computer accounts ending in $
and recognised service principals are excluded from users and from the
uniqueUsers counts.
applicationNames is required and comma-separated. Returns a map from each
requested name to its version histogram.
{
"Example Editor": {
"totalDevices": 88,
"versions": { "4.2.1": 71, "4.1.0": 16, "Unknown": 1 }
}
}| Parameter | Type | Default | Notes |
|---|---|---|---|
freshDays |
int | 7 |
1–90 |
staleDays |
int | 30 |
1–180 |
includeArchived |
bool | false |
Each device falls into one of four buckets by the date of its most recent
usage_history row: healthy (within freshDays), stale (within
staleDays), dark (older), or never (no usage rows at all). darkDevices
lists the dark and never devices, never first, then longest-gone first.
Devices an inventory reports as in storage sit outside all four buckets and
outside totalDevices; they are counted in summary.storage and kept off the
dark list.
{
"status": "ok",
"summary": {
"totalDevices": 412,
"healthy": 380,
"stale": 24,
"dark": 5,
"never": 3,
"storage": 7,
"freshDays": 7,
"staleDays": 30
},
"byPlatform": {
"Windows": { "healthy": 290, "stale": 18, "dark": 4, "never": 2, "total": 314 },
"macOS": { "healthy": 90, "stale": 6, "dark": 1, "never": 1, "total": 98 }
},
"darkDevices": [
{
"serialNumber": "WS-0002",
"deviceName": "design-lab-02",
"platform": "Windows",
"osName": "Windows 11 Enterprise",
"lastSeen": "2026-01-15 08:12:00+00",
"usage": "Lab",
"catalog": "Production",
"location": "Building A",
"lastUsageDate": "2025-11-30",
"daysSinceUsage": 46,
"totalHoursEver": 812.4,
"rowCount": 2204,
"bucket": "dark"
}
],
"lastUpdated": "2026-01-15T09:00:00+00:00"
}| Parameter | Type | Default | Notes |
|---|---|---|---|
search |
string | "" |
Matches commonName, issuer, subject, or serial |
status |
string | all |
all, valid, expired, expiring
|
limit |
int | 1000 |
1–10000 |
includeArchived |
bool | false |
Returns a bare array of certificate records with commonName, issuer,
subject, status, notBefore, notAfter, daysUntilExpiry, isExpired,
isExpiringSoon, storeName, storeLocation, keyAlgorithm,
certSerialNumber, and isSelfSigned, each carrying its device's
serialNumber, deviceName, and platform.
Sweeps one management tool's log tails across the fleet and returns matching
lines. With summary=true it also groups messages into fleet-wide patterns so
the same fault on a hundred devices is counted once.
A line in the shape [yyyy-MM-dd HH:mm:ss] LEVEL message (the level token may
be bracketed and in any case) is classified by that token alone, so an INFO
line that mentions a failure is not counted as an error. Other shapes,
including CMTrace records, are classified by level words and markers in the
line.
| Parameter | Type | Default | Notes |
|---|---|---|---|
levels |
string | error,warning |
Comma-separated: error, warning, info, debug
|
platform |
string | none |
windows or macos
|
file |
string | none | Only lines from this file within the root |
q |
string | none | Case-insensitive substring the line must contain |
summary |
bool | false |
Also return message patterns (top 200); otherwise patterns is null
|
maxLinesPerDevice |
int | 200 |
1–5000 |
includeArchived |
bool | false |
|
limit |
int | none | 1–5000 devices |
offset |
int | 0 |
{
"tool": "installs",
"levels": ["error", "warning"],
"devicesScanned": 412,
"devicesMatched": 17,
"devicesTruncated": 0,
"lineTotals": { "error": 22, "warning": 61, "info": 0, "debug": 0 },
"results": [
{
"serialNumber": "WS-0001",
"deviceName": "design-lab-01",
"platform": "Windows",
"lastSeen": "2026-01-15T08:55:02+00:00",
"root": {
"name": "ManagedInstalls",
"path": "C:\\ProgramData\\ManagedInstalls\\logs",
"newestModified": "2026-01-15T08:54:40+00:00",
"errorCount": 1,
"warningCount": 3
},
"counts": { "error": 1, "warning": 3, "info": 0, "debug": 0 },
"lines": [
{ "file": "run.log", "level": "error", "line": "[2026-01-15 08:54:39] ERROR install failed for ExampleEditor" }
],
"truncated": false
}
],
"patterns": [
{
"message": "install failed for ExampleEditor",
"level": "error",
"lines": 19,
"devices": 12,
"sample": "[2026-01-15 08:54:39] ERROR install failed for ExampleEditor"
}
]
}The agent check-in. Requires the ingest scope. Ingest-Pipeline covers
the whole path, including every rejection reason.
Headers the endpoint reads beyond authentication:
| Header | Purpose |
|---|---|
Content-Encoding |
gzip, deflate, or identity. Bounded inflate. |
Idempotency-Key |
Replay guard; a repeat is acknowledged without reprocessing |
X-Device-Serial, X-Device-Uuid, X-Device-Name, X-Platform, X-Client-Version
|
Mirror the payload's identity so a rejection is still attributable when the body cannot be parsed |
Request body:
{
"metadata": {
"deviceId": "11111111-2222-3333-4444-555555555555",
"serialNumber": "WS-0001",
"collectedAt": "2026-01-15T08:54:40+00:00",
"clientVersion": "2026.01.09.1420",
"platform": "Windows",
"collectionType": "Full",
"enabledModules": ["system", "hardware", "inventory"]
},
"events": [
{ "eventType": "success", "message": "Managed install run completed", "details": {} }
],
"system": { "operatingSystem": { "name": "Windows 11 Enterprise", "version": "10.0.26100" } },
"hardware": { "manufacturer": "Acme", "model": "Precision 5000" },
"inventory": { "deviceName": "design-lab-01", "assetTag": "ACME-11482" }
}Success:
{
"success": true,
"message": "Complete data storage: 3 modules, 1 events",
"device_id": "WS-0001",
"serial_number": "WS-0001",
"modules_processed": ["system", "hardware", "inventory"],
"events_stored": 1,
"timestamp": "2026-01-15T08:54:41.882014+00:00",
"internal_uuid": "11111111-2222-3333-4444-555555555555"
}An Idempotency-Key replay:
{
"status": "duplicate",
"idempotent": true,
"serialNumber": "WS-0001",
"message": "Submission with this Idempotency-Key was already processed"
}metadata.deviceId and metadata.serialNumber are required and non-empty;
platform must be one of Windows, macOS, Linux, Unknown;
collectionType must be Full or Single. Both camelCase and snake_case
spellings of the metadata keys are accepted. Modules may sit at the top level
or nested under a modules key.
| Parameter | Type | Default | Notes |
|---|---|---|---|
limit |
int | 100 |
1–1000 |
offset |
int | 0 |
|
startDate, endDate
|
string | none | ISO 8601 |
type |
string | none | One or a comma-separated list of success, warning, error, info, system
|
An unparseable date or an unrecognised type is logged and ignored rather than rejected.
{
"success": true,
"events": [
{
"id": 918442,
"device": "WS-0001",
"deviceName": "design-lab-01",
"assetTag": "ACME-11482",
"kind": "warning",
"message": "Managed install run completed with 2 warnings",
"ts": "2026-01-15T08:54:40+00:00",
"platform": "Windows 11 Enterprise",
"serialNumber": "WS-0001",
"eventType": "warning",
"timestamp": "2026-01-15T08:54:40+00:00"
}
],
"total": 148920,
"totalEvents": 148920,
"count": 1,
"limit": 100,
"offset": 0
}The view that answers "this device is installed but never appeared". Every rejected or repaired check-in is recorded here, including ones from machines that were never registered.
| Parameter | Type | Default | Notes |
|---|---|---|---|
limit |
int | 100 |
1–1000 |
offset |
int | 0 |
|
serial |
string | none | Case-insensitive substring |
reason |
string | none | Exact reason code |
hours |
int | 168 |
1–2160 |
outcome |
string | rejected |
rejected, retried, accepted, all
|
outcome is derived, not stored:
| Value | Meaning |
|---|---|
accepted |
Recorded below HTTP 400. The check-in got in; the row exists to keep a client defect visible. |
retried |
A transport failure (upload_aborted, body_unreadable, empty_body) on a device whose next check-in the server accepted after the failure. Acceptance is judged by server receipt time (device_ingest_state), not by the client's collectedAt, because a retry carries the same collection timestamp as the upload that dropped. |
rejected |
Turned away, with nothing since. These are the devices whose data genuinely did not arrive. |
{
"success": true,
"failures": [
{
"id": 4412,
"ts": "2026-01-15T07:31:09+00:00",
"failureType": "validation",
"reason": "sentinel_serial",
"detail": "Invalid serial number: 'To Be Filled By O.E.M.' is a sentinel/placeholder value. Device must provide a real hardware serial number.",
"statusCode": 400,
"outcome": "rejected",
"endpoint": "/api/v1/events",
"clientIp": "198.51.100.24",
"userAgent": "ReportMate/2026.01.09.1420",
"serialNumber": null,
"deviceUuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"deviceName": "unprovisioned-01",
"platform": "Windows",
"clientVersion": "2026.01.09.1420"
}
],
"summary": [
{ "reason": "sentinel_serial", "count": 6, "devices": 2, "lastSeen": "2026-01-15T07:31:09+00:00" }
],
"total": 6,
"count": 1,
"limit": 100,
"offset": 0,
"hours": 168,
"outcome": "rejected",
"counts": { "rejected": 6, "retried": 19, "accepted": 3 }
}One call replacing the ten the dashboard would otherwise make.
| Parameter | Type | Default | Notes |
|---|---|---|---|
eventsLimit |
int | 200 |
1–500, applied per event type |
includeArchived |
bool | false |
Each device carries a status derived from lastSeen: online, idle
after an hour, offline after a day. installStats is a plain aggregate over
the four precomputed counter columns on installs, split by platform, and
counts both items and devices. Devices an inventory reports as in storage
stay in devices but are left out of totalDevices and installStats, and
are counted in storageDevices.
{
"devices": [],
"totalDevices": 412,
"storageDevices": 7,
"installStats": {
"devicesWithErrors": 9,
"devicesWithWarnings": 41,
"winDevicesWithErrors": 6,
"winDevicesWithWarnings": 30,
"macDevicesWithErrors": 3,
"macDevicesWithWarnings": 11,
"totalErrorItems": 14,
"totalWarningItems": 88,
"winErrorItems": 10,
"winWarningItems": 70,
"macErrorItems": 4,
"macWarningItems": 18,
"hasInstallData": true
},
"events": [],
"totalEvents": 200,
"lastUpdated": "2026-01-15T09:00:00+00:00"
}Anonymous. Describes which credentials this deployment accepts and, when OIDC
is on, the issuer and audience a client needs to mint a bearer token. Nothing
in it is secret: the issuer and audience are the same values every token
carries in its claims. A client reads oidc.audience and requests a token for
that resource.
{
"credentials": ["Authorization: Bearer", "X-API-Key", "X-Client-Passphrase"],
"oidc": {
"enabled": true,
"issuers": ["https://login.example.com/00000000-0000-0000-0000-000000000000/v2.0"],
"audience": "api://reportmate",
"audiences": ["api://reportmate"]
}
}With OIDC off, credentials omits the bearer entry, enabled is false, the
lists are empty, and audience is null.
Every recorded state, keyed by upper-cased serial number, with per-state counts.
{
"devices": {
"WS-0002": {
"state": "storage",
"storageLocation": "Store Room 2",
"leaseNumber": "L-0042",
"assetTag": "ACME-11483",
"updatedAt": "2026-01-15T06:00:00+00:00"
}
},
"total": 1,
"counts": { "checked_out": 0, "storage": 1, "decommissioning": 0 }
}Written by an asset inventory system, which knows what ReportMate cannot: that
a silent device is on a shelf rather than lost. Needs the admin scope.
{
"devices": [
{ "serialNumber": "WS-0002", "state": "storage", "storageLocation": "Store Room 2", "leaseNumber": "L-0042", "assetTag": "ACME-11483" },
{ "serialNumber": "WS-0001", "state": "checked_out" }
],
"replace": true
}| Field | Notes |
|---|---|
serialNumber |
Required. Trimmed and upper-cased before storage. |
state |
Required. checked_out, storage, or decommissioning. |
storageLocation, leaseNumber, assetTag
|
Optional. |
replace |
Default false. With true, the payload is the whole truth: every serial left out loses its state. |
A repeated serial in one payload keeps its last entry. More than 20 000
devices in one write is a 413. replace: true with an empty list is a 400,
because an empty answer from an inventory is nearly always a failed read on
the other side rather than a fleet with nothing in it. A write invalidates
every cache.
{ "written": 2, "cleared": 5, "replace": true }Returns the org-scoped settings document. Before first write:
{ "exists": false, "value": null, "schemaVersion": 1 }After:
{
"exists": true,
"value": {
"schemaVersion": 1,
"general": {},
"inventory": { "fields": [{ "key": "assetTag", "label": "Asset tag", "order": 1, "visible": true }] },
"security": { "rules": [{ "id": "disk-encryption", "check": "encryption", "state": "enabled", "severity": "danger" }] }
},
"schemaVersion": 1,
"updatedAt": "2026-01-12T14:03:11+00:00",
"updatedBy": "admin@example.com"
}Replaces the document. The body must be a JSON object; unknown keys are
preserved so the schema can evolve. Rule severities are checked against ok,
warning, danger, neutral, unknown — anything else is a 422. An
optional X-Updated-By header records who made the change; it is stripped of
CR/LF and truncated to 255 characters before storage.
The response is the same shape as the GET. Writing invalidates every cache.
{ "client_id": "dashboard", "scopes": ["read"] }scopes must be a non-empty subset of read, ingest, admin; anything
else is a 400.
{
"id": "a1b2c3d4e5f6",
"client_id": "dashboard",
"scopes": ["read"],
"key": "rm_a1b2c3d4e5f6_0f1e2d3c4b5a69788796a5b4c3d2e1f00112233445566778",
"warning": "Store this key now -- it cannot be retrieved again."
}GET /api/v1/admin/api-keys returns {"keys": [...], "count": n} with
id, client_id, scopes, active, created_at, created_by, and
last_used — never the secret. DELETE /api/v1/admin/api-keys/{key_id} sets
active = false and returns {"id": "...", "status": "revoked"}, or 404 if
no such key exists.
Requires ?confirm=true; without it the call is a 400. Nothing cascades: no
module table declares a foreign key to devices, so the endpoint deletes the
device's rows from every module table, events, and usage_history
explicitly before removing the device row. deletedData.modules is the
per-table row count surveyed before the delete; totalModuleRecords is the
number of module rows actually removed.
{
"success": true,
"message": "Device WS-0001 and all associated data has been permanently deleted",
"serialNumber": "WS-0001",
"deviceId": "11111111-2222-3333-4444-555555555555",
"deviceName": "design-lab-01",
"wasArchived": false,
"deletedData": {
"events": 3184,
"usageHistory": 9022,
"modules": { "system": 1, "hardware": 1, "applications": 1 },
"totalModuleRecords": 10
},
"timestamp": "2026-01-15T09:04:22+00:00",
"warning": "This data cannot be recovered"
}Archiving is the reversible alternative: PATCH .../archive and
PATCH .../unarchive return {"success": true, "archived": true|false, ...}
and are idempotent.
| Parameter | Type | Notes |
|---|---|---|
from |
string | Required. First row date to include, YYYY-MM-DD
|
to |
string | Required. First row date to exclude |
Streams text/csv with a Content-Disposition attachment filename. Columns
are device_id, date, app_name, publisher, launches, total_seconds, active_seconds, foreground_seconds, users, updated_at. Rows are ordered by
date, device, then application, so two exports of the same range are
byte-identical. The first batch is read before the response starts, so a
database failure is a 500 rather than a 200 with an empty body.
usage_history is the one table a device cannot re-report — every other
module row is a current-state snapshot that self-heals on the next check-in.
Export closed months if you need the record to outlive your backup window.
| Parameter | Type | Default | Notes |
|---|---|---|---|
before |
string | required | Archive and remove rows dated before this YYYY-MM-DD
|
confirm |
bool | false |
Without it you get a preview and nothing is changed |
reason |
string | "" |
Stored on the archived rows so a batch can be identified later |
device |
string, repeatable | none | Limit the reset to these serial numbers |
device limits the reset to the named serial numbers; repeat the parameter
for several (?device=WS-0001&device=WS-0002). Use it when one client build
wrote bad rows and the rest of the fleet is sound.
Destructive on the live reporting table. Rows are copied verbatim into
usage_history_archive before deletion. Runs on an unpooled connection with a
long statement timeout because the work does not fit in the 120-second request
ceiling.
{
"status": "ok",
"executed": true,
"cutoffDate": "2026-01-01",
"devices": null,
"archived": 412884,
"deleted": 412884,
"remaining": { "rows": 98221, "earliestDate": "2026-01-01", "latestDate": "2026-01-14" }
}Without confirm=true the response has "status": "preview",
"executed": false, and wouldArchiveAndDelete (rows, devices, applications,
earliest and latest date) and wouldRemain objects in place of the counts.
| Parameter | Type | Default | Notes |
|---|---|---|---|
days |
int | 10 |
0–365. Clears devices whose last_seen is older than this. 0 clears every device. |
item_age_days |
float | none | 0–365. Also clears individual items whose own last attempt is older than this, on devices that are still checking in. |
Staleness is keyed on devices.last_seen, not on the installs row's
updated_at — the latter is touched on every ingest for every device, so it
matches nothing. Clearing rewrites the JSONB and recomputes the precomputed
cimian_errors / cimian_warnings / munki_errors / munki_warnings
columns in the same statement, because the dashboard reads the columns.
A device whose JSONB needed no change but whose counter columns disagree with
it has only the counters rewritten; those are counted in countersReconciled.
{
"success": true,
"cleared": 12,
"clearedByItemAge": 3,
"countersReconciled": 1,
"itemAgeDays": 14.0,
"clearedByItemAgeDevices": ["WS-0002"],
"totalStale": 19,
"days": 10,
"cutoffDate": "2026-01-05T09:04:22+00:00",
"clearedDevices": ["WS-0002"],
"timestamp": "2026-01-15T09:04:22+00:00"
}| Parameter | Type | Default | Notes |
|---|---|---|---|
batch |
int | 200 |
1–1000. Rows read per batch. |
limit |
int | none | Stop after this many rows, for a rehearsal against part of the fleet. |
Every stored install item carries the state ingest decided for it, and the four counter columns are stamped at the same moment, so a change to the classifier reaches a device only when it next reports. This applies the current classifier to what is already stored. It calls the same two functions ingest calls, and is idempotent: a row whose stamps and counters already match is not rewritten. It is manual and batched, never scheduled, and invalidates the read caches when it finishes.
{
"success": true,
"scanned": 412,
"changed": 18,
"countersMoved": 4,
"sample": [
{
"device_id": "WS-0002",
"before": { "cimian_errors": 0, "cimian_warnings": 2, "munki_errors": 0, "munki_warnings": 0 },
"after": { "cimian_errors": 1, "cimian_warnings": 1, "munki_errors": 0, "munki_warnings": 0 }
}
]
}sample holds at most 50 rows whose counters moved, and countersMoved is
the length of sample, so it also stops at 50. changed is the full count of
rewritten rows.
| Parameter | Type | Default | Notes |
|---|---|---|---|
samples |
int | 5 |
0–50 device ids listed per table |
Every fleet query joins a module table back to devices, so a module row
whose device row is gone contributes to nothing while still sitting in the
table. This counts them per module table and reports what the orphaned
installs rows would have added to the dashboard's error and warning tiles.
Read-only.
{
"success": true,
"totalOrphanedRows": 6,
"tables": {
"installs": { "orphanedRows": 1, "sample": ["TEST-0001"] },
"system": { "orphanedRows": 1, "sample": ["TEST-0001"] },
"hardware": { "orphanedRows": 0 }
},
"hiddenByOrphanedInstalls": { "devices": 1, "errors": 5, "warnings": 0 },
"timestamp": "2026-01-15T09:04:22+00:00"
}| Parameter | Type | Default | Notes |
|---|---|---|---|
device_ids |
string | required | Comma-separated device ids to clean up |
confirm |
bool | false |
Must be true
|
Deliberately not a blanket sweep: why a device row is missing decides whether
its data should go, so the caller names the ids. Any id that still has a
device row is refused with a 400 and nothing is deleted. The ids' rows are
removed from every module table and from events. Irreversible.
{
"success": true,
"deviceIds": ["TEST-0001"],
"totalRowsDeleted": 9,
"tables": { "installs": { "rows": 1 }, "system": { "rows": 1 }, "events": { "rows": 7 } },
"timestamp": "2026-01-15T09:04:22+00:00",
"warning": "This data cannot be recovered"
}| Parameter | Type | Default |
|---|---|---|
device |
string | dashboard |
Issues a 60-minute Web PubSub client access token. When the SDK is missing or
EVENTS_CONNECTION is unset it returns HTTP 200 with a null token and an
error field rather than failing, so the dashboard can fall back to polling.
{
"url": "wss://pubsub.example.com/client/hubs/events",
"accessToken": "eyJhbGciOi...",
"expiresOn": "2026-01-15T10:00:00+00:00"
}AGPL-3.0 — a commercial license is available. reportmate.app