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:readordevices: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 accountthey 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 answers404. See X.9.11. - A rate limit in requests per minute (default 600). Requests above it answer
429with aRetry-Afterheader. 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" }
]
}sourcenames who wrote the device value last:manual(console),script(write-back from a script) orapi.isSecretistruefor aSecretfield; itsvalueis alwaysnull.- 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 }
]
}levelnames where the value comes from:device,group,location,tenant,global, ornonewhen no level holds a value.sourceis set only whenlevelisdevice.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:
| Level | Routes | Visible when |
|---|---|---|
| Global | GET, PUT /v1/custom-fields/global | Always; needs settings_custom_fields_enabled |
| Tenant | GET, PUT /v1/tenants/{id}/custom-fields | The tenant is in the token's tenant scope |
| Location | GET, PUT /v1/locations/{id}/custom-fields | The location is in the token's tenant scope and, when the token is limited to locations, in that list |
| Group | GET, PUT /v1/groups/{id}/custom-fields | As 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
Manualfields of typeText,MultilineorSecretare accepted, and a value may not exceed 64 KB. Any other entry answers400 invalid_parameterwith the reason, and nothing of the request is written. nullclears 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. updatedByis 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"
}conditionsis the rule's condition list as stored. Every entry hastypeandnegate; an entity condition carries its id (deviceId,tenantId,locationIdorgroupId) and the entity'sname;internal_ipanddomaincarryvalue;platformcarriesvalues;device_attributecarriesfield,opandvalue.- A
locationorgroupcondition additionally carrieslocationIds/groupIdsandnamesin the same order (nullfor an entity that is gone or outside the token's tenants). It may name several entities ("is one of"); thenlocationId/groupIdandnamearenulland 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. policyNameisnullfor a rule that assigns no policy.sensorIdsandjobIdsare the sensors and jobs the rule adds to every matching device on top of its policy.deviceId,tenantId,locationIdandgroupIdon 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 intenantId;effectiveTenantIdis the tenant the rule belongs to, ornullfor 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 }
]
}conditionsneeds 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[] }withWindows,Linux,MacOS,Android,iOS,device_attribute { field, op, value }withfieldone ofdevice_name,operating_system,label,serial_number,device_class,architecture,manufacturer,model,mainboard,cpu,gpu,agent_version,last_active_user,timezone,antivirus_solutionandopone ofeq,contains,starts_with,wildcard,regex;service { field, op, value, status }withfieldnameordisplay_name,opeq,containsorwildcardandstatusany,runningorstopped(a Linux unit matches with or without.service);application { op, value, versionOp?, version? }withversionOpeq,gte,ltorstarts_with;custom_field { key, op, value? }withkeythe field key of a custom field definition andopeq,contains,regexoris_empty. Each entry may carrynegate: true. At most one non-negated condition per entity type. Alocationorgroupcondition may sendlocationIds[]/groupIds[](up to 500) instead oflocationId/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 answer400, and so dodeviceIdsandtenantIds. A list with one id is stored as the single id. Every id has to be inside the token's tenant scope (otherwise404), all ids of one condition have to belong to the same tenant and lie inside the tenant or location the rule names elsewhere (otherwise400). 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 answers400 invalid_parameterwith 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}/assignthe route names it),sensorIdsorjobIds; a body with none of them answers400.sensorIdsandjobIdsare lists of existing ids; an unknown id, or a sensor the SNMP discovery manages, answers400 invalid_parameternaming 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.enableddefaults totrue.prioritydefaults 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. PATCHhas merge-patch semantics onname,description,policyName,enabled,priority,conditions,sensorIdsandjobIds: a field left out stays as it is;conditions,sensorIdsandjobIdseach replace their whole list ([]empties it).policyName: nullremoves 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 answers201; send anIdempotency-Keyheader 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/jobsand/v1/sensors, the single reads andGET /v1/devices/{id}/sensorsreturnlabel; it honours?fields=.- The lists filter with
?label=(exact, case-insensitive), and?search=also matches the label. Scripts and jobs sort withsort=label. POST /v1/scripts,/v1/jobsand/v1/sensorsacceptlabel. OnPATCH, a body withoutlabelleaves it unchanged;""ornullremoves it. A longer value or a value that is not a string answers400 invalid_parameter.- The label is not part of the document the agents receive: a
PATCHthat 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/usersandGET /v1/users/{id}(users:read) returnroleas{ "id": 3, "name": "Technician", "builtIn": false }, ornullfor custom permissions, androleOriginwith the name of the role a custom account was detached from (nullwhen it never followed one). The list filters with?role=<name>,?role=customand?roleId=<int>, andsort=rolesorts by the role name. Both fields honour?fields=.GET /v1/rolesandGET /v1/roles/{id}(users:read) list the roles withid,name,description,builtIn,memberCount,createdAtandupdatedAt; the single read also carriespermissions, the same object shape asGET /v1/users/{id}/permissions. The list filters with?builtIn=and?search=.POST /v1/userstakesroleIdinstead of the formerrolevalue. WithroleIdthe account starts with that role's permissions and follows it; because that grants permissions, the token needsusers:permissions:writein addition tousers:write, and a role that holds a permission the bound account does not answers409 permission_escalation_forbidden. WithoutroleIdthe account has custom permissions and none of them, as before. The response carriesroleandroleOrigin.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 androleOriginnames the role. An unknownroleIdanswers404; the self-edit and escalation refusals of the permissions write apply.PUT /v1/users/{id}/permissionsdetaches an account that follows a role: the permissions written become its own,roleisnullafterwards androleOriginnames the role.PATCH /v1/users/{id}no longer acceptsrole. Disabling, deleting or demoting the last enabled account that holdsusers_enabled,users_manageandusers_edit— throughPATCHwithenabled: false,DELETE, the permissions write or a role without those three keys — answers409 last_user_manager.- Breaking change for older clients:
rolewas a lower-case string and is now an object ornull;POSTandPATCHreject the oldrolefield 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}andGET /v1/enrollment/pendingreturngeoCountryCode(ISO 3166-1 alpha-2, upper case) andgeoCountryName(the English name from the database). Both arenullfor 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/devicesfilters with?geoCountryCode=DE(exact, one code) and sorts withsort=geoCountryCodeorsort=-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/sensorsandGET /v1/sensors/{id}return them withserverSensor { allTenants, triggers }; for every other sensorserverSensorisnull. The triggers depend on the sub-category:0Unauthorized device:new_device,reinstall,hwid_changed,license_limit.2Device country:outside_countries,refused,country_changed.
- The tenants a server sensor covers are not returned;
allTenantstells whether it covers every tenant, including tenants created later. - Server sensors are read-only through the API:
POST,PUTandPATCHdo 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-runslists them newest first, paginated with?page=and?pageSize=(default 50, at most 500). The answer carriesitems,page,pageSize,total,totalPagesandhasMore.- Filters:
?deviceId=,?kind=,?outcome=, and?from=and?to=as RFC 3339 timestamps with an offset, compared againstrunAt. A timestamp without an offset answers400. GET /v1/patch-runs/{id}returns one run.idis the numeric run id used by all/patch-runsroutes.GET /v1/patch-runs/{id}/itemsreturns{ "runId": <id>, "items": [...] }with every update the run looked at. A skipped run has no items.
A run carries:
| Field | Meaning |
|---|---|
id | The numeric run id. |
agentRunId | The GUID the agent generated for the run. |
deviceId, deviceName | The device. |
kind | run when the run reached the installation, skip when no run started. |
outcome | For run: ran, nothing_pending or reboot_pending. For skip: disk_space, informational, disabled, no_settings or window_invalid. |
note | The agent's note on the run, in English. |
runAt, finishedAt | Start and end of the run in UTC. |
runAtLocal | The start on the device's own clock, as text. |
inWindow, catchUp | Whether the run started inside the maintenance window, or as a catch-up of a missed one. |
windowStart, windowEnd | The maintenance window of the policy in device time (HH:mm). |
installedCount, failedCount, heldBackCount, itemCount | The counts of the run. |
rebootRequired | The run left a restart outstanding. |
appStageSkipped | Winget and Chocolatey were not checked because Windows updates wait for a restart. |
itemsTruncated | The agent reported only part of the updates; failed updates come first. |
durationSeconds, agentVersion, receivedAt | Duration, 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 accountoff, the token uses the tenants selected in its dialog and, in addition, every tenant it created itself throughPOST /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 tokenand 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:
POST /v1/tenantswith{ "name": "..." }returns theidof the tenant.POST /v1/locationswith{ "tenantId": <id>, "name": "..." }.POST /v1/groupswith{ "tenantId": <id>, "locationId": <id>, "name": "..." }.POST /v1/devices/bulk/movewith{ "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 answers400 invalid_parameterand 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,deviceNametotal 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.
locationIdmoves 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.groupIdmoves the device into that group and with it into the group's location and tenant. Sent together withlocationId, the group has to belong to that location; otherwise the answer is400 invalid_parameter.PATCHwith"groupId": nulltakes the device out of its group and leaves the rest as it is.- The bulk form accepts
tenantIdas 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 it404(in the bulk form:not_foundfor that item). - A target in a tenant that is being deleted answers
409 conflict. An id that is not an integer answers400 invalid_parameter.
The agent keeps its assignment after a move; it does not move the device back at its next check-in.
Related
- Chapter 5 — Automations — conditions, priority, evaluation.
- Chapter 8.4 — Custom Fields — definitions, levels, secrets.
- Script variables and custom fields in scripts.
- Write custom field values from a script.
- X.2 — Permission reference — the permissions the scopes map to.