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, a subset of the account's tenants, optionally narrowed to a list of locations. A device, tenant, location or group outside that scope answers
404. - 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.
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.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;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. 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.
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"]}]}'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.