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. 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/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: 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 |
| GET | /api/v1/stats/installs |
read |
Aggregated install error/warning counters |
| 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 |
| 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.
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": "4c6f8f5a-1d2e-4f80-9a11-0b1c2d3e4f50",
"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": "4c6f8f5a-1d2e-4f80-9a11-0b1c2d3e4f50",
"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. 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": "4c6f8f5a-1d2e-4f80-9a11-0b1c2d3e4f50",
"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": "Disk 0", "sizeGB": 2048, "freeGB": 761 }],
"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.
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.
[
{
"id": "4c6f8f5a-1d2e-4f80-9a11-0b1c2d3e4f50_cimian_0",
"deviceId": "4c6f8f5a-1d2e-4f80-9a11-0b1c2d3e4f50",
"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.
{
"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 }
],
"devices": []
}| Parameter | Type | Default | Notes |
|---|---|---|---|
deviceNames, applicationNames, publishers, categories, versions
|
string | none | Comma-separated |
search |
string | none | Free-text match |
installDateFrom, installDateTo
|
string | none | |
sizeMin, sizeMax
|
int | none | |
usages, catalogs, areas, fleets, rooms, locations, platforms
|
string | none | Comma-separated inventory filters |
loadAll |
bool | false |
|
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.
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.
{
"status": "ok",
"summary": {
"totalDevices": 412,
"healthy": 380,
"stale": 24,
"dark": 5,
"never": 3,
"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.
| 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 |
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 },
"results": [
{
"serialNumber": "WS-0001",
"deviceName": "design-lab-01",
"lines": [
{ "file": "run.log", "level": "error", "text": "install failed for ExampleEditor" }
]
}
],
"patterns": [
{ "pattern": "install failed for <item>", "devices": 12, "lines": 19 }
]
}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": "4c6f8f5a-1d2e-4f80-9a11-0b1c2d3e4f50",
"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": "4c6f8f5a-1d2e-4f80-9a11-0b1c2d3e4f50"
}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 that has checked in successfully since. |
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": "9a0b1c2d-3e4f-5061-7283-94a5b6c7d8e9",
"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 |
{
"devices": [],
"totalDevices": 412,
"installStats": {
"devicesWithErrors": 9,
"devicesWithWarnings": 41,
"totalFailedInstalls": 14,
"totalWarnings": 88
},
"events": [],
"totalEvents": 200,
"lastUpdated": "2026-01-15T09:00:00+00:00"
}No parameters. Counts Cimian item statuses and Munki item statuses plus run-level message strings, so a Mac whose problem is a preflight failure with no named item still contributes.
{
"devicesWithErrors": 9,
"devicesWithWarnings": 41,
"totalFailedInstalls": 14,
"totalWarnings": 88,
"lastUpdated": "2026-01-15T09:00:00+00:00"
}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. Deletes the device
row, cascading to module and event rows, and explicitly removes the device's
usage_history rows, which have no foreign key.
{
"success": true,
"message": "Device WS-0001 and all associated data has been permanently deleted",
"serialNumber": "WS-0001",
"deviceId": "4c6f8f5a-1d2e-4f80-9a11-0b1c2d3e4f50",
"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 |
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",
"archived": 412884,
"deleted": 412884,
"remaining": 98221
}| 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.
{
"success": true,
"cleared": 12,
"clearedByItemAge": 3,
"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 |
|---|---|---|
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