Skip to content

API Reference

Rod Christiansen edited this page Sep 3, 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. 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/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: 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
GET /api/v1/stats/installs read Aggregated install error/warning counters

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

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

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

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

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.

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

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.

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

GET /api/v1/applications

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.

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.

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

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.

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

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

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

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
{
  "devices": [],
  "totalDevices": 412,
  "installStats": {
    "devicesWithErrors": 9,
    "devicesWithWarnings": 41,
    "totalFailedInstalls": 14,
    "totalWarnings": 88
  },
  "events": [],
  "totalEvents": 200,
  "lastUpdated": "2026-01-15T09:00:00+00:00"
}

GET /api/v1/stats/installs

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

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

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

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
}

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.

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

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