NetLock RMMNetLock RMM Docs
V — Appendix

Public API

The REST API of the server: tokens, scopes and rate limits in brief, where the Swagger reference lives, the custom field endpoints with level values and effective values, and the automation rules.

Public API

The server exposes a REST API under /v1 for external systems: a PSA, a CMDB, a documentation tool, an automation pipeline. This appendix explains how access works and documents the custom field endpoints and the automation rules in detail, because they carry rules the generated reference does not spell out. Every other endpoint is documented in the Swagger reference served by the server itself.

X.9.1 Access in brief

Switches. Settings → API tokens (route /settings/api-tokens) holds two deployment-wide switches: Public API enabled and Public API documentation enabled. The server re-reads the API switch about once a minute; no restart is needed. The documentation switch covers only /docs and the OpenAPI document.

Reference. With the documentation enabled, the server serves a Swagger UI at https://<server>/docs and the OpenAPI document at https://<server>/openapi/v1.json. Every route carries a summary and a description with its body shape, its scope and the permissions it needs. That page is the authoritative reference for request and response shapes.

Tokens. A token is created on the same page and shown once. It has the form nlk_<key id>_<secret> and is sent as a Bearer token:

Authorization: Bearer nlk_0123456789ab_...

A token is bound to a console account and carries:

  • Scopes such as customfields:read or devices:actions:script. Only scopes whose underlying permissions the bound account holds are offered, and the account's permissions are checked again on every request. A token never has more rights than its account.
  • Tenants: the tenants selected for the token plus the tenants the token created itself through the API, limited to what its account holds. With the option All tenants of the account they are the tenants of the account, evaluated on every request. A token can additionally be narrowed to a list of locations. A device, tenant, location or group outside that scope answers 404. See X.9.11.
  • A rate limit in requests per minute (default 600). Requests above it answer 429 with a Retry-After header. Independent of the token, one IP address is capped at 1200 requests per minute and at 20 failed authentications per minute.
  • An expiry date, or none. Tokens can be rotated and revoked from the same page.

Errors. Errors are JSON problem documents with a code, for example insufficient_scope (the token lacks the scope), insufficient_permission (the bound account lacks a permission), location_restricted (the token is limited to locations), invalid_parameter, not_found and rate_limited.

Audit. Writes through the API are written to the audit log with the token name.

X.9.2 Endpoint groups

The API covers the organization (tenants, locations, groups); devices with their inventory, enrolment, antivirus state, processes, event logs, files and registry; device actions (power, sync, commands and scripts, agent uninstall); mobile devices; events and notification results; custom fields; patch management and vulnerabilities; sensors, SNMP, uptime monitoring and the port scanner; software deployments and App Hub; scripts and jobs; policies, automations and maintenance windows; notification targets; the file server; tickets and time tracking; users and permissions; the audit log; reports and dashboards; relay sessions; outbound webhooks; application control, device control and controlled folder access; BitLocker; agent packages and installer links; and the platform's own health and licence state. See the Swagger reference for each of them.

X.9.3 Custom fields

Scope customfields:read for the reads and customfields:write for the writes. Every write in addition needs the permission collections_custom_fields_edit on the bound account; the global level needs settings_custom_fields_enabled for reads and writes. For the concepts (levels, inheritance, secrets) see Chapter 8.4.

Definitions

GET /v1/custom-fields returns the definition catalogue, paginated: id, name, description, author, createdAt and the definition document with its tabs, sections and fields. Definitions are global and not tenant scoped. Definitions cannot be created or changed over the API.

Device values

GET /v1/devices/{id}/custom-fields returns the rows stored on the device itself:

{
  "deviceId": 42,
  "items": [
    { "definitionId": 3, "definitionName": "AV", "fieldKey": "av_ou_id", "value": "ou-dev", "isSecret": false, "source": "api", "updatedAt": "2026-09-04T10:15:00Z" },
    { "definitionId": 3, "definitionName": "AV", "fieldKey": "av_token", "value": null, "isSecret": true, "source": "manual", "updatedAt": "2026-09-04T09:00:00Z" }
  ]
}
  • source names who wrote the device value last: manual (console), script (write-back from a script) or api.
  • isSecret is true for a Secret field; its value is always null.
  • A field the device has no row for is not listed. A value cleared in the console has no row either.

GET /v1/devices/{id}/custom-fields?effective=true returns one item per manual field of every definition with the value the device effectively reads, resolved the way a script variable is resolved: the device's own value first, then, for an inheritable field, the group, location, tenant and global value in that order.

{
  "deviceId": 42,
  "items": [
    { "definitionId": 3, "definitionName": "AV", "fieldKey": "av_ou_id", "value": "ou-123", "isSecret": false, "inheritable": true, "level": "tenant", "source": null, "updatedAt": "2026-09-04T10:15:00Z", "updatedBy": "ansible-token" },
    { "definitionId": 3, "definitionName": "AV", "fieldKey": "laps_status", "value": "windows-laps", "isSecret": false, "inheritable": false, "level": "device", "source": "script", "updatedAt": "2026-09-04T11:00:00Z", "updatedBy": null },
    { "definitionId": 4, "definitionName": "Notes", "fieldKey": "note", "value": null, "isSecret": false, "inheritable": false, "level": "none", "source": null, "updatedAt": null, "updatedBy": null }
  ]
}
  • level names where the value comes from: device, group, location, tenant, global, or none when no level holds a value.
  • source is set only when level is device. updatedBy (the account or token name) is set only for a value from a level above the device.
  • Secrets are never returned, on no level.

PUT /v1/devices/{id}/custom-fields sets device values:

{ "values": { "3:av_ou_id": "ou-dev", "3:note": null } }

The key of each entry is <definitionId>:<fieldKey>. null clears the value; the row is deleted, so the device inherits again. A value for a Secret field is stored encrypted. Stored values get source: api, the device is marked for a policy re-sync, and when an automation rule compares one of the fields, the rules are evaluated for the device and it is pushed at once if its assignment changed. The definition must exist; the key is not checked against the definition's fields. The answer is the device read back in the shape of the GET. The same values can be sent as customFields in PATCH /v1/devices/{id}.

Level values

One pair of routes per level, all with the same body and answer shape:

LevelRoutesVisible when
GlobalGET, PUT /v1/custom-fields/globalAlways; needs settings_custom_fields_enabled
TenantGET, PUT /v1/tenants/{id}/custom-fieldsThe tenant is in the token's tenant scope
LocationGET, PUT /v1/locations/{id}/custom-fieldsThe location is in the token's tenant scope and, when the token is limited to locations, in that list
GroupGET, PUT /v1/groups/{id}/custom-fieldsAs for its location

A level outside the scope answers 404. GET returns the rows stored on this level:

{
  "scopeType": "tenant",
  "scopeId": 5,
  "items": [
    { "definitionId": 3, "definitionName": "AV", "fieldKey": "av_ou_id", "value": "ou-123", "isSecret": false, "stale": false, "updatedAt": "2026-09-04T10:15:00Z", "updatedBy": "ansible-token" },
    { "definitionId": 3, "definitionName": "AV", "fieldKey": "av_token", "value": null, "isSecret": true, "stale": false, "updatedAt": "2026-09-04T10:15:00Z", "updatedBy": "ansible-token" }
  ]
}

stale is true when the field is no longer an inheritable manual field of its definition, or the definition is gone. The row is listed anyway so it can be cleared.

PUT takes the same body as the device endpoint, { "values": { "<definitionId>:<fieldKey>": value | null } }, and answers with the level read back:

  • Only inheritable Manual fields of type Text, Multiline or Secret are accepted, and a value may not exceed 64 KB. Any other entry answers 400 invalid_parameter with the reason, and nothing of the request is written.
  • null clears the row, also a stale one.
  • A token limited to locations cannot write tenant or global values (403 location_restricted). A token without any tenant cannot write global values.
  • updatedBy is the token name. One audit entry per request names the level and the counts, never the values.
  • Every write marks the devices of the level for a policy re-sync; a global write marks every device. When an automation rule compares one of the fields, the online devices of the level are pushed at once.

Example: set the OU id for tenant 5.

curl -X PUT "https://server.example.com/v1/tenants/5/custom-fields" \
  -H "Authorization: Bearer nlk_..." \
  -H "Content-Type: application/json" \
  -d '{"values":{"3:av_ou_id":"ou-123"}}'

X.9.4 Device actions and script variables

POST /v1/devices/{id}/actions/run-script (body { "scriptId" } or { "scriptName" }, optional runAsUser; scope devices:actions:script) and POST /v1/devices/{id}/actions/run-command (body { "command", "language", "runAsUser", "timeoutMinutes" }; scope devices:actions:shell) render script variables for the target device before the command is sent, in the shell named by language or the device's platform default. cmd is not rendered. The action record and GET /v1/device-actions/{id} keep the unrendered text, so a resolved secret never appears in an API answer. The result is the output with write-back marker lines removed; their values are stored on the device. Every completed command also writes an event Remote shell command executed. (type Remote Shell) on the device that names the token, the action, the run-as user and the shell and carries the output; the command text is not part of it. GET /v1/device-actions/{id} reads the shell output from the action record; result and output hold the same text for shell actions. Both actions exist as /v1/devices/bulk/actions/run-script and /v1/devices/bulk/actions/run-command as well.

POST /v1/sensors/{id}/test renders the sensor's scripts for the target device in the same way. Values the scripts would write back are listed as Would set: <keys> in the additional details and are not stored.

POST /v1/devices/{id}/files/actions/collect (scope devices:remote-files:read) issues a one-time upload token together with the command it sends to the device: the server accepts the collected file only with that token, which is bound to the device and the file name and valid for fifteen minutes. This is transparent for API clients — the call and its action record are unchanged, and the token never appears in an API answer — but it needs the Remote agent of release 3.3.0.6 on the device; with an older agent the action ends with a failed result (see A.4.1b).

See Script variables and custom fields in scripts and Write custom field values from a script.

X.9.5 Automations

Scope automations:read for the reads and automations:write for the writes. For the model — conditions, negation, priority, the enabled switch, how the server picks a device's policy and how sensors and jobs are added — see Chapter 5.

Reading rules

GET /v1/automations lists the rules, ordered by priority then id unless sorted otherwise (sort=priority, sort=-priority, sort=name, …), with the filters tenantId, deviceId, enabled (true/false), sensorId, jobId (rules that add that sensor or job) and search. GET /v1/automations/{id} returns one rule. Both return:

{
  "id": 13,
  "name": "Linux in ACME",
  "description": "",
  "enabled": true,
  "priority": 600,
  "policyName": "Linux base",
  "sensorIds": [7, 9],
  "jobIds": [],
  "conditions": [
    { "type": "tenant", "negate": false, "tenantId": 5, "name": "ACME" },
    { "type": "platform", "negate": false, "values": ["Linux"] },
    { "type": "device_attribute", "negate": true, "field": "device_name", "op": "wildcard", "value": "SRV-*" }
  ],
  "deviceId": null, "tenantId": 5, "locationId": null, "groupId": null,
  "effectiveTenantId": 5,
  "author": "admin@example.com",
  "createdAt": "2026-09-16T10:21:27Z"
}
  • conditions is the rule's condition list as stored. Every entry has type and negate; an entity condition carries its id (deviceId, tenantId, locationId or groupId) and the entity's name; internal_ip and domain carry value; platform carries values; device_attribute carries field, op and value.
  • A location or group condition additionally carries locationIds / groupIds and names in the same order (null for an entity that is gone or outside the token's tenants). It may name several entities ("is one of"); then locationId / groupId and name are null and only the lists carry the entries: { "type": "group", "negate": false, "groupId": null, "name": null, "groupIds": [12, 17, 23], "names": ["No print", "No print", "No print"] }. For a single entity both forms are filled.
  • policyName is null for a rule that assigns no policy. sensorIds and jobIds are the sensors and jobs the rule adds to every matching device on top of its policy.
  • deviceId, tenantId, locationId and groupId on the rule itself mirror the non-negated entity conditions that name one entity; a rule whose location or group condition names several carries the tenant of those entities in tenantId; effectiveTenantId is the tenant the rule belongs to, or null for a rule without an entity condition. Such a rule is instance wide and is returned to every token holding the scope.
  • A rule is returned only when the tenant it resolves to is inside the token's tenant scope.

The fields condition, expectedResult, actions, trigger, category and subCategory of the first API version are no longer returned; trigger is policyName now.

Writing rules

POST /v1/automations creates a rule, POST /v1/policies/{id}/assign creates a rule for the policy named by the route, PATCH /v1/automations/{id} changes one, DELETE /v1/automations/{id} removes one. The create body:

{
  "name": "Linux in ACME",
  "description": "optional",
  "policyName": "Linux base",
  "sensorIds": [7, 9],
  "jobIds": [],
  "enabled": true,
  "priority": 600,
  "conditions": [
    { "type": "tenant", "tenantId": 5 },
    { "type": "platform", "values": ["Linux"] },
    { "type": "device_attribute", "field": "device_name", "op": "wildcard", "value": "SRV-*", "negate": true }
  ]
}
  • conditions needs at least one entry; all have to hold. Types and fields: device { deviceId }, tenant { tenantId }, location { locationId }, group { groupId }, internal_ip { value }, domain { value }, platform { values[] } with Windows, Linux, MacOS, Android, iOS, device_attribute { field, op, value } with field one of device_name, operating_system, label, serial_number, device_class, architecture, manufacturer, model, mainboard, cpu, gpu, agent_version, last_active_user, timezone, antivirus_solution and op one of eq, contains, starts_with, wildcard, regex; service { field, op, value, status } with field name or display_name, op eq, contains or wildcard and status any, running or stopped (a Linux unit matches with or without .service); application { op, value, versionOp?, version? } with versionOp eq, gte, lt or starts_with; custom_field { key, op, value? } with key the field key of a custom field definition and op eq, contains, regex or is_empty. Each entry may carry negate: true. At most one non-negated condition per entity type. A location or group condition may send locationIds[] / groupIds[] (up to 500) instead of locationId / groupId — the condition holds when the device is in any of them, negated when it is in none of them. Send either the single id or the list; both answer 400, and so do deviceIds and tenantIds. A list with one id is stored as the single id. Every id has to be inside the token's tenant scope (otherwise 404), all ids of one condition have to belong to the same tenant and lie inside the tenant or location the rule names elsewhere (otherwise 400). A pattern that does not compile, an unknown platform, field, operator or status, a missing value or version, a key no definition carries or a secret field answers 400 invalid_parameter with the entry named. Service, application and custom field conditions are evaluated on the server against the device's last inventory upload (see Chapter 5.2.2).
  • A rule needs at least one action: policyName (an existing policy; on /v1/policies/{id}/assign the route names it), sensorIds or jobIds; a body with none of them answers 400. sensorIds and jobIds are lists of existing ids; an unknown id, or a sensor the SNMP discovery manages, answers 400 invalid_parameter naming the field. The items are added to every matching device on top of its policy and are handed out only to devices that have a policy. enabled defaults to true. priority defaults to the most specific organisation condition — device 100, internal IP 200, domain 300, group 400, location 500, tenant 600, otherwise 700 — and may be any number from 0.
  • Every entity a condition names has to be inside the token's tenant scope; one outside answers 404, as reading it would. A rule with no non-negated entity condition matches in every tenant and is accepted only when the token's scope covers every tenant of the instance.
  • PATCH has merge-patch semantics on name, description, policyName, enabled, priority, conditions, sensorIds and jobIds: a field left out stays as it is; conditions, sensorIds and jobIds each replace their whole list ([] empties it). policyName: null removes the policy; it is refused when the rule would be left without any action.
  • The answer of a create or change is { id, created | updated, policyName, sensorIds[], jobIds[], enabled, priority, conditions[] } with the conditions in the read shape (entity names resolved). A create answers 201; send an Idempotency-Key header to make a retry safe.
  • Every write marks the affected devices out of sync and queues an acceleration request, so online devices re-sync right away.

The single-condition body of the first version is still accepted. { "condition": "tenant" | "location" | "group" | "device" | "internal-ip" | "domain", "tenantId" | "locationId" | "groupId" | "deviceId" | "value": … } is translated into one condition and stored in the list form; the answer is the list form. A request that sends both conditions and the single-condition fields answers 400. On PATCH, the single-condition fields replace the whole condition list with that one condition — a PATCH { "tenantId": 7 } on a rule with three conditions leaves it with the one tenant condition; send conditions to change one entry of a list.

Policies. POST /v1/policies creates a policy with the same default settings the console's policy editor starts with; the answer is { id, created, defaultsApplied }. defaultsApplied is false when the web console has not published its defaults yet — the policy is then created without settings. On PUT /v1/policies/{id}/settings, a section that is left out or null takes the default setting instead of being cleared; the answer lists those sections in sectionsDefaulted (field names, like sectionsSet). Only if the web console has not published its defaults yet does such a section stay unset; sectionsCleared counts those.

DELETE /v1/policies/{id} deletes the rules that only assign the policy and keeps the rules that also add sensors or jobs, without the policy; it answers { id, deleted, assignmentsDeleted, assignmentsKept }. DELETE /v1/sensors/{id} and DELETE /v1/jobs/{id} remove the item from every rule that adds it; a rule left without any action is switched off.

GET /v1/devices/{id}/sensors lists the sensors the device receives: those of its policy and those the matching automation rules add, the latter only when the device has a policy and only for the device's platform. Every entry carries source, policy or automation.

GET /v1/devices/{id} carries automationState: the decision of the device's last evaluation as { policyName, policySource, automationId, sensorIds, jobIds, matchedAutomationIds, evaluatedAt, handedOutAt }, or null before the first evaluation. policySource is automation, default or none; evaluatedAt is updated by every evaluation, handedOutAt only when the device fetched its policy. The field is left out of list rows and honours ?fields=.

Example: give every Windows device of tenant 5 the policy Windows base, ahead of the tenant's other rules.

curl -X POST "https://server.example.com/v1/automations" \
  -H "Authorization: Bearer nlk_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Windows in ACME","policyName":"Windows base","priority":550,"conditions":[{"type":"tenant","tenantId":5},{"type":"platform","values":["Windows"]}]}'

X.9.6 Labels of scripts, jobs and sensors

Scripts, jobs and sensors carry a free-text label (up to 64 characters, null when none is set), which the console groups its lists by.

  • GET /v1/scripts, /v1/jobs and /v1/sensors, the single reads and GET /v1/devices/{id}/sensors return label; it honours ?fields=.
  • The lists filter with ?label= (exact, case-insensitive), and ?search= also matches the label. Scripts and jobs sort with sort=label.
  • POST /v1/scripts, /v1/jobs and /v1/sensors accept label. On PATCH, a body without label leaves it unchanged; "" or null removes it. A longer value or a value that is not a string answers 400 invalid_parameter.
  • The label is not part of the document the agents receive: a PATCH that changes only the label does not mark any device out of sync.

X.9.7 Users and roles

Console accounts either follow a role (a named permission set, see Chapter 14.4) or carry custom permissions.

  • GET /v1/users and GET /v1/users/{id} (users:read) return role as { "id": 3, "name": "Technician", "builtIn": false }, or null for custom permissions, and roleOrigin with the name of the role a custom account was detached from (null when it never followed one). The list filters with ?role=<name>, ?role=custom and ?roleId=<int>, and sort=role sorts by the role name. Both fields honour ?fields=.
  • GET /v1/roles and GET /v1/roles/{id} (users:read) list the roles with id, name, description, builtIn, memberCount, createdAt and updatedAt; the single read also carries permissions, the same object shape as GET /v1/users/{id}/permissions. The list filters with ?builtIn= and ?search=.
  • POST /v1/users takes roleId instead of the former role value. With roleId the account starts with that role's permissions and follows it; because that grants permissions, the token needs users:permissions:write in addition to users:write, and a role that holds a permission the bound account does not answers 409 permission_escalation_forbidden. Without roleId the account has custom permissions and none of them, as before. The response carries role and roleOrigin.
  • PUT /v1/users/{id}/role (users:permissions:write) with { "roleId": 3 } binds the account: its permissions become the role's permission object at once and every later change of the role applies to it. { "roleId": null } detaches the account; it keeps its permissions as custom permissions and roleOrigin names the role. An unknown roleId answers 404; the self-edit and escalation refusals of the permissions write apply.
  • PUT /v1/users/{id}/permissions detaches an account that follows a role: the permissions written become its own, role is null afterwards and roleOrigin names the role.
  • PATCH /v1/users/{id} no longer accepts role. Disabling, deleting or demoting the last enabled account that holds users_enabled, users_manage and users_edit — through PATCH with enabled: false, DELETE, the permissions write or a role without those three keys — answers 409 last_user_manager.
  • Breaking change for older clients: role was a lower-case string and is now an object or null; POST and PATCH reject the old role field as unknown.

X.9.8 Device country

The server resolves the country of a device's external address (ipAddressExternal) at every check-in against its bundled GeoIP database and stores it with the device (see Chapter 3.1 — Country of a device). No lookup leaves the installation.

  • GET /v1/devices, GET /v1/devices/{id}, GET /v1/mobile-devices, GET /v1/mobile-devices/{id} and GET /v1/enrollment/pending return geoCountryCode (ISO 3166-1 alpha-2, upper case) and geoCountryName (the English name from the database). Both are null for a device that connects through a private address, for an address the database has no country for, and for a device that has not checked in since the server was updated. On the device and mobile device reads they honour ?fields=.
  • GET /v1/devices filters with ?geoCountryCode=DE (exact, one code) and sorts with sort=geoCountryCode or sort=-geoCountryCode.
  • The fields are read-only; the country cannot be set through the API.
  • The columns come with upgrade section 119 of the Web Console. Until the Web Console has run it, the device reads of an updated server answer with an error, so update the Web Console before the server.

When the country restriction of the server is active, API clients are admitted by the country of their address like every other client; the token does not exempt them.

X.9.9 Server sensors and event types

Server sensors (category 12) are evaluated by the server itself; see Chapter 8.3 — Server sensors.

  • GET /v1/sensors and GET /v1/sensors/{id} return them with serverSensor { allTenants, triggers }; for every other sensor serverSensor is null. The triggers depend on the sub-category:
    • 0 Unauthorized device: new_device, reinstall, hwid_changed, license_limit.
    • 2 Device country: outside_countries, refused, country_changed.
  • The tenants a server sensor covers are not returned; allTenants tells whether it covers every tenant, including tenants created later.
  • Server sensors are read-only through the API: POST, PUT and PATCH do not create or change them, DELETE /v1/sensors/{id} removes one. Create and change them in the Web Console.

GET /v1/events filters with ?type= on the numeric event type: 0 antivirus, 1 job, 2 sensor, 4 device uptime, 5 website uptime, 6 port scanner, 7 SNMP discovery, 8 remote shell, 10 patch management, 11 device authorization (written by server sensors of the kind Unauthorized device), 12 device country (written by server sensors of the kind Device country), 13 rate limiting (written by the server when it blocks a device, or records in observe mode that it would; see A.4.1b). Events of server sensors carry the id of the sensor that wrote them in sensorId. Address blocks of rate limiting name no device and are not returned by the API, and the event.created webhook does not deliver them.

Every event carries date, the time the server received it, and since release 3.3.0.6 also occurredAt, the time it was created on the device as reported by the agent, as UTC ISO 8601. occurredAt is null for events of older agents, for events the server wrote itself and when the reported time lies more than 30 days before or more than 5 minutes after the receipt. date keeps its meaning in /v1/events, /v1/events/{id}, /v1/devices/{id}/events, in ?fields= and in the webhooks; sorting, retention and notifications follow date as before.

X.9.10 Patch jobs and scheduled patch runs

The API reads both kinds of entries of the Patch history tab (see Chapter 7.6). All routes need the scope patch:read. An entry whose device is outside the tenants or locations of the token answers 404.

Patch jobs. GET /v1/patch-jobs, GET /v1/patch-jobs/{id} and GET /v1/patch-jobs/{id}/items return the jobs an operator started by hand (Patch Now and uninstall) with their items. The list filters with ?deviceId=, ?status= and ?action=. Scheduled runs are not part of it.

Scheduled patch runs. The runs an agent started on its own from the patch schedule of its policy. Agents 3.3.0.5 and newer report them.

  • GET /v1/patch-runs lists them newest first, paginated with ?page= and ?pageSize= (default 50, at most 500). The answer carries items, page, pageSize, total, totalPages and hasMore.
  • Filters: ?deviceId=, ?kind=, ?outcome=, and ?from= and ?to= as RFC 3339 timestamps with an offset, compared against runAt. A timestamp without an offset answers 400.
  • GET /v1/patch-runs/{id} returns one run. id is the numeric run id used by all /patch-runs routes.
  • GET /v1/patch-runs/{id}/items returns { "runId": <id>, "items": [...] } with every update the run looked at. A skipped run has no items.

A run carries:

FieldMeaning
idThe numeric run id.
agentRunIdThe GUID the agent generated for the run.
deviceId, deviceNameThe device.
kindrun when the run reached the installation, skip when no run started.
outcomeFor run: ran, nothing_pending or reboot_pending. For skip: disk_space, informational, disabled, no_settings or window_invalid.
noteThe agent's note on the run, in English.
runAt, finishedAtStart and end of the run in UTC.
runAtLocalThe start on the device's own clock, as text.
inWindow, catchUpWhether the run started inside the maintenance window, or as a catch-up of a missed one.
windowStart, windowEndThe maintenance window of the policy in device time (HH:mm).
installedCount, failedCount, heldBackCount, itemCountThe counts of the run.
rebootRequiredThe run left a restart outstanding.
appStageSkippedWinget and Chocolatey were not checked because Windows updates wait for a restart.
itemsTruncatedThe agent reported only part of the updates; failed updates come first.
durationSeconds, agentVersion, receivedAtDuration, agent version and the time the server received the run.

An item carries id, updateId, updateSource (OS-Mandatory, Winget, Chocolatey, Apt, Dnf, Yum or MacOS), name, status, reason and detail. status is installed, failed, restart_pending, held_back or skipped. reason is the code behind the reason column of the console, for example not_approved, severity_filter, type_filter, ring_until, retry_wait, user_input, deferred, framework_package, no_upgrade, not_outdated, replaced, kept_back, held or unresolvable. detail holds the error text, including the Windows Update error code, or for ring_until and retry_wait the UTC time the update becomes due.

Runs are deleted by the retention of the Patch history (90 days by default). The tables come with upgrade section 121 of the Web Console, so update the Web Console before the server.

X.9.11 Tenants of a token and provisioning

Which tenants a token has. Settings → API tokens offers two ways to set the tenants of a token:

  • With the switch All tenants of the account off, the token uses the tenants selected in its dialog and, in addition, every tenant it created itself through POST /v1/tenants. Tenants the account gets later are not included.
  • With the switch on, the token uses every tenant its account is assigned to, including tenants assigned or created later. The selection in the dialog is then not an input.

In both cases the account is the upper limit: a tenant that is taken from the account is gone for its tokens as well, and a tenant that no longer exists is not part of any token. Changes made in the console reach the API within five seconds.

GET /v1/me shows the result: tenantIds are the tenants the token can use right now, tenantScope is account with the switch on and selected otherwise, and createdTenantIds lists the tenants among tenantIds that the token created itself.

Tenants a token created. POST /v1/tenants assigns the new tenant to the account the token is bound to and records the token with the tenant. The same token can use the tenant from its next request on. This applies to tenants created after the update; a tenant created earlier is used by a token only when it is selected for it or when the token uses all tenants of its account.

  • The edit dialog of the token marks these tenants as created by this token and preselects them. Deselecting one and saving withdraws the access; selecting it again grants it like any other tenant of the account.
  • Rotating a token keeps the access, because the token stays the same entry. A newly issued token does not inherit it; select the tenants for the new token.
  • Other tokens of the same account do not get the tenant automatically. They see it when it is selected for them or when they use all tenants of the account.
  • Webhook subscriptions, notification targets and reports that copy the token's tenants when they are created keep that list; they do not grow with the token.
  • A token that is narrowed to locations stays narrowed: it does not see a location it creates in a new tenant.

Provisioning with one token. A tenant, its locations and groups can be created and existing devices moved into them by the same token, one request after the other:

  1. POST /v1/tenants with { "name": "..." } returns the id of the tenant.
  2. POST /v1/locations with { "tenantId": <id>, "name": "..." }.
  3. POST /v1/groups with { "tenantId": <id>, "locationId": <id>, "name": "..." }.
  4. POST /v1/devices/bulk/move with { "ids": [...], "locationId": <id>, "groupId": <id> }, see X.9.12.

The token needs the scopes organization:write and devices:write, and organization:read and devices:read to read back what it wrote. The bound account needs tenants_enabled, tenants_manage, tenants_add, tenants_locations_add, tenants_groups_add, devices_general and devices_move. The tenant the devices come from has to be one of the token's tenants as well: the device and the target of a move both have to be inside the token's scope. For an automation, bind the token to an account of its own that holds only these permissions and tenants.

The two columns behind this come with upgrade section 127 of the Web Console, so update the Web Console before the server. A server that runs before the Web Console has been updated evaluates tokens as before, and a tenant created through the API in that time is not recorded with its token; the server picks the columns up within a minute after the Web Console has been updated, without a restart.

X.9.12 Device lists: filters, sorting and moves

Text filters. GET /v1/devices filters by deviceName, label, operatingSystem, domain, lastActiveUser, ipAddressInternal, serialNumber and agentVersion:

  • A value without a wildcard has to match the whole field. Case and accents are ignored.
  • * stands for any number of characters and ? for exactly one: deviceName=web-* (starts with), deviceName=*-prod (ends with), deviceName=*web* (contains), deviceName=web-???? (four characters after the prefix).
  • \*, \? and \\ stand for the character itself. % and _ have no special meaning.
  • A value may be at most 255 characters long and contain at most four *; anything else answers 400 invalid_parameter and names the parameter.

All filters combine with AND, also with the existing ones. platform, deviceClass and geoCountryCode are compared exactly and take no wildcards. search looks for its text anywhere in deviceName, label, serialNumber or ipAddressInternal and takes no wildcards either.

GET /v1/mobile-devices has the text filters deviceName and label. GET /v1/tenants, GET /v1/locations and GET /v1/groups have the text filter name, for example to look a tenant up before creating it.

Sorting. GET /v1/devices sorts by id, deviceName, label, lastAccess, platform, tenantName, locationName, groupName, operatingSystem, serialNumber, agentVersion, cpuUsage, ramUsage and geoCountryCode; a leading - sorts descending, several fields are separated by commas. id follows the order in which the devices were first registered — there is no first-seen timestamp — so sort=-id returns the most recently registered device first. Devices with equal sort values keep a fixed order, so consecutive pages neither repeat nor skip a device.

The highest name with a prefix, without reading the list:

GET /v1/devices?deviceName=web-*&sort=-deviceName&pageSize=1&fields=id,deviceName

total in the answer is the number of devices that match.

Moving devices. PATCH /v1/devices/{id} and POST /v1/devices/bulk/move move a device with locationId, groupId or both. A move needs the permission devices_move on the bound account.

  • locationId moves the device to that location and to the tenant the location belongs to. A location of another tenant therefore moves the device into that tenant. The group of the device is cleared, unless the device is already in that location.
  • groupId moves the device into that group and with it into the group's location and tenant. Sent together with locationId, the group has to belong to that location; otherwise the answer is 400 invalid_parameter.
  • PATCH with "groupId": null takes the device out of its group and leaves the rest as it is.
  • The bulk form accepts tenantId as well. It is optional and has to be the tenant of the target location.
  • The device and the target both have to be inside the token's scope. A target outside it answers 400 invalid_parameter, a device outside it 404 (in the bulk form: not_found for that item).
  • A target in a tenant that is being deleted answers 409 conflict. An id that is not an integer answers 400 invalid_parameter.

The agent keeps its assignment after a move; it does not move the device back at its next check-in.