Skip to content

API Reference

Rod Christiansen edited this page Oct 8, 2026 · 3 revisions

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.

Authentication in one line

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.

Route index

Health and service

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)

Devices

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

Fleet

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

Events

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

Statistics

Method Path Auth Purpose
GET /api/v1/dashboard read Devices, install stats, and recent events in one call

Inventory state

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

Settings

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.

Admin

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.

Conventions

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
}

Endpoint detail

GET /api/v1/devices

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
}

GET /api/v1/device/{serial_number}

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": {}
    }
  }
}

GET /api/v1/device/{serial_number}/modules/{module_name}

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 }
  }
}

GET /api/v1/device/{serial_number}/logs/{tool}

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"] }
  }
}

GET /api/v1/device/{serial_number}/applications/usage/history

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
}

GET /api/v1/hardware

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.

GET /api/v1/installs

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"
  }
]

GET /api/v1/installs/filters

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": []
}

GET /api/v1/applications

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.

GET /api/v1/applications/usage

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"
}

GET /api/v1/applications/usage/by-device

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.

GET /api/v1/applications/distribution

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 }
  }
}

GET /api/v1/applications/collection-health

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"
}

GET /api/v1/security/certificates

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.

GET /api/v1/management/logs/{tool}

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"
    }
  ]
}

POST /api/v1/events

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.

GET /api/v1/events

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
}

GET /api/v1/events/failures

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 }
}

GET /api/v1/dashboard

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"
}

GET /api/v1/auth/config

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.

GET /api/v1/inventory-state

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 }
}

PUT /api/v1/inventory-state

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 }

GET /api/v1/settings

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"
}

PUT /api/v1/settings

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.

POST /api/v1/admin/api-keys

{ "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.

DELETE /api/v1/device/{serial_number}

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.

GET /api/v1/admin/usage-history/export

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.

POST /api/v1/admin/usage-history/reset-baseline

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.

DELETE /api/v1/admin/installs/clear-errors

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"
}

POST /api/v1/admin/installs/reclassify

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.

GET /api/v1/admin/orphans

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"
}

DELETE /api/v1/admin/orphans

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"
}

GET /api/v1/negotiate

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"
}

Clone this wiki locally