Developers

The Denwen RMM API

Connect your own tools to Denwen RMM with an API key your administrator issues. The API is described as OpenAPI 3.1.0, document version 1.4.0.

35 published operations, 18 webhooks. The machine-readable document: /api/openapi.json

Overview

The Denwen RMM API: the documented, supported subset of the routes an API key can call.

Every path below is relative to your Denwen RMM instance: on the hosted service that is https://rmm.denwen.co.uk, so the base URL of the API is https://rmm.denwen.co.uk/api. Requests and responses are JSON. The whole API is described by one OpenAPI 3.1 document, served without authentication at /api/openapi.json on every instance, which you can load into Postman, an OpenAPI client generator or any other tool that reads the format.

What is supported

Only the operations in this document are supported. An API key can reach other console routes, but those are undocumented on purpose (console plumbing, shapes not yet committed, high-risk actions) and may change or disappear without notice.

Authentication

Send a key as a bearer token. Two kinds exist: standard keys (apiKey) and integration keys (integrationKey). See the security schemes for what each may reach.

Standard API keys

A standard API key, created in Administration > API Keys and shown once. Send it as Authorization: Bearer <your key>. A key acts as the role chosen when it was created, which can never exceed the role of the operator who created it. A read key may use safe methods (GET, HEAD, OPTIONS) only; a full key may also write. Some areas refuse every API key whatever its role, because a credential there could mint another credential or redirect sign-in: /api/admin/api-keys, /api/admin/users, /api/admin/roles, /api/admin/sso, /api/admin/security-policy, /api/admin/smtp, /api/vulnerabilities/nvd-key, /api/admin/platform-settings, /api/admin/licence, /api/admin/backups, /api/admin/data-protection, /api/session-consent, /api/support-access, /api/auth/invite, /api/auth/register, /api/auth/oidc, /api/auth/mfa, /api/auth/passkeys, /api/auth/sessions, /api/auth/password, /api/auth/temp-code, /api/auth/resume-code, /api/auth/account, /api/admin/integrations, /api/auth/integrations, /api/ai/activity. These are read-only to a key: /api/tenants, /api/admin/tenants, /api/customers, /api/hierarchy. A standard key is refused under /api/integrations/.

Key format: dwn_<key id>_<secret>. Every request carries it in one header:

Authorization: Bearer <your key>

Tenancy

Every key created by an organisation's operator belongs to that organisation and sees only that organisation's customers, sites and devices. A key created by a provider (global) operator belongs to no organisation and reaches platform administration only (for example the provider-level alert rules), never an organisation's data. Everywhere else it is refused 403 TENANT_REQUIRED. Sending X-Denwen-Tenant: <organisation id> does not change that: it is refused 403 PROVIDER_ACCESS_REQUIRED, because an organisation grants provider access to named staff for a limited time, never to a key. Where the server restricts provider credentials to approved network addresses, a provider key from any other address is refused 403 PROVIDER_IP_REFUSED. To read an organisation's data, use a key created inside that organisation. A resource outside the key's reach answers 404, not 403, so its existence is not disclosed.

Denwen MSP integration keys

An integration key, minted by pairing an integration (Administration > Integrations), never by hand. It reaches only /api/integrations/denwen-msp/ (and the original /api/integrations/flowmsp/, served identically) and nothing else. It may read, and may POST to exactly /api/integrations/denwen-msp/v1/launch, /api/integrations/denwen-msp/v1/connectability, /api/integrations/denwen-msp/v1/alert-tickets, /api/integrations/denwen-msp/v1/actions, /api/integrations/denwen-msp/v1/customer-refs (launch and connectability also require a signed launch assertion, actions a signed action assertion; alert-tickets and customer-refs need none); every other write is refused whatever the key's scope.

The integration’s operations are listed under Denwen MSP integration, and the events it receives under Webhooks.

Rate limits

Integration keys: 120 requests per integration per fixed 60 s window. POST /v1/launch is further limited to 12 per Denwen MSP user and 60 per integration, and POST /v1/connectability to 30 and 120, per window. Responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; a 429 carries Retry-After. Standard keys: no per-key limit is enforced yet. Clients must still handle 429 (and honour Retry-After when present), because one will be added.

Errors

401 means no credential was recognised: the Authorization header is missing, or it carries a key (dwn_…) that is malformed, unknown or has the wrong secret (these are deliberately indistinguishable). A bearer that is not shaped like a key at all, and is not a valid console session token, answers 403 Forbidden instead. Otherwise 403 means the key was recognised but cannot do this: it is revoked or expired, it is read-only and the request writes, the area refuses API keys, the key's role lacks the permission, or it is a provider key outside platform administration (TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED; see Tenancy). Standard routes answer the LegacyError body (read error); integration routes answer the ContractError envelope. Fields may be added to any response.

Standard error body (LegacyError)

The standard API error body. Read error; other fields are optional and additive.

Fields of the standard error body
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.

Integration error envelope (ContractError)

The Denwen MSP integration error envelope (contract §9).

Fields of the integration error envelope
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.

Versioning and deprecation

This document is versioned on its own (semver, currently 1.4.0). Standard paths are unversioned. A breaking change to a stable operation is announced with Deprecation, Sunset and Link response headers at least 90 days ahead. Operations marked x-stability: beta may change in a minor version. Integration paths keep /v1 and carry x-contract-version.

Reference

Denwen MSP integration

The Denwen MSP integration API and its outbound webhooks (integration key; contract 1.10.0). Its operations are listed under /api/integrations/denwen-msp/v1. The original prefix /api/integrations/flowmsp/v1 (Denwen MSP was formerly FlowMSP) is still served, identically (same routes, key, checks and limits), for existing integrations; GET /ping lists both in features.pathPrefixes.

POST/api/integrations/denwen-msp/v1/actions

Run an action on a device from a ticket

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspCreateAction

Runs a ticket action (script, reboot, patch_scan, and diagnostics where listed in ping.features.actions.kinds) as the Denwen MSP user's LINKED RMM user, with that user's own permission at the device's site. Needs BOTH the integration key AND an action assertion (typ: "denwen-action+jwt", purpose: "action") whose signature covers the exact action, the ticket and the idempotencyKey (contract §18.3); a leaked key alone cannot run anything. An action never creates an account link. script, reboot and diagnostics need trusted Denwen MSP MFA within features.actions.mfaMaxAgeMin. Only offered library scripts run; there is no free-form script. Answers 202 with the action queued, or 200 with replayed: true for a repeat of the same idempotencyKey, sub, device and action. The result arrives as the action.completed webhook. Time out after 5 s and retry with the same idempotencyKey and a new assertion. Limits: 10 per Denwen MSP user and 30 per integration per 60 s, on their own budget, and 6 per device per 10 minutes (contract §18.4, §18.9).

Request body
Body of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
assertion requiredstringA compact JWS signed by Denwen MSP's registered key with header typ: "denwen-action+jwt" and purpose: "action", carrying the action, ticket and idempotencyKey claims (contract §18.3). At most 8192 bytes.
Responses
200

An idempotent repeat: the existing action, nothing dispatched again.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
action requiredContractActionA ticket action (contract §18.6).
action.consoleUrl requiredstring | nullThe device in the RMM console; link it only when it starts with the pinned base URL.
action.createdAt requiredstring (date-time)When the action was accepted.
action.device requiredobject
action.device.customerId requiredinteger
action.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
action.device.id requiredinteger
action.device.siteId requiredinteger
action.finishedAt requiredstring (date-time) | null
action.id requiredstring
action.idempotencyKey requiredstring
action.kind required"script" | "reboot" | "patch_scan" | "diagnostics"Open set on the wire: show an unknown kind generically.
action.reboot requiredobject | null
action.reboot.backOnlineAt requiredstring (date-time) | null
action.reboot.noticeSeconds requiredintegerThe notice actually given.
action.reboot.userLoggedOn requiredboolean
action.refusal requiredenum (25 values) | nullSet only for refused and abandoned. Open set.
action.requestedBy requiredobject
action.requestedBy.email requiredstring
action.requestedBy.flowmspSub requiredstringThe Denwen MSP user (sub) who asked. (Denwen MSP, formerly FlowMSP: the field name keeps its contract v1 name, and the RMM sends only this name; it never reads it from a request, where sub comes from the signed action assertion.)
action.requestedBy.rmmUserId requiredintegerThe LINKED RMM user the action runs as.
action.result requiredobject | nullNull until a terminal state other than refused.
action.result.exitCode requiredinteger | null
action.result.redactions requiredintegerHow many credential-looking spans were replaced before sending.
action.result.stderr requiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days.
action.result.stdout requiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days.
action.result.summary requiredstring
action.result.truncated requiredboolean
action.script requiredobject | null
action.script.id requiredinteger
action.script.name requiredstring
action.script.version requiredinteger
action.startedAt requiredstring (date-time) | null
action.state required"queued" | "running" | "succeeded" | "failed" | "timed_out" | "refused" | "abandoned"Closed set.
action.ticket requiredobject
action.ticket.id requiredstringDenwen MSP's ticket id.
action.ticket.number requiredstring | nullThe ticket number people use, e.g. 10523.
action.ticket.url requiredstringhttps, with the origin of the integration webhook URL.
contractVersion requiredstring
replayed requiredbooleanTrue on an idempotent repeat (200): the existing action, nothing dispatched again.
202

Accepted and queued.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
202 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
action requiredContractActionA ticket action (contract §18.6).
action.consoleUrl requiredstring | nullThe device in the RMM console; link it only when it starts with the pinned base URL.
action.createdAt requiredstring (date-time)When the action was accepted.
action.device requiredobject
action.device.customerId requiredinteger
action.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
action.device.id requiredinteger
action.device.siteId requiredinteger
action.finishedAt requiredstring (date-time) | null
action.id requiredstring
action.idempotencyKey requiredstring
action.kind required"script" | "reboot" | "patch_scan" | "diagnostics"Open set on the wire: show an unknown kind generically.
action.reboot requiredobject | null
action.reboot.backOnlineAt requiredstring (date-time) | null
action.reboot.noticeSeconds requiredintegerThe notice actually given.
action.reboot.userLoggedOn requiredboolean
action.refusal requiredenum (25 values) | nullSet only for refused and abandoned. Open set.
action.requestedBy requiredobject
action.requestedBy.email requiredstring
action.requestedBy.flowmspSub requiredstringThe Denwen MSP user (sub) who asked. (Denwen MSP, formerly FlowMSP: the field name keeps its contract v1 name, and the RMM sends only this name; it never reads it from a request, where sub comes from the signed action assertion.)
action.requestedBy.rmmUserId requiredintegerThe LINKED RMM user the action runs as.
action.result requiredobject | nullNull until a terminal state other than refused.
action.result.exitCode requiredinteger | null
action.result.redactions requiredintegerHow many credential-looking spans were replaced before sending.
action.result.stderr requiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days.
action.result.stdout requiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days.
action.result.summary requiredstring
action.result.truncated requiredboolean
action.script requiredobject | null
action.script.id requiredinteger
action.script.name requiredstring
action.script.version requiredinteger
action.startedAt requiredstring (date-time) | null
action.state required"queued" | "running" | "succeeded" | "failed" | "timed_out" | "refused" | "abandoned"Closed set.
action.ticket requiredobject
action.ticket.id requiredstringDenwen MSP's ticket id.
action.ticket.number requiredstring | nullThe ticket number people use, e.g. 10523.
action.ticket.url requiredstringhttps, with the origin of the integration webhook URL.
contractVersion requiredstring
replayed requiredbooleanTrue on an idempotent repeat (200): the existing action, nothing dispatched again.
Example response
{
  "action": {
    "consoleUrl": "https://rmm.acme-msp.example/endpoint/1187",
    "createdAt": "2026-11-20T09:00:00.412Z",
    "device": {
      "customerId": 42,
      "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
      "id": 1187,
      "siteId": 310
    },
    "finishedAt": null,
    "id": "act_01M6RS0K55X7AQ28PXWTGBGKGZ",
    "idempotencyKey": "tka_01JD4M9ZQ8V2X0B1C2D3E4F5G6",
    "kind": "script",
    "reboot": null,
    "refusal": null,
    "requestedBy": {
      "email": "sam.taylor@acme-msp.example",
      "flowmspSub": "usr_01JC2Z8Q4M7V9X0B1C2D3E4F5G",
      "rmmUserId": 12
    },
    "result": null,
    "script": {
      "id": 41,
      "name": "Clear print queue",
      "version": 3
    },
    "startedAt": null,
    "state": "queued",
    "ticket": {
      "id": "cm3tkt0nq8",
      "number": "10523",
      "url": "https://msp.example/tickets/cm3tkt0nq8"
    }
  },
  "contractVersion": "1.10.0",
  "replayed": false
}
400

invalid_request: a bad body (details names the field), or a bad claim in the assertion: details.action, details.ticket, details.idempotencyKey, details.parameters.<name> or details.grant; or details.reason: "ticket_url_origin" (the ticket URL is not on the webhook URL's origin).

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
401

unauthorized: the key, or the assertion, with details.reason one of the contract §15.13 assertion reasons (assertion_invalid, unknown_kid, bad_signature, issuer_mismatch, audience_mismatch, purpose_mismatch, assertion_expired, assertion_replayed).

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not a paired integration key or the tenant is suspended; or details.reason one of launch_not_configured, actions_disabled, action_kind_disabled, account_link_required, account_link_conflict, account_not_linkable, missing_permission, outside_site_scope, mfa_required (contract §18.8).

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "forbidden",
    "details": {
      "reason": "mfa_required"
    },
    "message": "This action needs a recent Denwen MSP sign-in with multi-factor authentication."
  },
  "requestId": "req_457caf4bbab2226d29523697"
}
404

not_found: the device does not exist or belongs to another tenant, or the script is not offered to this integration or not visible at the device's site (never 403).

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
404 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
409

forbidden: details.reason one of device_offline, device_not_active, device_pending_approval, device_reclaim_pending, agent_unsupported, action_in_progress, idempotency_conflict, script_changed, script_too_large, script_os_mismatch.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
409 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 10 actions per Denwen MSP user or 30 per integration per 60 s, or 6 per device per 10 minutes. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
503

unavailable: ticket actions are suspended on this RMM (details.reason: "actions_suspended") or not available right now. Retry later.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
503 response of POST /api/integrations/denwen-msp/v1/actions
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl -X POST "https://<your instance>/api/integrations/denwen-msp/v1/actions" \
  -H "Authorization: Bearer <your key>" \
  -H "Content-Type: application/json" \
  -d '{ … }'

GET/api/integrations/denwen-msp/v1/actions/{id}

Get one action

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspGetAction

The current state of an action this integration created; the fallback when action.completed has not arrived by timeoutSec + 5 minutes. result.stdout and result.stderr are null more than 7 days after finishedAt. Counts against the 120/60 s sync budget (contract §18.5).

Parameters
Parameters of GET /api/integrations/denwen-msp/v1/actions/{id}
NameInTypeDescription
id requiredpathstringThe action id, act_ + 26 characters.
Responses
200

The action.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of GET /api/integrations/denwen-msp/v1/actions/{id}
FieldTypeDescription
action requiredContractActionA ticket action (contract §18.6).
action.consoleUrl requiredstring | nullThe device in the RMM console; link it only when it starts with the pinned base URL.
action.createdAt requiredstring (date-time)When the action was accepted.
action.device requiredobject
action.device.customerId requiredinteger
action.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
action.device.id requiredinteger
action.device.siteId requiredinteger
action.finishedAt requiredstring (date-time) | null
action.id requiredstring
action.idempotencyKey requiredstring
action.kind required"script" | "reboot" | "patch_scan" | "diagnostics"Open set on the wire: show an unknown kind generically.
action.reboot requiredobject | null
action.reboot.backOnlineAt requiredstring (date-time) | null
action.reboot.noticeSeconds requiredintegerThe notice actually given.
action.reboot.userLoggedOn requiredboolean
action.refusal requiredenum (25 values) | nullSet only for refused and abandoned. Open set.
action.requestedBy requiredobject
action.requestedBy.email requiredstring
action.requestedBy.flowmspSub requiredstringThe Denwen MSP user (sub) who asked. (Denwen MSP, formerly FlowMSP: the field name keeps its contract v1 name, and the RMM sends only this name; it never reads it from a request, where sub comes from the signed action assertion.)
action.requestedBy.rmmUserId requiredintegerThe LINKED RMM user the action runs as.
action.result requiredobject | nullNull until a terminal state other than refused.
action.result.exitCode requiredinteger | null
action.result.redactions requiredintegerHow many credential-looking spans were replaced before sending.
action.result.stderr requiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days.
action.result.stdout requiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days.
action.result.summary requiredstring
action.result.truncated requiredboolean
action.script requiredobject | null
action.script.id requiredinteger
action.script.name requiredstring
action.script.version requiredinteger
action.startedAt requiredstring (date-time) | null
action.state required"queued" | "running" | "succeeded" | "failed" | "timed_out" | "refused" | "abandoned"Closed set.
action.ticket requiredobject
action.ticket.id requiredstringDenwen MSP's ticket id.
action.ticket.number requiredstring | nullThe ticket number people use, e.g. 10523.
action.ticket.url requiredstringhttps, with the origin of the integration webhook URL.
contractVersion requiredstring
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of GET /api/integrations/denwen-msp/v1/actions/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, or the RMM tenant is suspended. Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of GET /api/integrations/denwen-msp/v1/actions/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
404

not_found: no such action, or another integration created it (never 403).

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
404 response of GET /api/integrations/denwen-msp/v1/actions/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of GET /api/integrations/denwen-msp/v1/actions/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 requests per integration per fixed 60 s window. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of GET /api/integrations/denwen-msp/v1/actions/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of GET /api/integrations/denwen-msp/v1/actions/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
503

unavailable: ticket actions are not available right now. Retry later.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
503 response of GET /api/integrations/denwen-msp/v1/actions/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl "https://<your instance>/api/integrations/denwen-msp/v1/actions/<id>" \
  -H "Authorization: Bearer <your key>"

POST/api/integrations/denwen-msp/v1/alert-tickets

Write ticket links back to alerts

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspAlertTickets

Links 1 to 100 alerts to their Denwen MSP tickets, for the console's ticket badge. A ticket URL must share the origin of the integration webhook URL. A closed or resolved ticket resolves an alert that does not clear itself when resolve-on-close is on. A write-back older than the stored one is ignored_stale. not_found is final (an unknown alert, or its link expired after 90 days). The response is Cache-Control: no-store. Its own budget of 120 per integration per 60 s, not the sync budget (contract §10, §16.6).

Request body
Body of POST /api/integrations/denwen-msp/v1/alert-tickets
FieldTypeDescription
links requiredarray of object
links[].alertId requiredinteger
links[].ticket requiredobject
links[].ticket.id requiredstring
links[].ticket.number requiredstring | null
links[].ticket.status required"open" | "resolved" | "closed"
links[].ticket.updatedAt requiredstring (date-time)The ticket change time; a write-back older than the stored one is ignored.
links[].ticket.url requiredstringMust share the origin of the integration webhook URL.
Responses
200

One result per link, in request order.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of POST /api/integrations/denwen-msp/v1/alert-tickets
FieldTypeDescription
results requiredarray of object
results[].alertId requiredinteger
results[].outcome required"linked" | "updated" | "resolved_alert" | "ignored_stale" | "not_found"not_found: no such alert for this integration, or its link expired (90 days); final, do not retry.
Example response
{
  "results": [
    {
      "alertId": 812,
      "outcome": "resolved_alert"
    },
    {
      "alertId": 813,
      "outcome": "linked"
    },
    {
      "alertId": 815,
      "outcome": "not_found"
    }
  ]
}
400

invalid_request: a bad body (details names the field), or details.reason ticket_url_origin or too_many_links.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of POST /api/integrations/denwen-msp/v1/alert-tickets
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of POST /api/integrations/denwen-msp/v1/alert-tickets
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, the tenant is suspended, or details.reason is alerts_disabled (alerts are off for this integration). Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of POST /api/integrations/denwen-msp/v1/alert-tickets
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of POST /api/integrations/denwen-msp/v1/alert-tickets
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 write-backs per integration per 60 s. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of POST /api/integrations/denwen-msp/v1/alert-tickets
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of POST /api/integrations/denwen-msp/v1/alert-tickets
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl -X POST "https://<your instance>/api/integrations/denwen-msp/v1/alert-tickets" \
  -H "Authorization: Bearer <your key>" \
  -H "Content-Type: application/json" \
  -d '{ … }'

GET/api/integrations/denwen-msp/v1/alerts

List open alerts

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspListAlerts

A snapshot of the open alerts that pass this integration's alert filter (severity, categories and eventTypes), by id ascending, for reconciling after a gap or an integration.resync_suggested naming alerts. Empty while alerts are off. Follow nextCursor until it is null. customerId and siteId filter; another tenant's id returns an empty page (contract §16.5).

Parameters
Parameters of GET /api/integrations/denwen-msp/v1/alerts
NameInTypeDescription
limitquerystring1 to the maximum page size (ping limits.maxPageSize).
cursorquerystringnextCursor from the previous page.
customerIdquerystringOnly alerts of this customer.
siteIdquerystringOnly alerts of this site.
Responses
200

One page of open alerts.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of GET /api/integrations/denwen-msp/v1/alerts
FieldTypeDescription
data requiredarray of ContractAlertOpen alerts that pass the integration alert filter, by id ascending.
data[].asset requiredContractHypervisorAlertAsset | ContractUptimeAlertAsset | null§16.11 (1.8.0): the hypervisor host or website/port check of a hypervisor / uptime alert; null for every other kind.
data[].autoClears requiredbooleanTrue when the RMM clears the alert itself once the condition ends.
data[].category requiredstring
data[].consoleUrl requiredstring | null
data[].description requiredstring | null
data[].device requiredContractDeviceRef | nullAt the time of the event. Null for site_outage, hypervisor and uptime.
data[].device.customerId requiredinteger
data[].device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data[].device.id requiredinteger
data[].device.siteId requiredinteger
data[].id requiredinteger
data[].key requiredstringStable de-duplication key for the condition.
data[].lastOccurredAt requiredstring (date-time)The latest occurrence.
data[].raisedAt requiredstring (date-time)When the alert was raised.
data[].resolvedAt requiredstring (date-time) | null
data[].severity requiredstringwarning or critical today. Open set; read an unknown value as warning.
data[].site requiredobject | nullNull only for hypervisor and uptime (§16.11), which are sent only to an integration that opted in.
data[].site.customerId requiredinteger
data[].site.id requiredinteger
data[].source requiredobject
data[].source.kind requiredenum (9 values)Open set on the wire.
data[].source.ruleId requiredinteger | null
data[].source.ruleName requiredstring | null
data[].state required"open" | "resolved"
data[].title requiredstring
nextCursor requiredstring | nullNull on the last page.
serverTime requiredstring (date-time)The watermark: do not clear an alert learnt of after it merely because no page listed it.
Example response
{
  "data": [
    {
      "asset": null,
      "autoClears": false,
      "category": "Events",
      "consoleUrl": "https://rmm.acme-msp.example/endpoint/1187",
      "description": "Event 4625 (Microsoft-Windows-Security-Auditing) matched rule \"Repeated failed sign-ins\": An account failed to log on. Account name: reception. Failure reason: unknown user name or bad password.",
      "device": {
        "customerId": 42,
        "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
        "id": 1187,
        "siteId": 310
      },
      "id": 812,
      "key": "DWN-XYGW-9KF9-ZBEK-B8N$|eventRule:31:Security:4625",
      "lastOccurredAt": "2026-11-06T15:53:05.561Z",
      "raisedAt": "2026-10-22T09:12:40.000Z",
      "resolvedAt": null,
      "severity": "warning",
      "site": {
        "customerId": 42,
        "id": 310
      },
      "source": {
        "kind": "event_rule",
        "ruleId": 31,
        "ruleName": "Repeated failed sign-ins"
      },
      "state": "open",
      "title": "Repeated failed sign-ins on CD-RECEPTION01"
    },
    {
      "asset": null,
      "autoClears": true,
      "category": "Storage",
      "consoleUrl": "https://rmm.acme-msp.example/endpoint/1187",
      "description": "Rule \"Disk space low\": disk_usage C: is 96.2% (threshold 90%).",
      "device": {
        "customerId": 42,
        "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
        "id": 1187,
        "siteId": 310
      },
      "id": 813,
      "key": "DWN-XYGW-9KF9-ZBEK-B8N$|rule:17:disk_usage:C:",
      "lastOccurredAt": "2026-10-22T09:20:05.000Z",
      "raisedAt": "2026-10-22T09:20:05.000Z",
      "resolvedAt": null,
      "severity": "critical",
      "site": {
        "customerId": 42,
        "id": 310
      },
      "source": {
        "kind": "telemetry_rule",
        "ruleId": 17,
        "ruleName": "Disk space low"
      },
      "state": "open",
      "title": "Disk C: 96% full on CD-RECEPTION01"
    },
    {
      "asset": null,
      "autoClears": true,
      "category": "Connectivity",
      "consoleUrl": null,
      "description": null,
      "device": null,
      "id": 814,
      "key": "site:310|site-outage",
      "lastOccurredAt": "2026-10-22T09:31:00.000Z",
      "raisedAt": "2026-10-22T09:31:00.000Z",
      "resolvedAt": null,
      "severity": "critical",
      "site": {
        "customerId": 42,
        "id": 310
      },
      "source": {
        "kind": "site_outage",
        "ruleId": null,
        "ruleName": null
      },
      "state": "open",
      "title": "Site outage at Harley Street surgery: 4 of 5 endpoints are offline"
    }
  ],
  "nextCursor": null,
  "serverTime": "2026-11-06T15:52:36.438Z"
}
400

invalid_request (a bad parameter, named in details) or invalid_cursor (tampered with, older than 24 hours, or for other filters, resource or client: restart the listing without a cursor, same updatedSince).

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of GET /api/integrations/denwen-msp/v1/alerts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of GET /api/integrations/denwen-msp/v1/alerts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, or the RMM tenant is suspended. Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of GET /api/integrations/denwen-msp/v1/alerts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of GET /api/integrations/denwen-msp/v1/alerts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 requests per integration per fixed 60 s window. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of GET /api/integrations/denwen-msp/v1/alerts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of GET /api/integrations/denwen-msp/v1/alerts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl "https://<your instance>/api/integrations/denwen-msp/v1/alerts" \
  -H "Authorization: Bearer <your key>"

POST/api/integrations/denwen-msp/v1/connectability

Check whether a user can connect to a device

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspConnectability

For the Connect drawer: can this Denwen MSP user connect to this device now, and what would happen? Takes an assertion with purpose: "check" (its mode is ignored). Errors are only those of the assertion checks and 404 for the device; every other state is reported in the body. It never creates a link and never issues or changes a link code. The integration key is read-scoped: it may POST here because this path is in its write allowlist (x-integration-write-allowlist). Limits: 30 per Denwen MSP user and 120 per integration per 60 s (contract §10, §15.11).

Request body
Body of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
assertion requiredstringA compact JWS signed by Denwen MSP's registered key, purpose: "check" (contract §15.3). At most 8192 bytes.
Responses
200

The answer.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
consent requiredobject
consent.agentCanPrompt requiredbooleanFalse with required true: the relay refuses a desktop session until the agent is updated.
consent.required requiredboolean
consent.source required"platform" | "tenant" | "site" | "device" | "provider" | "provider-enforced" | null
contractVersion requiredstring
device requiredobject
device.approvalState required"active" | "pending"
device.deviceUid requiredstring
device.id requiredinteger
device.lastSeen requiredstring (date-time) | null
device.lifecycleState requiredstring
device.reclaimPending requiredboolean
device.status required"online" | "offline"
mfa requiredobject
mfa.mfaMaxAgeMin requiredinteger
mfa.rmmChallengeRequired requiredboolean
mfa.trustMfa requiredboolean
mfa.trusted requiredboolean
modes requiredobject
modes.console requiredobject
modes.console.available requiredboolean
modes.console.reason requiredenum (24 values) | nullA contract §15.13 reason, or null.
modes.control requiredobject
modes.control.available requiredboolean
modes.control.reason requiredenum (24 values) | nullA contract §15.13 reason, or null.
modes.view requiredobject
modes.view.available requiredboolean
modes.view.reason requiredenum (24 values) | nullA contract §15.13 reason, or null.
recording requiredobject1.7.1 (additive; ping.features.connect.recordingInConnectability): the §15.17 recording-notice answer, in the same call.
recording.notice requiredstringThe words the person at the device is shown, exactly. Empty when record is false.
recording.noticeSha256 requiredstring | nullLower-case hex SHA-256 (UTF-8) of notice: the rnh a control launch carries when requireAcknowledgement. Null when record is false.
recording.record requiredbooleanA desktop session to this device would be recorded now.
recording.requireAcknowledgement requiredbooleanThe owning organisation requires the technician to confirm the notice has been given. Always false when record is false.
serverTime requiredstring (date-time)The RMM clock.
user requiredobject
user.confirmUrl requiredstring | nullAlways null: connectability never issues or changes a link code.
user.link required"confirmed" | "pending" | "would_link" | "not_found" | "not_linkable" | "conflict" | "email_not_verified"
user.reason requiredenum (23 values) | null
user.rmmUserId requiredinteger | nullNull unless confirmed or pending.
Example response
{
  "consent": {
    "agentCanPrompt": true,
    "required": true,
    "source": "tenant"
  },
  "contractVersion": "1.10.0",
  "device": {
    "approvalState": "active",
    "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
    "id": 1187,
    "lastSeen": "2026-10-21T08:59:52.310Z",
    "lifecycleState": "active",
    "reclaimPending": false,
    "status": "online"
  },
  "mfa": {
    "mfaMaxAgeMin": 480,
    "rmmChallengeRequired": false,
    "trustMfa": true,
    "trusted": true
  },
  "modes": {
    "console": {
      "available": true,
      "reason": null
    },
    "control": {
      "available": true,
      "reason": null
    },
    "view": {
      "available": false,
      "reason": "mode_unsupported"
    }
  },
  "recording": {
    "notice": "This session is being recorded. The recording will be kept for 30 days and then deleted.\n\nRemote support sessions on Acme IT devices are recorded so that support work can be checked. Ask the IT team for a copy.",
    "noticeSha256": "199262cee030578266c5e2c2535eaaf599092f8fe52509d46f9a1f6a8c24a3ab",
    "record": true,
    "requireAcknowledgement": true
  },
  "serverTime": "2026-11-06T15:53:06.431Z",
  "user": {
    "confirmUrl": null,
    "link": "confirmed",
    "reason": null,
    "rmmUserId": 12
  }
}
400

invalid_request: a bad body; details names the field.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
401

unauthorized: the key (see the authentication rules), or the assertion, with details.reason as for /v1/launch (contract §15.13).

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "unauthorized",
    "details": {
      "reason": "assertion_expired"
    },
    "message": "The launch assertion has expired; check the server clocks."
  },
  "requestId": "req_205a4e5c9e21999bc2c29c1a"
}
403

forbidden: the key is not a paired integration key or the tenant is suspended; or details.reason is launch_not_configured or launch_disabled.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "forbidden",
    "details": {
      "reason": "launch_disabled"
    },
    "message": "Connect is not enabled in Denwen RMM."
  },
  "requestId": "req_f835bcc9e9a2b70c8811b51d"
}
404

not_found: the device in the assertion does not exist or belongs to another tenant.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
404 response of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 30 checks per Denwen MSP user or 120 per integration per 60 s. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
503

unavailable: Connect is temporarily unavailable. Retry after Retry-After if present.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
503 response of POST /api/integrations/denwen-msp/v1/connectability
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl -X POST "https://<your instance>/api/integrations/denwen-msp/v1/connectability" \
  -H "Authorization: Bearer <your key>" \
  -H "Content-Type: application/json" \
  -d '{ … }'

POST/api/integrations/denwen-msp/v1/customer-refs

Record Denwen MSP's ids on a customer

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspCustomerRefs

Records which Denwen MSP organization one customer is mapped to, or removes that with externalRefs: null. Off by default: ping.features.externalRefs.state says whether this integration may call it. The value replaces what is stored; a repeat changes nothing and a write older than the stored updatedAt is ignored_stale. It touches only this record, never a customer field the RMM owns, and sends no webhook. A url must share the origin of the integration webhook URL. The response is Cache-Control: no-store. Its own budget of 120 per integration per 60 s, not the sync budget (contract §10, §23).

Request body
Body of POST /api/integrations/denwen-msp/v1/customer-refs
FieldTypeDescription
customerId requiredintegerThe RMM customer. Another tenant's is 404.
externalRefs requiredobject | nullThe whole new value, replacing what is stored; null removes it.
externalRefs.organizationId requiredstring
externalRefs.organizationNamestring | nullOne line of text.
externalRefs.urlstring | nullMust share the origin of the integration webhook URL.
updatedAt requiredstring (date-time)When this mapping last changed in Denwen MSP; a write older than the stored one is ignored.
Responses
200

What happened, and what the RMM now holds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of POST /api/integrations/denwen-msp/v1/customer-refs
FieldTypeDescription
customerId requiredinteger
externalRefs requiredContractCustomerRefs | nullWhat the RMM holds after this call.
externalRefs.organizationId requiredstringThe Denwen MSP organization this customer is mapped to.
externalRefs.organizationName requiredstring | nullThe organization's name in Denwen MSP.
externalRefs.updatedAt requiredstring (date-time)The change time Denwen MSP sent; a write older than it is ignored.
externalRefs.url requiredstring | nullThe organization's page in Denwen MSP. https, on the origin of the integration webhook URL.
outcome required"recorded" | "unchanged" | "cleared" | "ignored_stale"unchanged: the same value, nothing moved. ignored_stale: older than what is stored, which externalRefs shows.
Example response
{
  "customerId": 42,
  "externalRefs": {
    "organizationId": "cm2k7r4pa0000lb08x9d1q5v2",
    "organizationName": "Contoso Dental",
    "updatedAt": "2026-10-20T14:03:00.000Z",
    "url": "https://msp.example/organizations/cm2k7r4pa0000lb08x9d1q5v2"
  },
  "outcome": "recorded"
}
400

invalid_request: a bad body (details names the field), or details.reason ref_url_origin or unassigned_customer.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of POST /api/integrations/denwen-msp/v1/customer-refs
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "invalid_request",
    "details": {
      "field": "externalRefs.url",
      "reason": "ref_url_origin"
    },
    "message": "The URL must be https and on the same origin as this integration's webhook URL."
  },
  "requestId": "req_7030d5ca0fe1902f227793b3"
}
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of POST /api/integrations/denwen-msp/v1/customer-refs
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, the tenant is suspended, or details.reason is customer_refs_disabled (the switch is off for this integration). Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of POST /api/integrations/denwen-msp/v1/customer-refs
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "forbidden",
    "details": {
      "reason": "customer_refs_disabled"
    },
    "message": "Denwen MSP customer links are not enabled in Denwen RMM."
  },
  "requestId": "req_e240c1340ecb10b80901f97f"
}
404

not_found: no such customer in this tenant. Another tenant's customer answers the same.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
404 response of POST /api/integrations/denwen-msp/v1/customer-refs
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "not_found",
    "message": "No such customer."
  },
  "requestId": "req_83393ce9c057ad2a091b24f5"
}
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of POST /api/integrations/denwen-msp/v1/customer-refs
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 calls per integration per 60 s. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of POST /api/integrations/denwen-msp/v1/customer-refs
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of POST /api/integrations/denwen-msp/v1/customer-refs
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl -X POST "https://<your instance>/api/integrations/denwen-msp/v1/customer-refs" \
  -H "Authorization: Bearer <your key>" \
  -H "Content-Type: application/json" \
  -d '{ … }'

GET/api/integrations/denwen-msp/v1/customers

List customers

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspListCustomers

The tenant's customers (contract §6.3). Sites are not embedded; use /v1/sites. Keyset-paginated on (updatedAt, id), ascending, up to T0 (the RMM database clock at the first page, carried in the cursor). Follow nextCursor until it is null, then store serverTime (the watermark, T0 minus 30 s) as the next updatedSince. An item appears at most once in one listing; consumers must be idempotent by (id, updatedAt) across listings. Tombstones (deleted items, kept 35 days) are included unless includeDeleted=false. A customerId or siteId naming another tenant's object returns an empty page (contract §6.2, §7).

Parameters
Parameters of GET /api/integrations/denwen-msp/v1/customers
NameInTypeDescription
updatedSincequerystring (date-time)Only items with updatedAt >= this (inclusive). RFC 3339 with an offset. Omit for a full snapshot.
cursorquerystringnextCursor of the previous page. Bound to the client, resource and filters; refused after 24 hours (invalid_cursor).
limitqueryintegerPage size.
includeDeletedquerybooleanInclude tombstones (contract §7).
Responses
200

One page of the listing.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of GET /api/integrations/denwen-msp/v1/customers
FieldTypeDescription
data requiredarray of ContractCustomerCustomers, live items and (with includeDeleted) tombstones, ordered by (updatedAt, id).
data[].accountRef requiredstringThe customer's immutable account reference (6 digits).
data[].address requiredContractAddressA postal address. Every part is a string or null.
data[].address.city requiredstring | nullTown or city.
data[].address.country requiredstring | nullCountry.
data[].address.line1 requiredstring | nullAddress line 1.
data[].address.line2 requiredstring | nullAddress line 2.
data[].address.postalCode requiredstring | nullPostcode or ZIP.
data[].address.region requiredstring | nullCounty, state or region.
data[].contactEmail requiredstring | nullContact email.
data[].contactName requiredstring | nullContact name.
data[].contactPhone requiredstring | nullContact phone.
data[].deleted requiredbooleanTrue only for a tombstone (contract §7).
data[].externalRefs requiredContractCustomerRefs | null1.10.0, additive: what the paired Denwen MSP recorded here (contract §23). Null when nothing is, and on a tombstone.
data[].externalRefs.organizationId requiredstringThe Denwen MSP organization this customer is mapped to.
data[].externalRefs.organizationName requiredstring | nullThe organization's name in Denwen MSP.
data[].externalRefs.updatedAt requiredstring (date-time)The change time Denwen MSP sent; a write older than it is ignored.
data[].externalRefs.url requiredstring | nullThe organization's page in Denwen MSP. https, on the origin of the integration webhook URL.
data[].id requiredintegerCustomer id. Never reused.
data[].kind required"normal" | "unassigned"unassigned for the tenant's reserved holding customer; exclude it from auto-matching.
data[].name requiredstringUnique within the tenant.
data[].updatedAt requiredstring (date-time)Last change; for a tombstone, the deletion time.
data[].website requiredstring | nullWebsite.
nextCursor requiredstring | nullPass as cursor for the next page; null on the last page. A page may be empty with a non-null cursor.
serverTime requiredstring (date-time)The WATERMARK: T0 minus 30 s, not the current clock. Send it as the next updatedSince after processing the last page.
400

invalid_request (a bad parameter, named in details) or invalid_cursor (tampered with, older than 24 hours, or for other filters, resource or client: restart the listing without a cursor, same updatedSince).

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of GET /api/integrations/denwen-msp/v1/customers
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of GET /api/integrations/denwen-msp/v1/customers
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, or the RMM tenant is suspended. Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of GET /api/integrations/denwen-msp/v1/customers
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of GET /api/integrations/denwen-msp/v1/customers
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 requests per integration per fixed 60 s window. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of GET /api/integrations/denwen-msp/v1/customers
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of GET /api/integrations/denwen-msp/v1/customers
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl "https://<your instance>/api/integrations/denwen-msp/v1/customers" \
  -H "Authorization: Bearer <your key>"

GET/api/integrations/denwen-msp/v1/devices

List devices

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspListDevices

The tenant's devices in every lifecycle state, purged ones as tombstones (contract §6.5). customerId and siteId filter. updatedAt moves only for the synced fields of contract §6.7, not for lastSeen alone. Keyset-paginated on (updatedAt, id), ascending, up to T0 (the RMM database clock at the first page, carried in the cursor). Follow nextCursor until it is null, then store serverTime (the watermark, T0 minus 30 s) as the next updatedSince. An item appears at most once in one listing; consumers must be idempotent by (id, updatedAt) across listings. Tombstones (deleted items, kept 35 days) are included unless includeDeleted=false. A customerId or siteId naming another tenant's object returns an empty page (contract §6.2, §7).

Parameters
Parameters of GET /api/integrations/denwen-msp/v1/devices
NameInTypeDescription
updatedSincequerystring (date-time)Only items with updatedAt >= this (inclusive). RFC 3339 with an offset. Omit for a full snapshot.
cursorquerystringnextCursor of the previous page. Bound to the client, resource and filters; refused after 24 hours (invalid_cursor).
limitqueryintegerPage size.
includeDeletedquerybooleanInclude tombstones (contract §7).
customerIdqueryintegerOnly that customer's items.
siteIdqueryintegerOnly that site's devices.
Responses
200

One page of the listing.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of GET /api/integrations/denwen-msp/v1/devices
FieldTypeDescription
data requiredarray of ContractDeviceDevices, live items and (with includeDeleted) tombstones, ordered by (updatedAt, id).
data[].agentBuild requiredstring | nullThe agent package identity.
data[].approvalState required"active" | "pending"pending: in the Unassigned container, or a held reclaim.
data[].capabilities requiredobject
data[].capabilities.credentialInjectV1 requiredbooleanAlways false in v1.
data[].customerId requiredinteger
data[].deleted requiredbooleanTrue only for a tombstone (contract §7).
data[].deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data[].displayName requiredstring | nullThe operator's label.
data[].domain requiredstring | nullWindows domain or workgroup.
data[].enrolledAt requiredstring (date-time)When the device record was created.
data[].hardware requiredobject
data[].hardware.manufacturer requiredstring | nullSystem manufacturer.
data[].hardware.model requiredstring | nullSystem model.
data[].hardware.serialNumber requiredstring | nullSystem serial number.
data[].id requiredintegerDevice id, as used in denwen://session/<id>. Survives rename and re-enrolment.
data[].isVirtualMachine requiredboolean | null
data[].lastSeen requiredstring (date-time) | null
data[].lifecycleState requiredstringOpen set: active, uninstalled, pending_delete, quarantined, offboarded; purged for a tombstone only.
data[].loggedUser requiredstring | nullAs reported at the last agent authentication.
data[].name requiredstringThe hostname the agent reports.
data[].network requiredobject
data[].network.ipAddresses requiredarray of stringEvery routable address, primary first, deduplicated.
data[].network.macAddresses requiredarray of stringEvery valid MAC, primary first, deduplicated.
data[].network.primaryIp requiredstring | nullIPv4 of the primary adapter.
data[].network.primaryMac requiredstring | nullMAC of the primary adapter, upper-case, colon-separated.
data[].network.publicIp requiredstring | nullAs recorded by the relay.
data[].os requiredstring | nulle.g. Windows 11 Pro.
data[].osBuild requiredstring | nulle.g. 22631.
data[].osVersion requiredstring | nullWindows feature release, e.g. 23H2.
data[].publicKeyFingerprint requiredstring | nullsha256: + lower-case hex SHA-256 of the agent key's DER SubjectPublicKeyInfo.
data[].publicKeySpki requiredstring | null1.3.0: the agent key as standard base64 (padded) of its DER SubjectPublicKeyInfo, the exact bytes publicKeyFingerprint hashes; null exactly when the fingerprint is null. Pin it with the fingerprint; credential envelopes are encrypted to it (contract §17).
data[].siteId requiredinteger
data[].status required"online" | "offline"
data[].type requiredstring | nullLower-case, open set: workstation, laptop, server today.
data[].uninstalledAt requiredstring (date-time) | null
data[].updatedAt requiredstring (date-time)Last change to a synced field (contract §6.7); for a tombstone, the purge time.
nextCursor requiredstring | nullPass as cursor for the next page; null on the last page. A page may be empty with a non-null cursor.
serverTime requiredstring (date-time)The WATERMARK: T0 minus 30 s, not the current clock. Send it as the next updatedSince after processing the last page.
400

invalid_request (a bad parameter, named in details) or invalid_cursor (tampered with, older than 24 hours, or for other filters, resource or client: restart the listing without a cursor, same updatedSince).

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of GET /api/integrations/denwen-msp/v1/devices
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of GET /api/integrations/denwen-msp/v1/devices
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, or the RMM tenant is suspended. Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of GET /api/integrations/denwen-msp/v1/devices
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of GET /api/integrations/denwen-msp/v1/devices
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 requests per integration per fixed 60 s window. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of GET /api/integrations/denwen-msp/v1/devices
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of GET /api/integrations/denwen-msp/v1/devices
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl "https://<your instance>/api/integrations/denwen-msp/v1/devices" \
  -H "Authorization: Bearer <your key>"

GET/api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice

Get the recording notice for a device

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspGetRecordingNotice

Would a desktop session to this device be recorded now, does its organisation require the operator to confirm the notice has been given before connecting (Administration, Data Protection), and what is the person at the device told? Decided by the same rule the relay applies when the session starts, so the two cannot disagree. When record && requireAcknowledgement, show notice and ask the technician to confirm it, then send rna (= deviceId) and rnh (SHA-256 of notice) in the mode: "control" launch assertion; without them /v1/launch answers 409 recording_notice_unacknowledged. Read only; no assertion (the answer does not depend on who asks); Cache-Control: no-store (contract §15.17).

Parameters
Parameters of GET /api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice
NameInTypeDescription
deviceUid requiredpathstringThe DWN-... device uid (any case or punctuation; normalised).
Responses
200

The recording notice decision.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of GET /api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice
FieldTypeDescription
data requiredobject
data.deviceId requiredintegerThe RMM's numeric device id: the session target a confirmation names (rna).
data.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.notice requiredstringThe words the person at the device is shown, exactly; rnh is the lower-case hex SHA-256 of this string as UTF-8. Empty when record is false.
data.record requiredbooleanA desktop session to this device would be recorded now.
data.requireAcknowledgement requiredbooleanThe owning organisation requires the operator to confirm the notice has been given before connecting. Always false when record is false. A control launch must carry rna and rnh exactly when record && requireAcknowledgement.
Example response
{
  "data": {
    "deviceId": 1187,
    "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
    "notice": "This session is being recorded. The recording will be kept for 30 days and then deleted.\n\nRemote support sessions on Acme IT devices are recorded so that support work can be checked. Ask the IT team for a copy.",
    "record": true,
    "requireAcknowledgement": true
  }
}
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of GET /api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, or the RMM tenant is suspended. Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of GET /api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
404

not_found: no such device, or it belongs to another tenant (never 403).

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
404 response of GET /api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of GET /api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 requests per integration per fixed 60 s window. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of GET /api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of GET /api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
503

unavailable: the recording policy could not be read. Retry; launch nothing meanwhile.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
503 response of GET /api/integrations/denwen-msp/v1/devices/{deviceUid}/recording-notice
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl "https://<your instance>/api/integrations/denwen-msp/v1/devices/<deviceUid>/recording-notice" \
  -H "Authorization: Bearer <your key>"

GET/api/integrations/denwen-msp/v1/devices/{id}

Get one device

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspGetDevice

Always current (not cached), so suitable for a live status page. A purged device within the 35-day retention answers with its tombstone (contract §6.6).

Parameters
Parameters of GET /api/integrations/denwen-msp/v1/devices/{id}
NameInTypeDescription
id requiredpathstringThe DWN-... device uid (any case or punctuation; normalised) or the numeric device id.
Responses
200

The device, the same shape as one listing item.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of GET /api/integrations/denwen-msp/v1/devices/{id}
FieldTypeDescription
data requiredContractDeviceA managed device (contract §6.5). A tombstone also has lifecycleState: purged and status: offline.
data.agentBuild requiredstring | nullThe agent package identity.
data.approvalState required"active" | "pending"pending: in the Unassigned container, or a held reclaim.
data.capabilities requiredobject
data.capabilities.credentialInjectV1 requiredbooleanAlways false in v1.
data.customerId requiredinteger
data.deleted requiredbooleanTrue only for a tombstone (contract §7).
data.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.displayName requiredstring | nullThe operator's label.
data.domain requiredstring | nullWindows domain or workgroup.
data.enrolledAt requiredstring (date-time)When the device record was created.
data.hardware requiredobject
data.hardware.manufacturer requiredstring | nullSystem manufacturer.
data.hardware.model requiredstring | nullSystem model.
data.hardware.serialNumber requiredstring | nullSystem serial number.
data.id requiredintegerDevice id, as used in denwen://session/<id>. Survives rename and re-enrolment.
data.isVirtualMachine requiredboolean | null
data.lastSeen requiredstring (date-time) | null
data.lifecycleState requiredstringOpen set: active, uninstalled, pending_delete, quarantined, offboarded; purged for a tombstone only.
data.loggedUser requiredstring | nullAs reported at the last agent authentication.
data.name requiredstringThe hostname the agent reports.
data.network requiredobject
data.network.ipAddresses requiredarray of stringEvery routable address, primary first, deduplicated.
data.network.macAddresses requiredarray of stringEvery valid MAC, primary first, deduplicated.
data.network.primaryIp requiredstring | nullIPv4 of the primary adapter.
data.network.primaryMac requiredstring | nullMAC of the primary adapter, upper-case, colon-separated.
data.network.publicIp requiredstring | nullAs recorded by the relay.
data.os requiredstring | nulle.g. Windows 11 Pro.
data.osBuild requiredstring | nulle.g. 22631.
data.osVersion requiredstring | nullWindows feature release, e.g. 23H2.
data.publicKeyFingerprint requiredstring | nullsha256: + lower-case hex SHA-256 of the agent key's DER SubjectPublicKeyInfo.
data.publicKeySpki requiredstring | null1.3.0: the agent key as standard base64 (padded) of its DER SubjectPublicKeyInfo, the exact bytes publicKeyFingerprint hashes; null exactly when the fingerprint is null. Pin it with the fingerprint; credential envelopes are encrypted to it (contract §17).
data.siteId requiredinteger
data.status required"online" | "offline"
data.type requiredstring | nullLower-case, open set: workstation, laptop, server today.
data.uninstalledAt requiredstring (date-time) | null
data.updatedAt requiredstring (date-time)Last change to a synced field (contract §6.7); for a tombstone, the purge time.
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of GET /api/integrations/denwen-msp/v1/devices/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, or the RMM tenant is suspended. Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of GET /api/integrations/denwen-msp/v1/devices/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
404

not_found: no such device, or it belongs to another tenant (never 403), or it was purged more than 35 days ago.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
404 response of GET /api/integrations/denwen-msp/v1/devices/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of GET /api/integrations/denwen-msp/v1/devices/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 requests per integration per fixed 60 s window. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of GET /api/integrations/denwen-msp/v1/devices/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of GET /api/integrations/denwen-msp/v1/devices/{id}
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl "https://<your instance>/api/integrations/denwen-msp/v1/devices/<id>" \
  -H "Authorization: Bearer <your key>"

POST/api/integrations/denwen-msp/v1/launch

Launch a remote session or console hand-off

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspLaunch

Needs BOTH the integration key AND a launch assertion signed by the key the tenant registered (contract §15.3); a leaked key alone cannot open a session. The integration key is read-scoped: it may POST here because this path is in its write allowlist (x-integration-write-allowlist), not because of its scope. next says what to do: open_viewer (navigate to viewerUri once), open_console or complete_mfa_in_console (open consoleUrl in a new tab), or, only for a request with client: "web" (1.3.0), open_web_viewer (open webUrl in a new tab; the RMM hand-off signs in and opens its web viewer). Codes are single-use and expire after 60 s; the response is Cache-Control: no-store. Limits: 12 per Denwen MSP user and 60 per integration per 60 s, on their own budget, not the 120/60 s sync budget (contract §10, §15.8).

Request body
Body of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
assertion requiredstringA compact JWS signed by Denwen MSP's registered key, purpose: "launch" (contract §15.3). At most 8192 bytes. With mode: "control" it may also carry (§15.17) rna, the RMM device id the operator confirmed the recording notice for, and rnh, the lower-case hex SHA-256 of the exact notice text from GET /v1/devices/{deviceUid}/recording-notice; a present claim of the wrong type is 401 assertion_invalid.
client"native" | "web"1.3.0: which viewer to open. Default native (the 1.2 behaviour). web needs mode: control and an RMM that lists web in ping.features.connect.clients.
theme"dark" | "light"Viewer window theme (presentation only). Default dark.
Responses
200

Launched.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
consent requiredobject
consent.required requiredbooleanTrue when the person at the machine will be asked to allow the session.
consent.source required"platform" | "tenant" | "site" | "device" | "provider" | "provider-enforced" | null
consoleUrl requiredstring | nullNon-null when next is open_console or complete_mfa_in_console. Always <baseUrl>/handoff#handoff=hnd_....
contractVersion requiredstring
device requiredobject
device.deviceUid requiredstring
device.id requiredinteger
device.status required"online" | "offline"
expiresAt requiredstring (date-time)When the issued code (viewer or console) expires: 60 s after issue.
launchId requiredstringCorrelates this launch with session.* events.
mfa requiredobject
mfa.rmmChallengeRequired requiredboolean
mfa.satisfiedBy required"flowmsp" | "not_required" | nullflowmsp: Denwen MSP's own MFA was trusted (the value keeps its contract v1 spelling, formerly FlowMSP, and the RMM sends only this value). not_required: no second factor was needed. Null when the RMM will ask for its own code.
mode required"control" | "console"Echo of the assertion's mode.
next required"open_viewer" | "open_console" | "complete_mfa_in_console" | "open_web_viewer"What Denwen MSP does next. open_web_viewer (1.3.0) is returned only to a request that said client: "web".
user requiredobject
user.email requiredstring
user.link required"confirmed"
user.linkedNow requiredbooleanTrue when this launch created the link.
user.rmmUserId requiredinteger
viewerUri requiredstring | nullNon-null only when next is open_viewer. Always starts with denwen://session/. Opaque: never parse, store or log it.
webUrl requiredstring | null1.3.0: non-null only when next is open_web_viewer. Always <baseUrl>/handoff#handoff=hnd_...; open it in a new tab with noopener,noreferrer.
Example response
{
  "consent": {
    "required": true,
    "source": "tenant"
  },
  "consoleUrl": null,
  "contractVersion": "1.10.0",
  "device": {
    "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
    "id": 1187,
    "status": "online"
  },
  "expiresAt": "2026-11-06T15:54:06.414Z",
  "launchId": "lnc_01M6RS0K3CT3PAE7KX3DFA9QNS",
  "mfa": {
    "rmmChallengeRequired": false,
    "satisfiedBy": "flowmsp"
  },
  "mode": "control",
  "next": "open_viewer",
  "user": {
    "email": "sam.taylor@acme-msp.example",
    "link": "confirmed",
    "linkedNow": false,
    "rmmUserId": 12
  },
  "viewerUri": "denwen://session/1187?relay=wss%3A%2F%2Feu1.rmm.acme-msp.example&code=e1143198d4a46139765ca8a3f7184d0b&theme=dark&name=Reception%20PC",
  "webUrl": null
}
400

invalid_request: a bad body (details names the field), or details.mode: "unsupported" (view is not available), details.grant: "unsupported", or (1.3.0) details.client: must be native or web or web needs mode control.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "invalid_request",
    "details": {
      "mode": "unsupported"
    },
    "message": "This Connect option is not available yet."
  },
  "requestId": "req_0a818f76aece51a7e1445e00"
}
401

unauthorized: the key (see the authentication rules), or the assertion, with details.reason one of assertion_invalid, unknown_kid, bad_signature, issuer_mismatch, audience_mismatch, purpose_mismatch, assertion_expired, assertion_replayed (contract §15.13).

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "unauthorized",
    "details": {
      "reason": "bad_signature"
    },
    "message": "Connect is misconfigured; ask an administrator to check the Denwen RMM signing key."
  },
  "requestId": "req_0af6543535a5a728554082e2"
}
403

forbidden: the key is not a paired integration key or the tenant is suspended; or Connect refused, with details.reason one of launch_not_configured, launch_disabled, account_not_found, account_not_linkable, account_link_pending (with a fresh details.confirmUrl; only the newest works), account_link_conflict (with details.conflict), email_not_verified, missing_permission, outside_site_scope; or (1.3.0) web_viewer_disabled (client: "web" while the RMM has not released its web viewer; retry with native).

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "forbidden",
    "details": {
      "confirmUrl": "https://rmm.acme-msp.example/handoff#link=lnk_QoQCenbc9K8DtC_o71uy51yRP88sapzXI4yRHGy_efQ",
      "reason": "account_link_pending"
    },
    "message": "Confirm the link in Denwen RMM."
  },
  "requestId": "req_e8a339ab59465212cb0683f9"
}
404

not_found: the device in the assertion does not exist or belongs to another tenant.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
404 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "not_found",
    "message": "This device is no longer in Denwen RMM."
  },
  "requestId": "req_8ae27a1f142936d5afc519fc"
}
409

forbidden: the device cannot take a session now; details.reason is device_offline, device_not_active, device_pending_approval or device_reclaim_pending; or (1.5.0) remote_control_unsupported, for mode: control on a device whose agent cannot serve a desktop (a Linux agent without remote desktop; mode: console still works); or (§15.17) recording_notice_unacknowledged: a mode: control session would be recorded under a policy that requires the operator's confirmation, and the assertion did not carry rna (this device's id) with rnh (the hash of the notice as it reads now). details.notice: "changed" when rna was right but the notice has changed since it was shown. Refused before anything is issued.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
409 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Example response
{
  "error": {
    "code": "forbidden",
    "details": {
      "reason": "device_offline"
    },
    "message": "The device is offline."
  },
  "requestId": "req_f3f69a4f8bcc271b49d8c75e"
}
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 12 launches per Denwen MSP user or 60 per integration per 60 s. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
503

unavailable: Connect is temporarily unavailable. Retry after Retry-After if present.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
503 response of POST /api/integrations/denwen-msp/v1/launch
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl -X POST "https://<your instance>/api/integrations/denwen-msp/v1/launch" \
  -H "Authorization: Bearer <your key>" \
  -H "Content-Type: application/json" \
  -d '{ … }'

GET/api/integrations/denwen-msp/v1/ping

Prove the key and name the tenant

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspPing

Used for "Test connection" and for pinning tenant.accountRef. serverTime is the real RMM clock (use it, not a listing watermark, to measure clock skew). features.connect reports whether Connect is configured and (1.3.0) which viewers a launch may ask for in clients (contract §6.1, §15.16). (1.4.0) features.actions reports which ticket actions may be asked for and their MFA and notice rules (§18), and features.help whether tray help requests are on and the state of the portal link (§19, §20).

Responses
200

The key works.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of GET /api/integrations/denwen-msp/v1/ping
FieldTypeDescription
contractVersion requiredstringThe contract version the RMM serves (1.10.0).
features requiredobject
features.actions requiredobjectTicket actions (1.4, additive; contract §18).
features.actions.kinds requiredarray of "script" | "reboot" | "patch_scan" | "diagnostics"The action kinds this integration may ask for (§18.1). Offer only these.
features.actions.maxOutputBytes requiredintegerresult.stdout and result.stderr are each cut to this many bytes.
features.actions.mfaMaxAgeMin requiredinteger
features.actions.outputToFlowmsp requiredbooleanWhether action output is sent to Denwen MSP. False: actions report exit code, summary and state only, never output. (Denwen MSP, formerly FlowMSP: the field name keeps its contract v1 name, and the RMM sends only this name.)
features.actions.rebootNoticeSeconds requiredobjectThe notice a reboot gives a signed-in user, in seconds; a lower noticeSeconds is raised to min.
features.actions.rebootNoticeSeconds.default requiredinteger
features.actions.rebootNoticeSeconds.max requiredinteger
features.actions.rebootNoticeSeconds.min requiredinteger
features.actions.requiresMfa requiredbooleanThe integration's "MFA required" setting (on by default; turned off only by a pairing actor). While true, script, reboot and diagnostics need trusted Denwen MSP MFA no older than mfaMaxAgeMin; while false, no kind does.
features.actions.state required"enabled" | "disabled" | "not_configured"not_configured: no Connect issuer or signing key (the action assertion needs them). disabled: off for this integration, or suspended platform-wide.
features.alerts requiredobjectAlerts to tickets (1.2, additive).
features.alerts.categories requiredarray of stringEmpty means every category.
features.alerts.enabledAt requiredstring (date-time) | null
features.alerts.infrastructure requiredboolean§16.11: this integration's "Also send hypervisor and website/port check alerts" (off by default). While true, alerts with site: null and an asset are sent.
features.alerts.infrastructureKinds requiredarray of "hypervisor" | "uptime"§16.11 (1.8.0, additive): the deviceless alert kinds this RMM can send. Absent on an older RMM.
features.alerts.minSeverity required"warning" | "critical"
features.alerts.resolveOnTicketClose requiredboolean
features.alerts.state required"enabled" | "disabled"
features.connect requiredobjectConnect and SSO (1.1, additive).
features.connect.clients requiredarray of "native" | "web"1.3.0: the viewers a launch may ask for (client). web is listed only while the RMM has released its web viewer; offer the Web choice only then.
features.connect.mfaMaxAgeMin requiredinteger
features.connect.modes requiredarray of "control" | "console"
features.connect.recordingInConnectability requiredboolean1.7.1 (additive): POST /v1/connectability carries recording, the §15.17 answer in the same call. Read recording only while this is true.
features.connect.recordingNotice requiredboolean§15.17 (additive): the RMM serves GET /v1/devices/{deviceUid}/recording-notice and honours the launch assertion's rna / rnh (the operator's recording notice confirmation). While it is absent or false, ask nothing and send nothing.
features.connect.state required"enabled" | "disabled" | "not_configured"not_configured: no issuer or no signing key registered (contract §15.2).
features.connect.trustMfa requiredboolean
features.externalRefs requiredobjectCustomer references (1.10, additive; contract §23).
features.externalRefs.state required"enabled" | "disabled"enabled: this integration may call POST /v1/customer-refs (off by default; contract §23).
features.help requiredobjectTray help requests and the portal link (1.4, additive; contract §19, §20).
features.help.portalLink required"none" | "pending_approval" | "approved"The tray portal link (§20): pending_approval until an RMM administrator approves the URL Denwen MSP reported.
features.help.state required"enabled" | "disabled"Tray help requests (§19) are being sent for this integration.
features.pathPrefixesarray of stringThe v1 path prefixes this RMM serves, each reaching the same routes with the same authentication: /api/integrations/flowmsp/v1 (the original prefix, from before Denwen MSP was renamed from FlowMSP) and /api/integrations/denwen-msp/v1 (additive).
integration requiredobject
integration.id requiredinteger
integration.kind required"flowmsp"The integration kind: Denwen MSP. flowmsp is its contract v1 value (formerly FlowMSP), and the RMM sends only this value.
integration.status required"active"A disabled client answers 410 instead.
limits requiredobjectInformational.
limits.maxPageSize requiredinteger
limits.requestsPerMinute requiredinteger
serverTime requiredstring (date-time)The RMM database clock (real time, not a watermark).
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant (TNT- + 6, immutable). Pin it.
tenant.name requiredstring
Example response
{
  "contractVersion": "1.10.0",
  "features": {
    "actions": {
      "kinds": [
        "script",
        "reboot",
        "patch_scan"
      ],
      "maxOutputBytes": 16384,
      "mfaMaxAgeMin": 30,
      "outputToFlowmsp": true,
      "rebootNoticeSeconds": {
        "default": 300,
        "max": 900,
        "min": 300
      },
      "requiresMfa": true,
      "state": "enabled"
    },
    "alerts": {
      "categories": [],
      "enabledAt": "2026-10-22T08:00:00.000Z",
      "infrastructure": false,
      "infrastructureKinds": [
        "hypervisor",
        "uptime"
      ],
      "minSeverity": "warning",
      "resolveOnTicketClose": true,
      "state": "enabled"
    },
    "connect": {
      "clients": [
        "native",
        "web"
      ],
      "mfaMaxAgeMin": 480,
      "modes": [
        "control",
        "console"
      ],
      "recordingInConnectability": true,
      "recordingNotice": true,
      "state": "enabled",
      "trustMfa": true
    },
    "externalRefs": {
      "state": "enabled"
    },
    "help": {
      "portalLink": "approved",
      "state": "enabled"
    },
    "pathPrefixes": [
      "/api/integrations/flowmsp/v1",
      "/api/integrations/denwen-msp/v1"
    ]
  },
  "integration": {
    "id": 7,
    "kind": "flowmsp",
    "status": "active"
  },
  "limits": {
    "maxPageSize": 500,
    "requestsPerMinute": 120
  },
  "serverTime": "2026-11-06T15:53:06.388Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2",
    "name": "Acme MSP"
  }
}
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of GET /api/integrations/denwen-msp/v1/ping
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, or the RMM tenant is suspended. Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of GET /api/integrations/denwen-msp/v1/ping
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of GET /api/integrations/denwen-msp/v1/ping
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 requests per integration per fixed 60 s window. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of GET /api/integrations/denwen-msp/v1/ping
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of GET /api/integrations/denwen-msp/v1/ping
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl "https://<your instance>/api/integrations/denwen-msp/v1/ping" \
  -H "Authorization: Bearer <your key>"

GET/api/integrations/denwen-msp/v1/scripts

List the scripts offered to this integration

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspListScripts

The library scripts the tenant has offered to Denwen MSP for ticket actions: name, description, shell, version, timeout and declared parameters, never the script content. runnable is false, with notRunnableReason too_large, timeout_over_cap or changed_since_offered (edited in the RMM after it was offered; runnable again once re-offered), for a script an action would refuse. Cache it for 5 minutes. Follow nextCursor until it is null. Counts against the 120/60 s sync budget (contract §18.2).

Parameters
Parameters of GET /api/integrations/denwen-msp/v1/scripts
NameInTypeDescription
limitquerystring1 to 500.
cursorquerystringnextCursor from the previous page.
Responses
200

One page of offered scripts.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of GET /api/integrations/denwen-msp/v1/scripts
FieldTypeDescription
contractVersion requiredstring
data requiredarray of ContractScriptThe scripts offered to this integration, by id ascending.
data[].category requiredstring | null
data[].description requiredstring | null
data[].id requiredinteger
data[].name requiredstring
data[].notRunnableReason required"too_large" | "timeout_over_cap" | "changed_since_offered" | null
data[].os required"windows" | "linux"From the shell: powershell and cmd run on Windows, bash on Linux.
data[].parameters requiredarray of object
data[].parameters[].default requiredstring | number | boolean | null
data[].parameters[].label requiredstring | null
data[].parameters[].name requiredstring
data[].parameters[].required requiredboolean
data[].parameters[].type required"string" | "number" | "boolean"
data[].runnable requiredboolean
data[].shell required"powershell" | "cmd" | "bash"
data[].siteIds requiredarray of integer | nullNull for every site, else the one site the script is scoped to.
data[].timeoutSec requiredinteger
data[].updatedAt requiredstring (date-time)Last change to the script.
data[].version requiredintegerSend it as scriptVersion; a newer version is refused script_changed.
nextCursor requiredstring | nullNull on the last page.
serverTime requiredstring (date-time)The RMM clock.
Example response
{
  "contractVersion": "1.10.0",
  "data": [
    {
      "category": "Printing",
      "description": "Stops the spooler, clears queued jobs, starts it again.",
      "id": 41,
      "name": "Clear print queue",
      "notRunnableReason": null,
      "os": "windows",
      "parameters": [
        {
          "default": "",
          "label": "Printer (blank = all)",
          "name": "PrinterName",
          "required": false,
          "type": "string"
        }
      ],
      "runnable": true,
      "shell": "powershell",
      "siteIds": null,
      "timeoutSec": 60,
      "updatedAt": "2026-11-20T09:00:00.000Z",
      "version": 3
    }
  ],
  "nextCursor": null,
  "serverTime": "2026-11-06T15:53:06.460Z"
}
400

invalid_request (a bad parameter, named in details) or invalid_cursor (tampered with, older than 24 hours, or for other filters, resource or client: restart the listing without a cursor, same updatedSince).

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of GET /api/integrations/denwen-msp/v1/scripts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of GET /api/integrations/denwen-msp/v1/scripts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, or the RMM tenant is suspended. Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of GET /api/integrations/denwen-msp/v1/scripts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of GET /api/integrations/denwen-msp/v1/scripts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 requests per integration per fixed 60 s window. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of GET /api/integrations/denwen-msp/v1/scripts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of GET /api/integrations/denwen-msp/v1/scripts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
503

unavailable: ticket actions are not available right now. Retry later.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
503 response of GET /api/integrations/denwen-msp/v1/scripts
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl "https://<your instance>/api/integrations/denwen-msp/v1/scripts" \
  -H "Authorization: Bearer <your key>"

GET/api/integrations/denwen-msp/v1/sites

List sites

  • stable
  • Integration key
  • Scope: read
  • Contract 1.10.0
  • denwenMspListSites

The tenant's sites (contract §6.4). customerId filters by customer. Keyset-paginated on (updatedAt, id), ascending, up to T0 (the RMM database clock at the first page, carried in the cursor). Follow nextCursor until it is null, then store serverTime (the watermark, T0 minus 30 s) as the next updatedSince. An item appears at most once in one listing; consumers must be idempotent by (id, updatedAt) across listings. Tombstones (deleted items, kept 35 days) are included unless includeDeleted=false. A customerId or siteId naming another tenant's object returns an empty page (contract §6.2, §7).

Parameters
Parameters of GET /api/integrations/denwen-msp/v1/sites
NameInTypeDescription
updatedSincequerystring (date-time)Only items with updatedAt >= this (inclusive). RFC 3339 with an offset. Omit for a full snapshot.
cursorquerystringnextCursor of the previous page. Bound to the client, resource and filters; refused after 24 hours (invalid_cursor).
limitqueryintegerPage size.
includeDeletedquerybooleanInclude tombstones (contract §7).
customerIdqueryintegerOnly that customer's items.
Responses
200

One page of the listing.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
200 response of GET /api/integrations/denwen-msp/v1/sites
FieldTypeDescription
data requiredarray of ContractSiteSites, live items and (with includeDeleted) tombstones, ordered by (updatedAt, id).
data[].address requiredContractAddressA postal address. Every part is a string or null.
data[].address.city requiredstring | nullTown or city.
data[].address.country requiredstring | nullCountry.
data[].address.line1 requiredstring | nullAddress line 1.
data[].address.line2 requiredstring | nullAddress line 2.
data[].address.postalCode requiredstring | nullPostcode or ZIP.
data[].address.region requiredstring | nullCounty, state or region.
data[].customerId requiredinteger
data[].deleted requiredbooleanTrue only for a tombstone (contract §7).
data[].id requiredintegerSite id. Never reused.
data[].kind required"normal" | "unassigned"Every customer may have one unassigned site; the tenant's holding area is the unassigned site of the unassigned customer.
data[].name requiredstringUnique within the customer.
data[].siteRef requiredstring | nullOptional site reference.
data[].updatedAt requiredstring (date-time)Last change; for a tombstone, the deletion time.
nextCursor requiredstring | nullPass as cursor for the next page; null on the last page. A page may be empty with a non-null cursor.
serverTime requiredstring (date-time)The WATERMARK: T0 minus 30 s, not the current clock. Send it as the next updatedSince after processing the last page.
400

invalid_request (a bad parameter, named in details) or invalid_cursor (tampered with, older than 24 hours, or for other filters, resource or client: restart the listing without a cursor, same updatedSince).

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
400 response of GET /api/integrations/denwen-msp/v1/sites
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
401

unauthorized: the Authorization header is missing, or the key is malformed, unknown, has the wrong secret, or is revoked or expired (the message says which). Do not retry; surface to an administrator.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
401 response of GET /api/integrations/denwen-msp/v1/sites
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
403

forbidden: the key is not an integration key paired with Denwen MSP, or the RMM tenant is suspended. Do not retry.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
403 response of GET /api/integrations/denwen-msp/v1/sites
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
410

integration_disabled: the integration client is disabled in the RMM. Stop until an administrator re-enables it.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
410 response of GET /api/integrations/denwen-msp/v1/sites
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
429

rate_limited: over 120 requests per integration per fixed 60 s window. Retry after Retry-After seconds.

  • RateLimit-Limit: The budget applied to this request, per fixed window.
  • RateLimit-Remaining: Requests left in the current window.
  • RateLimit-Reset: Seconds until the window resets.
  • Retry-After: Seconds to wait before retrying.
  • X-Request-Id: Correlates the request; equals requestId in an error body.
429 response of GET /api/integrations/denwen-msp/v1/sites
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
500

internal: unexpected. Retry with backoff.

  • X-Request-Id: Correlates the request; equals requestId in an error body.
500 response of GET /api/integrations/denwen-msp/v1/sites
FieldTypeDescription
error requiredobject
error.code requiredenum (9 values)
error.detailsmap of any
error.message requiredstring
requestId requiredstringAlso sent as the X-Request-Id response header.
Request
curl "https://<your instance>/api/integrations/denwen-msp/v1/sites" \
  -H "Authorization: Bearer <your key>"

Devices

Managed endpoints: inventory summary, alerts, timeline, script and patch history.

GET/api/devices

List devices

  • beta
  • Standard API key
  • Scope: read
  • Permission: devices.view
  • listDevices

Every device within the key's reach, ordered by hostname, as an unpaginated array, in the published projection. An API key always receives that projection; view=summary names it explicitly and changes nothing for a key. By default only active (managed) devices are listed; lifecycle=all includes uninstalled ones, whose history is kept until purgeAfter.

Parameters
Parameters of GET /api/devices
NameInTypeDescription
viewquery"summary"Optional for an API key, which always receives the published projection.
siteIdquerystringOnly devices in this site. A site outside the key's reach yields an empty array.
lifecyclequerystringactive (the default), all, or one lifecycle state: uninstalled or pending_delete.
Responses
200

The devices.

200 response of GET /api/devices
FieldTypeDescription
[].accessCapabilities requiredDeviceAccessCapabilities | nullWhich ways into the endpoint its agent reported it supports. Null when the agent never reported capabilities.
[].accessCapabilities.remoteDesktopV1trueA Linux agent that can serve a remote desktop session. Windows agents never send it: every Windows endpoint serves one.
[].accessCapabilities.remoteLoginV1trueA Linux agent on GNOME 46 or later that can open a Remote login: a separate, headless GNOME session through GDM, not the screen in front of the computer.
[].accessCapabilities.safeModeV1trueA Windows agent that can restart its endpoint into Safe Mode with Networking and back, with a failsafe that returns it to normal Windows if it cannot reach the server.
[].accessCapabilities.terminalV1trueA Linux agent whose remote terminal works.
[].accessCapabilities.toolsFilesV1trueA Linux agent whose file browser works.
[].agentBuild requiredstring | nullThe build stamp of the agent binary, as reported at its last authentication.
[].approvalState requiredstringactive, or pending while the device waits in the Unassigned container for an operator to place it.
[].bootMode required"normal" | "safe-network" | "safe-minimal" | nullHow Windows booted, as the agent reported at its last authentication. Null when the agent never reported it (older agents, Linux).
[].createdAt requiredstring (date-time)
[].deviceUid requiredstring | nullThe permanent Denwen identity, in its presentation form. Survives rename and re-enrolment.
[].displayName requiredstring | nullThe friendly name an operator chose, if any.
[].effectiveName requiredstringdisplayName when set, otherwise name. What to show a person.
[].enforceSignedControl requiredbooleanWhether the agent accepts only signed control commands.
[].id requiredintegerThe device id used in every /api/devices/{id} path.
[].lastSeen requiredstring (date-time)
[].lifecycleState requiredstringactive, uninstalled or pending_delete. Independent of status.
[].loggedUser requiredstring | null
[].name requiredstringThe hostname the agent reports.
[].os requiredstring
[].patchPolicyId requiredinteger | null
[].probeRole requiredbooleanWhether the device is nominated to run uptime checks.
[].purgeAfter requiredstring (date-time) | nullWhen an uninstalled device and its history will be deleted.
[].relayNode requiredDeviceRelayNode | null
[].relayNode.id requiredstring
[].relayNode.publicWsUrl requiredstringThe public WebSocket URL a remote session to this device connects through.
[].relayNode.status requiredstringonline, degraded, draining or offline.
[].serverTime requiredstring (date-time)The server clock when the response was built. Compare lastSeen against this, not the client clock.
[].site requiredDeviceSiteRef | null
[].site.customerId requiredinteger | nullThe customer (client organisation) the site belongs to.
[].site.id requiredinteger
[].site.name requiredstring
[].siteId requiredinteger
[].status requiredstringConnectivity only: online or offline.
[].telemetry requiredDeviceTelemetrySummary | null
[].telemetry.networkarray of object | objectNetwork adapters. Older agents wrap the array in data.
[].telemetry.patchesobjectThe last Windows Update scan: when, its outcome, and the updates it found missing.
[].telemetry.patches.rebootRequiredboolean | null
[].telemetry.patches.scanStatusstring | null
[].telemetry.patches.scannedAtstring | null
[].telemetry.patches.updates requiredarray of object
[].telemetry.patches.updates[].classificationstring | null
[].telemetry.patches.updates[].kbstring | null
[].telemetry.patches.updates[].titlestring | null
[].telemetry.systemobject | objectOperating system and agent facts, as the agent last reported them. Older agents wrap the object in data.
[].type requiredstringThe kind of endpoint, e.g. workstation, server or laptop.
[].uninstalledAt requiredstring (date-time) | null
[].updatedAt requiredstring (date-time)
[].uptime requiredstring | null
Example response
[
  {
    "accessCapabilities": {},
    "agentBuild": "pkg:a1b2c3d4",
    "approvalState": "active",
    "bootMode": null,
    "createdAt": "2026-08-07T10:12:00.000Z",
    "deviceUid": "DWN-7K2M-9QX4-RB3H-T6W3",
    "displayName": "Reception PC",
    "effectiveName": "Reception PC",
    "enforceSignedControl": true,
    "id": 101,
    "lastSeen": "2026-10-22T09:29:41.000Z",
    "lifecycleState": "active",
    "loggedUser": "ACME\\j.smith",
    "name": "ACME-WS01",
    "os": "Windows",
    "patchPolicyId": 3,
    "probeRole": false,
    "purgeAfter": null,
    "relayNode": {
      "id": "relay-lon-1",
      "publicWsUrl": "wss://relay.example.test",
      "status": "online"
    },
    "serverTime": "2026-10-22T09:30:00.000Z",
    "site": {
      "customerId": 4,
      "id": 11,
      "name": "Head Office"
    },
    "siteId": 11,
    "status": "online",
    "telemetry": {
      "network": {
        "data": [
          {
            "gateway": "192.0.2.1",
            "ipv4": "192.0.2.41",
            "macAddress": "00-00-5E-00-53-29",
            "name": "Ethernet",
            "status": "Up"
          }
        ]
      },
      "patches": {
        "rebootRequired": false,
        "scanStatus": "ok",
        "scannedAt": "2026-10-22T03:00:00Z",
        "updates": [
          {
            "classification": "Security Updates",
            "kb": "KB5044285",
            "title": "2026-10 Cumulative Update for Windows 11"
          }
        ]
      },
      "system": {
        "data": {
          "agentVersion": "2.4.0",
          "build": "22631.4317",
          "buildNumber": "22631",
          "osName": "Windows 11 Pro",
          "osVersion": "23H2",
          "publicIp": "203.0.113.24"
        }
      }
    },
    "type": "workstation",
    "uninstalledAt": null,
    "updatedAt": "2026-10-22T09:29:41.000Z",
    "uptime": "3d 4h"
  },
  {
    "accessCapabilities": {},
    "agentBuild": "pkg:a1b2c3d4",
    "approvalState": "active",
    "bootMode": null,
    "createdAt": "2026-08-07T10:12:00.000Z",
    "deviceUid": "DWN-3HB8-T6WZ-PA4K-M2N4",
    "displayName": null,
    "effectiveName": "ACME-SRV01",
    "enforceSignedControl": false,
    "id": 102,
    "lastSeen": "2026-10-21T22:05:10.000Z",
    "lifecycleState": "active",
    "loggedUser": null,
    "name": "ACME-SRV01",
    "os": "Windows",
    "patchPolicyId": null,
    "probeRole": true,
    "purgeAfter": null,
    "relayNode": {
      "id": "relay-lon-1",
      "publicWsUrl": "wss://relay.example.test",
      "status": "online"
    },
    "serverTime": "2026-10-22T09:30:00.000Z",
    "site": {
      "customerId": 4,
      "id": 11,
      "name": "Head Office"
    },
    "siteId": 11,
    "status": "offline",
    "telemetry": {
      "system": {
        "agentVersion": "2.4.0",
        "buildNumber": "20348",
        "osName": "Windows Server 2022 Standard",
        "osVersion": "21H2",
        "publicIp": "203.0.113.24"
      }
    },
    "type": "server",
    "uninstalledAt": null,
    "updatedAt": "2026-10-22T09:29:41.000Z",
    "uptime": null
  }
]
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/devices
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key's role lacks devices.view, or the key is revoked or expired. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/devices
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the devices.

500 response of GET /api/devices
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/devices" \
  -H "Authorization: Bearer <your key>"

GET/api/devices/{id}

Get a device

  • beta
  • Standard API key
  • Scope: read
  • Permission: devices.view
  • getDevice

One device, in the same projection as the list plus two keys only this route answers: performance (the most recent CPU, memory and disk reading, with when it was stored and whether it is stale) and antivirus (whether the device is protected, by the same rule as the console). Each is null when the agent never reported it. An API key always receives this projection; view=public names it explicitly and changes nothing for a key.

Parameters
Parameters of GET /api/devices/{id}
NameInTypeDescription
id requiredpathstringThe device id (Device.id).
viewquery"public"Optional for an API key, which always receives the published projection.
Responses
200

The device.

200 response of GET /api/devices/{id}
FieldTypeDescription
accessCapabilities requiredDeviceAccessCapabilities | nullWhich ways into the endpoint its agent reported it supports. Null when the agent never reported capabilities.
accessCapabilities.remoteDesktopV1trueA Linux agent that can serve a remote desktop session. Windows agents never send it: every Windows endpoint serves one.
accessCapabilities.remoteLoginV1trueA Linux agent on GNOME 46 or later that can open a Remote login: a separate, headless GNOME session through GDM, not the screen in front of the computer.
accessCapabilities.safeModeV1trueA Windows agent that can restart its endpoint into Safe Mode with Networking and back, with a failsafe that returns it to normal Windows if it cannot reach the server.
accessCapabilities.terminalV1trueA Linux agent whose remote terminal works.
accessCapabilities.toolsFilesV1trueA Linux agent whose file browser works.
agentBuild requiredstring | nullThe build stamp of the agent binary, as reported at its last authentication.
antivirus requiredDeviceAntivirus | nullAntivirus protection. Null when the agent never reported its security state.
antivirus.products requiredarray of DeviceAntivirusProductWhen source is securityCenter: the products Security Center reported (at most 16). When source is defender (Windows Server, which has no Security Center): one entry, Microsoft Defender Antivirus, judged from Defender's own status. Empty when source is none.
antivirus.products[].enabled requiredboolean | nullReal-time protection on. Null when not reported.
antivirus.products[].name requiredstring
antivirus.products[].upToDate requiredboolean | nullDefinitions current. Null when not reported.
antivirus.source required"securityCenter" | "defender" | "none"What decided: Windows Security Center (every registered product, any vendor), Microsoft Defender's own reading where Security Center is unavailable (Windows Server), or nothing.
antivirus.status required"protected" | "attention" | "unprotected" | "unknown"protected: a product is enabled and up to date. attention: products are registered but none is both. unprotected: Windows Security Center has no antivirus registered. unknown: nothing to judge.
approvalState requiredstringactive, or pending while the device waits in the Unassigned container for an operator to place it.
bootMode required"normal" | "safe-network" | "safe-minimal" | nullHow Windows booted, as the agent reported at its last authentication. Null when the agent never reported it (older agents, Linux).
createdAt requiredstring (date-time)
deviceUid requiredstring | nullThe permanent Denwen identity, in its presentation form. Survives rename and re-enrolment.
displayName requiredstring | nullThe friendly name an operator chose, if any.
effectiveName requiredstringdisplayName when set, otherwise name. What to show a person.
enforceSignedControl requiredbooleanWhether the agent accepts only signed control commands.
id requiredintegerThe device id used in every /api/devices/{id} path.
lastSeen requiredstring (date-time)
lifecycleState requiredstringactive, uninstalled or pending_delete. Independent of status.
loggedUser requiredstring | null
name requiredstringThe hostname the agent reports.
os requiredstring
patchPolicyId requiredinteger | null
performance requiredDevicePerformance | nullCurrent CPU, memory and disk use. Null when the agent never reported performance.
performance.at requiredstring (date-time) | nullWhen the server stored the reading. A reading stored before the server recorded that time is dated by the device's lastSeen and is always stale.
performance.cpuPercent requirednumber | nullWhole-machine CPU use, 0-100 to one decimal. Null when the agent did not measure it.
performance.disks requiredarray of DevicePerformanceDiskAt most 16 volumes, in the order the agent reported them.
performance.disks[].freeBytes requiredinteger
performance.disks[].mount requiredstring | nullC: on Windows; the mount point on Linux, with the directory that names a user (/home/<user>, /media/<user>, /run/media/<user>) replaced by *. Null when the agent reported something else.
performance.disks[].totalBytes requiredintegerVolume size. The agent reports sizes in GiB to two decimals, so byte counts are accurate to about 10 MB.
performance.disks[].usedPercent requirednumberCapacity used, 0-100 to one decimal.
performance.memoryPercent requirednumber | nullPhysical memory in use, 0-100 to one decimal.
performance.memoryTotalBytes requiredinteger | nullInstalled physical memory the operating system sees.
performance.stale requiredbooleanTrue when the numbers may no longer be current: the reading is more than two minutes old, undated, or the device is not online.
probeRole requiredbooleanWhether the device is nominated to run uptime checks.
purgeAfter requiredstring (date-time) | nullWhen an uninstalled device and its history will be deleted.
relayNode requiredDeviceRelayNode | null
relayNode.id requiredstring
relayNode.publicWsUrl requiredstringThe public WebSocket URL a remote session to this device connects through.
relayNode.status requiredstringonline, degraded, draining or offline.
serverTime requiredstring (date-time)The server clock when the response was built. Compare lastSeen against this, not the client clock.
site requiredDeviceSiteRef | null
site.customerId requiredinteger | nullThe customer (client organisation) the site belongs to.
site.id requiredinteger
site.name requiredstring
siteId requiredinteger
status requiredstringConnectivity only: online or offline.
telemetry requiredDeviceTelemetrySummary | null
telemetry.networkarray of object | objectNetwork adapters. Older agents wrap the array in data.
telemetry.patchesobjectThe last Windows Update scan: when, its outcome, and the updates it found missing.
telemetry.patches.rebootRequiredboolean | null
telemetry.patches.scanStatusstring | null
telemetry.patches.scannedAtstring | null
telemetry.patches.updates requiredarray of object
telemetry.patches.updates[].classificationstring | null
telemetry.patches.updates[].kbstring | null
telemetry.patches.updates[].titlestring | null
telemetry.systemobject | objectOperating system and agent facts, as the agent last reported them. Older agents wrap the object in data.
type requiredstringThe kind of endpoint, e.g. workstation, server or laptop.
uninstalledAt requiredstring (date-time) | null
updatedAt requiredstring (date-time)
uptime requiredstring | null
Example response
{
  "accessCapabilities": {},
  "agentBuild": "pkg:a1b2c3d4",
  "antivirus": {
    "products": [
      {
        "enabled": true,
        "name": "Microsoft Defender Antivirus",
        "upToDate": true
      }
    ],
    "source": "securityCenter",
    "status": "protected"
  },
  "approvalState": "active",
  "bootMode": null,
  "createdAt": "2026-08-07T10:12:00.000Z",
  "deviceUid": "DWN-7K2M-9QX4-RB3H-T6W3",
  "displayName": "Reception PC",
  "effectiveName": "Reception PC",
  "enforceSignedControl": true,
  "id": 101,
  "lastSeen": "2026-10-22T09:29:41.000Z",
  "lifecycleState": "active",
  "loggedUser": "ACME\\j.smith",
  "name": "ACME-WS01",
  "os": "Windows",
  "patchPolicyId": 3,
  "performance": {
    "at": "2026-10-22T09:29:38.000Z",
    "cpuPercent": 23.5,
    "disks": [
      {
        "freeBytes": 188141042401,
        "mount": "C:",
        "totalBytes": 511466180444,
        "usedPercent": 63.2
      },
      {
        "freeBytes": 871073054720,
        "mount": "D:",
        "totalBytes": 1000190509056,
        "usedPercent": 12.9
      }
    ],
    "memoryPercent": 40,
    "memoryTotalBytes": 17179869184,
    "stale": false
  },
  "probeRole": false,
  "purgeAfter": null,
  "relayNode": {
    "id": "relay-lon-1",
    "publicWsUrl": "wss://relay.example.test",
    "status": "online"
  },
  "serverTime": "2026-10-22T09:30:00.000Z",
  "site": {
    "customerId": 4,
    "id": 11,
    "name": "Head Office"
  },
  "siteId": 11,
  "status": "online",
  "telemetry": {
    "network": {
      "data": [
        {
          "gateway": "192.0.2.1",
          "ipv4": "192.0.2.41",
          "macAddress": "00-00-5E-00-53-29",
          "name": "Ethernet",
          "status": "Up"
        }
      ]
    },
    "patches": {
      "rebootRequired": false,
      "scanStatus": "ok",
      "scannedAt": "2026-10-22T03:00:00Z",
      "updates": [
        {
          "classification": "Security Updates",
          "kb": "KB5044285",
          "title": "2026-10 Cumulative Update for Windows 11"
        }
      ]
    },
    "system": {
      "data": {
        "agentVersion": "2.4.0",
        "build": "22631.4317",
        "buildNumber": "22631",
        "osName": "Windows 11 Pro",
        "osVersion": "23H2",
        "publicIp": "203.0.113.24"
      }
    }
  },
  "type": "workstation",
  "uninstalledAt": null,
  "updatedAt": "2026-10-22T09:29:41.000Z",
  "uptime": "3d 4h"
}
400

The id is not a number.

400 response of GET /api/devices/{id}
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/devices/{id}
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key's role lacks devices.view, or the key is revoked or expired. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/devices/{id}
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
404

No such device within the key's reach. A device in another organisation answers 404, not 403.

404 response of GET /api/devices/{id}
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the device.

500 response of GET /api/devices/{id}
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/devices/<id>" \
  -H "Authorization: Bearer <your key>"

GET/api/devices/{id}/alerts

List a device's alerts

  • beta
  • Standard API key
  • Scope: read
  • Permission: devices.view
  • listDeviceAlerts

Every alert raised on the device, newest first, as an unpaginated array.

Parameters
Parameters of GET /api/devices/{id}/alerts
NameInTypeDescription
id requiredpathstringThe device id (Device.id).
resolvedquery"true" | "false"Only resolved, or only open, alerts.
severityquerystringOnly this severity: critical, warning or info.
categoryquerystringOnly this category.
searchquerystringCase-insensitive match on message, category or affected component.
Responses
200

The alerts.

200 response of GET /api/devices/{id}/alerts
FieldTypeDescription
[].affectedComponent requiredstring | null
[].assignedTo requiredstring | nullWho acknowledged the alert. Null until someone does.
[].category requiredstring
[].createdAt requiredstring (date-time)
[].description requiredstring | null
[].deviceId requiredinteger | null
[].id requiredinteger
[].isResolved requiredboolean
[].maintenanceWindowIdinteger | nullThe alert maintenance window that held back its notifications, if one did.
[].message requiredstring
[].resolvedAtstring (date-time) | nullWhen the alert was resolved. Null while open, and on alerts resolved before 1.4.0 recorded it.
[].resolvedBystring | nullWho resolved it: an operator, PSA or Denwen MSP. Null when the platform resolved it (the condition cleared).
[].rootCause requiredstring | null
[].severity requiredstringcritical, warning or info.
[].snoozedBystring | nullWho snoozed it. Null when not snoozed.
[].snoozedUntilstring (date-time) | nullUntil when the alert is snoozed (still open, set aside). Null or in the past: not snoozed.
[].tenantId requiredinteger | nullSet only on an alert with no device; a device alert takes its organisation from the device.
Example response
[
  {
    "affectedComponent": "C:",
    "assignedTo": null,
    "category": "Disk",
    "createdAt": "2026-10-22T08:15:00.000Z",
    "description": "Free space on C: fell below the 10% threshold.",
    "deviceId": 101,
    "id": 5003,
    "isResolved": false,
    "message": "Disk C: is 93% full",
    "rootCause": null,
    "severity": "warning",
    "tenantId": null
  },
  {
    "affectedComponent": "Agent",
    "assignedTo": "api-key:Acme PSA",
    "category": "Connectivity",
    "createdAt": "2026-10-20T23:40:00.000Z",
    "description": null,
    "deviceId": 101,
    "id": 5001,
    "isResolved": true,
    "message": "Agent offline for 15 minutes",
    "rootCause": "Scheduled restart",
    "severity": "critical",
    "tenantId": null
  }
]
400

The id is not a number.

400 response of GET /api/devices/{id}/alerts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/devices/{id}/alerts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key is recognised but refused: it is revoked or expired, or it is read-only and the request writes. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/devices/{id}/alerts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
404

No such device within the key's reach, or the key's role lacks the permission at this device's site. The two are not distinguished.

404 response of GET /api/devices/{id}/alerts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/devices/<id>/alerts" \
  -H "Authorization: Bearer <your key>"

POST/api/devices/{id}/alerts/{alertId}/acknowledge

Acknowledge a device alert

  • beta
  • Standard API key
  • Scope: full
  • Permission: devices.manage
  • acknowledgeDeviceAlert

Records that someone has taken the alert. assignedTo is set from the caller's identity (for a key, api-key:<key name>); no request body is read. Answers the device's alerts, newest first, after the change.

Parameters
Parameters of POST /api/devices/{id}/alerts/{alertId}/acknowledge
NameInTypeDescription
id requiredpathstringThe device id (Device.id).
alertId requiredpathstringThe alert id (DeviceAlert.id). Must belong to this device.
Responses
200

The device's alerts after the change.

200 response of POST /api/devices/{id}/alerts/{alertId}/acknowledge
FieldTypeDescription
[].affectedComponent requiredstring | null
[].assignedTo requiredstring | nullWho acknowledged the alert. Null until someone does.
[].category requiredstring
[].createdAt requiredstring (date-time)
[].description requiredstring | null
[].deviceId requiredinteger | null
[].id requiredinteger
[].isResolved requiredboolean
[].maintenanceWindowIdinteger | nullThe alert maintenance window that held back its notifications, if one did.
[].message requiredstring
[].resolvedAtstring (date-time) | nullWhen the alert was resolved. Null while open, and on alerts resolved before 1.4.0 recorded it.
[].resolvedBystring | nullWho resolved it: an operator, PSA or Denwen MSP. Null when the platform resolved it (the condition cleared).
[].rootCause requiredstring | null
[].severity requiredstringcritical, warning or info.
[].snoozedBystring | nullWho snoozed it. Null when not snoozed.
[].snoozedUntilstring (date-time) | nullUntil when the alert is snoozed (still open, set aside). Null or in the past: not snoozed.
[].tenantId requiredinteger | nullSet only on an alert with no device; a device alert takes its organisation from the device.
Example response
[
  {
    "affectedComponent": "C:",
    "assignedTo": "api-key:Acme PSA",
    "category": "Disk",
    "createdAt": "2026-10-22T08:15:00.000Z",
    "description": "Free space on C: fell below the 10% threshold.",
    "deviceId": 101,
    "id": 5003,
    "isResolved": false,
    "message": "Disk C: is 93% full",
    "rootCause": null,
    "severity": "warning",
    "tenantId": null
  },
  {
    "affectedComponent": "Agent",
    "assignedTo": "api-key:Acme PSA",
    "category": "Connectivity",
    "createdAt": "2026-10-20T23:40:00.000Z",
    "description": null,
    "deviceId": 101,
    "id": 5001,
    "isResolved": true,
    "message": "Agent offline for 15 minutes",
    "rootCause": "Scheduled restart",
    "severity": "critical",
    "tenantId": null
  }
]
400

An id is not a number.

400 response of POST /api/devices/{id}/alerts/{alertId}/acknowledge
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of POST /api/devices/{id}/alerts/{alertId}/acknowledge
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key is recognised but refused: it is revoked or expired, or it is read-only and the request writes. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of POST /api/devices/{id}/alerts/{alertId}/acknowledge
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
404

No such device within the key's reach, the key's role lacks devices.manage at this device's site, or no such alert on this device.

404 response of POST /api/devices/{id}/alerts/{alertId}/acknowledge
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl -X POST "https://<your instance>/api/devices/<id>/alerts/<alertId>/acknowledge" \
  -H "Authorization: Bearer <your key>"

POST/api/devices/{id}/alerts/{alertId}/resolve

Resolve a device alert

  • beta
  • Standard API key
  • Scope: full
  • Permission: devices.manage
  • resolveDeviceAlert

Marks the alert resolved and closes any PSA ticket raised for it (queued; the response does not wait for the PSA). No request body is read. Answers the device's alerts, newest first, after the change.

Parameters
Parameters of POST /api/devices/{id}/alerts/{alertId}/resolve
NameInTypeDescription
id requiredpathstringThe device id (Device.id).
alertId requiredpathstringThe alert id (DeviceAlert.id). Must belong to this device.
Responses
200

The device's alerts after the change.

200 response of POST /api/devices/{id}/alerts/{alertId}/resolve
FieldTypeDescription
[].affectedComponent requiredstring | null
[].assignedTo requiredstring | nullWho acknowledged the alert. Null until someone does.
[].category requiredstring
[].createdAt requiredstring (date-time)
[].description requiredstring | null
[].deviceId requiredinteger | null
[].id requiredinteger
[].isResolved requiredboolean
[].maintenanceWindowIdinteger | nullThe alert maintenance window that held back its notifications, if one did.
[].message requiredstring
[].resolvedAtstring (date-time) | nullWhen the alert was resolved. Null while open, and on alerts resolved before 1.4.0 recorded it.
[].resolvedBystring | nullWho resolved it: an operator, PSA or Denwen MSP. Null when the platform resolved it (the condition cleared).
[].rootCause requiredstring | null
[].severity requiredstringcritical, warning or info.
[].snoozedBystring | nullWho snoozed it. Null when not snoozed.
[].snoozedUntilstring (date-time) | nullUntil when the alert is snoozed (still open, set aside). Null or in the past: not snoozed.
[].tenantId requiredinteger | nullSet only on an alert with no device; a device alert takes its organisation from the device.
Example response
[
  {
    "affectedComponent": "C:",
    "assignedTo": "api-key:Acme PSA",
    "category": "Disk",
    "createdAt": "2026-10-22T08:15:00.000Z",
    "description": "Free space on C: fell below the 10% threshold.",
    "deviceId": 101,
    "id": 5003,
    "isResolved": true,
    "message": "Disk C: is 93% full",
    "resolvedBy": "api-key:Acme PSA",
    "rootCause": null,
    "severity": "warning",
    "tenantId": null
  },
  {
    "affectedComponent": "Agent",
    "assignedTo": "api-key:Acme PSA",
    "category": "Connectivity",
    "createdAt": "2026-10-20T23:40:00.000Z",
    "description": null,
    "deviceId": 101,
    "id": 5001,
    "isResolved": true,
    "message": "Agent offline for 15 minutes",
    "rootCause": "Scheduled restart",
    "severity": "critical",
    "tenantId": null
  }
]
400

An id is not a number.

400 response of POST /api/devices/{id}/alerts/{alertId}/resolve
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of POST /api/devices/{id}/alerts/{alertId}/resolve
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key is recognised but refused: it is revoked or expired, or it is read-only and the request writes. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of POST /api/devices/{id}/alerts/{alertId}/resolve
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
404

No such device within the key's reach, the key's role lacks devices.manage at this device's site, or no such alert on this device.

404 response of POST /api/devices/{id}/alerts/{alertId}/resolve
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl -X POST "https://<your instance>/api/devices/<id>/alerts/<alertId>/resolve" \
  -H "Authorization: Bearer <your key>"

GET/api/devices/{id}/app-updates

Get a device's outdated applications

  • beta
  • Standard API key
  • Scope: read
  • Permission: patch.view
  • getDeviceAppUpdates

Third-party applications on the device with a newer version available. Computed on read from the device's last software inventory and winget scan and the software catalogue, so a catalogue change shows at once without a rescan.

Parameters
Parameters of GET /api/devices/{id}/app-updates
NameInTypeDescription
id requiredpathstringThe device id (Device.id).
Responses
200

The outdated applications and the state of the last winget scan.

200 response of GET /api/devices/{id}/app-updates
FieldTypeDescription
apps requiredarray of objectOutdated applications, one per install.
apps[].architecturestring | null
apps[].available requiredstringThe version available.
apps[].catalogueId requiredinteger | null
apps[].installed requiredstringThe installed version.
apps[].installerSha256string | null
apps[].installerUrlstring | null
apps[].name requiredstring
apps[].publisher requiredstring | null
apps[].scope requiredstringmachine, or user:<DOMAIN\name> for an install belonging to one user.
apps[].silentFlagsstring | null
apps[].source required"winget" | "catalogue"Who said it is outdated: winget on the device, or the software catalogue.
apps[].wingetId requiredstring | null
architecturestring | nullThe device's processor architecture, when reported.
winget requiredobject
winget.detail requiredstring | null
winget.scannedAt requiredstring | null
winget.stale requiredbooleanTrue when the last scan is old enough that the list may no longer be true.
winget.status required"ok" | "unavailable" | "failed" | "never"
winget.users requiredarray of objectOne entry per signed-in user whose own installs were scanned.
winget.users[].detail requiredstring | null
winget.users[].found requiredinteger
winget.users[].scope requiredstring
winget.users[].status required"ok" | "failed"
Example response
{
  "apps": [
    {
      "architecture": "x64",
      "available": "130.0.6723.70",
      "catalogueId": 8,
      "installed": "129.0.6668.90",
      "installerSha256": null,
      "installerUrl": "https://dl.google.com/chrome/install/googlechromestandaloneenterprise64.msi",
      "name": "Google Chrome",
      "publisher": "Google LLC",
      "scope": "machine",
      "silentFlags": "/qn",
      "source": "catalogue",
      "wingetId": null
    },
    {
      "available": "8.7",
      "catalogueId": null,
      "installed": "8.6.9",
      "name": "Notepad++",
      "publisher": null,
      "scope": "machine",
      "source": "winget",
      "wingetId": "Notepad++.Notepad++"
    }
  ],
  "architecture": null,
  "winget": {
    "detail": null,
    "scannedAt": "2026-10-22T02:00:00Z",
    "stale": false,
    "status": "ok",
    "users": [
      {
        "detail": null,
        "found": 0,
        "scope": "user:ACME\\j.smith",
        "status": "ok"
      }
    ]
  }
}
400

The id is not a number.

400 response of GET /api/devices/{id}/app-updates
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/devices/{id}/app-updates
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key is recognised but refused: it is revoked or expired, or it is read-only and the request writes. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/devices/{id}/app-updates
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
404

No such device within the key's reach, or the key's role lacks the permission at this device's site. The two are not distinguished.

404 response of GET /api/devices/{id}/app-updates
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/devices/<id>/app-updates" \
  -H "Authorization: Bearer <your key>"

GET/api/devices/{id}/patches/deployments

List patch jobs on a device

  • beta
  • Standard API key
  • Scope: read
  • Permission: patch.view
  • listDevicePatchDeployments

The 20 most recent Windows Update install jobs on the device, newest first.

Parameters
Parameters of GET /api/devices/{id}/patches/deployments
NameInTypeDescription
id requiredpathstringThe device id (Device.id).
statusquerystringOnly jobs with this status.
Responses
200

The jobs.

200 response of GET /api/devices/{id}/patches/deployments
FieldTypeDescription
[].attemptinteger1 for the first install; a retry the patch policy queued carries the next number.
[].automationTaskIdinteger | nullThe automation that queued this install for a maintenance window, if one did.
[].completedAt requiredstring (date-time) | null
[].createdAt requiredstring (date-time)
[].detail requiredstring | null
[].deviceId requiredinteger
[].failureAlertIdinteger | nullThe alert raised when this failure episode ended in failure.
[].id requiredinteger
[].operatorEmail requiredstring | null
[].rebootCompletedAtstring (date-time) | nullWhen the endpoint restarted (the boot time it reported), or a scan found no restart pending.
[].rebootDeadlineAtstring (date-time) | nullAfter this the restart happens regardless of the window; null when the policy sets no deadline.
[].rebootDetailstring | nullWhy, in a sentence.
[].rebootDueAtstring (date-time) | nullWhen a waiting restart is next looked at (the reboot window, or the deadline).
[].rebootRequired requiredboolean
[].rebootSentAtstring (date-time) | null
[].rebootStatusstring | nullWhat the patch policy did about a restart this install needed: skipped, waiting, sent, completed, unconfirmed or failed; null when none was needed.
[].requestId requiredstringThe correlation id of the signed install command.
[].requestedUpdates requiredarray of stringThe Windows Update ids requested.
[].results requiredarray of object | nullPer-update outcome, once the agent reports it.
[].results[].hresultinteger | string
[].results[].resultCodeinteger
[].results[].titlestring
[].results[].updateIdstring
[].retryOfIdinteger | nullFor a retry: the id of the first attempt of this failure episode.
[].scheduledFor requiredstring (date-time) | nullFor a deferred job: when the maintenance window next opens.
[].startedAt requiredstring (date-time) | null
[].status requiredstringdeferred, queued, accepted, downloading, installing, succeeded, succeeded_reboot_required or failed.
[].updatedAt requiredstring (date-time)
Example response
[
  {
    "completedAt": "2026-10-21T22:41:02.000Z",
    "createdAt": "2026-10-21T22:00:00.000Z",
    "detail": "1 of 1 installed; restart required",
    "deviceId": 101,
    "deviceUid": "7K2M9QX4RB3HT6W3",
    "id": 412,
    "operatorEmail": "alex@acme.test",
    "operatorId": 3,
    "rebootRequired": true,
    "requestId": "req-8d20e1",
    "requestedUpdates": [
      "wua-guid-1"
    ],
    "results": [
      {
        "hresult": 0,
        "resultCode": 2,
        "title": "2026-10 Cumulative Update for Windows 11",
        "updateId": "wua-guid-1"
      }
    ],
    "scheduledFor": null,
    "startedAt": "2026-10-21T22:00:30.000Z",
    "status": "succeeded_reboot_required",
    "updatedAt": "2026-10-21T22:41:02.000Z"
  }
]
400

The id is not a number.

400 response of GET /api/devices/{id}/patches/deployments
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/devices/{id}/patches/deployments
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key is recognised but refused: it is revoked or expired, or it is read-only and the request writes. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/devices/{id}/patches/deployments
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
404

No such device within the key's reach, or the key's role lacks the permission at this device's site. The two are not distinguished.

404 response of GET /api/devices/{id}/patches/deployments
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/devices/<id>/patches/deployments" \
  -H "Authorization: Bearer <your key>"

GET/api/devices/{id}/scripts

List script runs on a device

  • beta
  • Standard API key
  • Scope: read
  • Permission: automation.view
  • listDeviceScriptRuns

The most recent remote script runs on the device, newest first, with their output.

Parameters
Parameters of GET /api/devices/{id}/scripts
NameInTypeDescription
id requiredpathstringThe device id (Device.id).
limitquerystringHow many runs, 1 to 100. Default 25.
statusquerystringOnly runs with this status.
searchquerystringCase-insensitive match on operator, script text or request id.
Responses
200

The runs.

200 response of GET /api/devices/{id}/scripts
FieldTypeDescription
[].createdAt requiredstring (date-time)
[].durationMs requiredinteger | null
[].exitCode requiredinteger | null
[].finishedAt requiredstring (date-time) | null
[].id requiredinteger
[].integrationAction requiredobject | nullSet when the run came from a Denwen MSP ticket action (contract 1.4.0 §18); null for every other run.
[].integrationAction.id requiredstringThe ticket action, act_ + 26 characters.
[].integrationAction.ticketNumber requiredstring | null
[].integrationAction.ticketUrl requiredstring | nullThe Denwen MSP ticket; null unless it is on the webhook origin of the pairing.
[].operatorEmail requiredstring | nullWho ran it. Null for a scheduled run.
[].requestId requiredstring | null
[].script requiredstring
[].shell requiredstringpowershell on Windows, sh on Linux.
[].startedAt requiredstring (date-time)
[].status requiredstringsuccess, failed, timeout, offline, unauthorized, not-capable, unconfigured or error.
[].stderr requiredstring | null
[].stdout requiredstring | null
[].truncated requiredbooleanTrue when stdout or stderr was cut to the size limit.
Example response
[
  {
    "createdAt": "2026-10-22T08:02:10.000Z",
    "durationMs": 1840,
    "exitCode": 0,
    "finishedAt": "2026-10-22T08:02:12.000Z",
    "id": 3101,
    "integrationAction": null,
    "operatorEmail": "alex@acme.test",
    "requestId": "req-3f1c9a",
    "script": "Get-Service Spooler | Restart-Service -PassThru",
    "shell": "powershell",
    "startedAt": "2026-10-22T08:02:10.000Z",
    "status": "success",
    "stderr": "",
    "stdout": "Status   Name     DisplayName\r\n------   ----     -----------\r\nRunning  Spooler  Print Spooler",
    "truncated": false
  }
]
400

The id is not a number.

400 response of GET /api/devices/{id}/scripts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/devices/{id}/scripts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key is recognised but refused: it is revoked or expired, or it is read-only and the request writes. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/devices/{id}/scripts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
404

No such device within the key's reach, or the key's role lacks the permission at this device's site. The two are not distinguished.

404 response of GET /api/devices/{id}/scripts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/devices/<id>/scripts" \
  -H "Authorization: Bearer <your key>"

GET/api/devices/{id}/timeline

Get a device's timeline

  • beta
  • Standard API key
  • Scope: read
  • Permission: devices.view
  • getDeviceTimeline

The device's merged history, newest first: activity, alerts, agent events, script runs, patch jobs and audited actions on this device. Each source is capped at its most recent entries (a few hundred), so this is recent history, not a complete archive.

Parameters
Parameters of GET /api/devices/{id}/timeline
NameInTypeDescription
id requiredpathstringThe device id (Device.id).
Responses
200

The timeline.

200 response of GET /api/devices/{id}/timeline
FieldTypeDescription
[].category requiredstringAlerts, Scripts, Patches, Remote Sessions, Logins, Reboots, Software or Agent Events.
[].detailsstring
[].id requiredstringUnique within the timeline. The prefix names the source (act-, alt-, evt-, scr-, dep-, aud-); treat it as opaque.
[].operator requiredstringWho did it, or System.
[].severity requiredstringcritical, warning or info.
[].summary requiredstring
[].timestamp requiredstring (date-time)
Example response
[
  {
    "category": "Alerts",
    "details": "Affected Component: C:",
    "id": "alt-5003",
    "operator": "System",
    "severity": "warning",
    "summary": "Alert Triggered: Disk C: is 93% full",
    "timestamp": "2026-10-22T08:15:00.000Z"
  },
  {
    "category": "Scripts",
    "details": "Get-Service Spooler | Restart-Service -PassThru  ·  1840 ms",
    "id": "scr-3101",
    "operator": "alex@acme.test",
    "severity": "info",
    "summary": "Ran powershell script — success (exit 0)",
    "timestamp": "2026-10-22T08:02:10.000Z"
  },
  {
    "category": "Agent Events",
    "details": "Internal activity tracking",
    "id": "act-901",
    "operator": "Alex Morgan",
    "severity": "info",
    "summary": "Remote session started by alex@acme.test",
    "timestamp": "2026-10-22T07:58:00.000Z"
  },
  {
    "category": "Logins",
    "details": "{\"session\":1}",
    "id": "evt-7001",
    "operator": "System",
    "severity": "info",
    "summary": "ACME\\j.smith signed in",
    "timestamp": "2026-10-22T07:45:00.000Z"
  },
  {
    "category": "Reboots",
    "id": "aud-66001",
    "operator": "alex@acme.test",
    "severity": "info",
    "summary": "restart requested",
    "timestamp": "2026-10-21T23:30:00.000Z"
  },
  {
    "category": "Reboots",
    "details": "1 of 1 installed; restart required",
    "id": "dep-412",
    "operator": "System",
    "severity": "info",
    "summary": "Deployment #412 — succeeded reboot required (1 update)",
    "timestamp": "2026-10-21T22:00:00.000Z"
  },
  {
    "category": "Alerts",
    "details": "Affected Component: Agent",
    "id": "alt-5001",
    "operator": "System",
    "severity": "critical",
    "summary": "Alert Triggered: Agent offline for 15 minutes",
    "timestamp": "2026-10-20T23:40:00.000Z"
  }
]
400

The id is not a number.

400 response of GET /api/devices/{id}/timeline
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/devices/{id}/timeline
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key is recognised but refused: it is revoked or expired, or it is read-only and the request writes. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/devices/{id}/timeline
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
404

No such device within the key's reach, or the key's role lacks the permission at this device's site. The two are not distinguished.

404 response of GET /api/devices/{id}/timeline
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/devices/<id>/timeline" \
  -H "Authorization: Bearer <your key>"

Organisation

The organisation tree (customers, sites) and support sessions.

GET/api/customers

List customers with their sites and endpoints

  • beta
  • Standard API key
  • Scope: read
  • Permission: devices.view
  • listCustomers

Every customer (client organisation) of the key's organisation, by name, each with its sites and the endpoints at each site that are not uninstalled. A provider (global) key is refused (403 TENANT_REQUIRED, or PROVIDER_ACCESS_REQUIRED with the tenant header). A key whose role is confined to some sites sees only those sites, and only the customers that own one of them. Each endpoint is the published Device projection. An API key always receives this shape; view=public names it explicitly and changes nothing for a key. A bare array; not paginated.

Parameters
Parameters of GET /api/customers
NameInTypeDescription
viewquery"public"Optional for an API key, which always receives the published shape.
Responses
200

The customers, their sites and their endpoints.

200 response of GET /api/customers
FieldTypeDescription
[].accountRef requiredstringThe platform-wide account reference a person reads back on the telephone.
[].addressLine1 requiredstring | null
[].addressLine2 requiredstring | null
[].city requiredstring | null
[].contactEmail requiredstring | null
[].contactName requiredstring | null
[].contactPhone requiredstring | null
[].country requiredstring | null
[].createdAt requiredstring (date-time)
[].id requiredinteger
[].name requiredstringUnique within the organisation, not across the platform.
[].notes requiredstringFree-text notes an operator keeps on the record. Empty when none.
[].postcode requiredstring | null
[].region requiredstring | null
[].sites requiredarray of CustomerSiteThe customer's sites the key may see, by name.
[].sites[].addressLine1 requiredstring | null
[].sites[].addressLine2 requiredstring | null
[].sites[].city requiredstring | null
[].sites[].contactEmail requiredstring | null
[].sites[].contactName requiredstring | null
[].sites[].contactPhone requiredstring | null
[].sites[].country requiredstring | null
[].sites[].createdAt requiredstring (date-time)
[].sites[].customerId requiredinteger
[].sites[].devices requiredarray of DeviceThe site's endpoints that are not uninstalled, by id.
[].sites[].devices[].accessCapabilities requiredDeviceAccessCapabilities | nullWhich ways into the endpoint its agent reported it supports. Null when the agent never reported capabilities.
[].sites[].devices[].accessCapabilities.remoteDesktopV1trueA Linux agent that can serve a remote desktop session. Windows agents never send it: every Windows endpoint serves one.
[].sites[].devices[].accessCapabilities.remoteLoginV1trueA Linux agent on GNOME 46 or later that can open a Remote login: a separate, headless GNOME session through GDM, not the screen in front of the computer.
[].sites[].devices[].accessCapabilities.safeModeV1trueA Windows agent that can restart its endpoint into Safe Mode with Networking and back, with a failsafe that returns it to normal Windows if it cannot reach the server.
[].sites[].devices[].accessCapabilities.terminalV1trueA Linux agent whose remote terminal works.
[].sites[].devices[].accessCapabilities.toolsFilesV1trueA Linux agent whose file browser works.
[].sites[].devices[].agentBuild requiredstring | nullThe build stamp of the agent binary, as reported at its last authentication.
[].sites[].devices[].approvalState requiredstringactive, or pending while the device waits in the Unassigned container for an operator to place it.
[].sites[].devices[].bootMode required"normal" | "safe-network" | "safe-minimal" | nullHow Windows booted, as the agent reported at its last authentication. Null when the agent never reported it (older agents, Linux).
[].sites[].devices[].createdAt requiredstring (date-time)
[].sites[].devices[].deviceUid requiredstring | nullThe permanent Denwen identity, in its presentation form. Survives rename and re-enrolment.
[].sites[].devices[].displayName requiredstring | nullThe friendly name an operator chose, if any.
[].sites[].devices[].effectiveName requiredstringdisplayName when set, otherwise name. What to show a person.
[].sites[].devices[].enforceSignedControl requiredbooleanWhether the agent accepts only signed control commands.
[].sites[].devices[].id requiredintegerThe device id used in every /api/devices/{id} path.
[].sites[].devices[].lastSeen requiredstring (date-time)
[].sites[].devices[].lifecycleState requiredstringactive, uninstalled or pending_delete. Independent of status.
[].sites[].devices[].loggedUser requiredstring | null
[].sites[].devices[].name requiredstringThe hostname the agent reports.
[].sites[].devices[].os requiredstring
[].sites[].devices[].patchPolicyId requiredinteger | null
[].sites[].devices[].probeRole requiredbooleanWhether the device is nominated to run uptime checks.
[].sites[].devices[].purgeAfter requiredstring (date-time) | nullWhen an uninstalled device and its history will be deleted.
[].sites[].devices[].relayNode requiredDeviceRelayNode | null
[].sites[].devices[].relayNode.id requiredstring
[].sites[].devices[].relayNode.publicWsUrl requiredstringThe public WebSocket URL a remote session to this device connects through.
[].sites[].devices[].relayNode.status requiredstringonline, degraded, draining or offline.
[].sites[].devices[].serverTime requiredstring (date-time)The server clock when the response was built. Compare lastSeen against this, not the client clock.
[].sites[].devices[].site requiredDeviceSiteRef | null
[].sites[].devices[].site.customerId requiredinteger | nullThe customer (client organisation) the site belongs to.
[].sites[].devices[].site.id requiredinteger
[].sites[].devices[].site.name requiredstring
[].sites[].devices[].siteId requiredinteger
[].sites[].devices[].status requiredstringConnectivity only: online or offline.
[].sites[].devices[].telemetry requiredDeviceTelemetrySummary | null
[].sites[].devices[].telemetry.networkarray of object | objectNetwork adapters. Older agents wrap the array in data.
[].sites[].devices[].telemetry.patchesobjectThe last Windows Update scan: when, its outcome, and the updates it found missing.
[].sites[].devices[].telemetry.patches.rebootRequiredboolean | null
[].sites[].devices[].telemetry.patches.scanStatusstring | null
[].sites[].devices[].telemetry.patches.scannedAtstring | null
[].sites[].devices[].telemetry.patches.updates requiredarray of object
[].sites[].devices[].telemetry.systemobject | objectOperating system and agent facts, as the agent last reported them. Older agents wrap the object in data.
[].sites[].devices[].type requiredstringThe kind of endpoint, e.g. workstation, server or laptop.
[].sites[].devices[].uninstalledAt requiredstring (date-time) | null
[].sites[].devices[].updatedAt requiredstring (date-time)
[].sites[].devices[].uptime requiredstring | null
[].sites[].id requiredinteger
[].sites[].kind requiredstringnormal, or unassigned: the reserved site where endpoints land when their installer named no site. Endpoints there accept no commands until an administrator places them.
[].sites[].name requiredstring
[].sites[].notes requiredstringFree-text notes an operator keeps on the record. Empty when none.
[].sites[].postcode requiredstring | null
[].sites[].preferredClusterId requiredstring | nullThe relay cluster remote sessions at this site prefer, when one is set.
[].sites[].region requiredstring | null
[].sites[].requireSessionConsent requiredboolean | nullWhether a remote session at this site must be accepted by the signed-in user. Null inherits the organisation's setting.
[].sites[].siteRef requiredstring | nullThe operator's own label for the place, e.g. a building code or store number.
[].sites[].updatedAt requiredstring (date-time)
[].tenantId requiredintegerThe organisation (tenant) the customer belongs to.
[].updatedAt requiredstring (date-time)
[].website requiredstring | null
Example response
[
  {
    "accountRef": "ACC-7K2M-9QX4",
    "addressLine1": "1 King Street",
    "addressLine2": null,
    "city": "London",
    "contactEmail": "it@abc-ltd.example",
    "contactName": "Priya Shah",
    "contactPhone": "+44 20 7946 0000",
    "country": "United Kingdom",
    "createdAt": "2026-04-04T10:00:00.000Z",
    "id": 10,
    "name": "ABC Ltd",
    "notes": "",
    "postcode": "EC2V 8AU",
    "region": null,
    "sites": [
      {
        "addressLine1": "1 King Street",
        "addressLine2": null,
        "city": "London",
        "contactEmail": null,
        "contactName": null,
        "contactPhone": null,
        "country": "United Kingdom",
        "createdAt": "2026-04-04T10:00:00.000Z",
        "customerId": 10,
        "devices": [
          {
            "accessCapabilities": {},
            "agentBuild": "pkg:a1b2c3d4",
            "approvalState": "active",
            "bootMode": null,
            "createdAt": "2026-07-23T10:00:00.000Z",
            "deviceUid": "DWN-7K2M-9QX4-RB3H-T6W0",
            "displayName": "Reception PC",
            "effectiveName": "Reception PC",
            "enforceSignedControl": true,
            "id": 1001,
            "lastSeen": "2026-10-21T09:59:00.000Z",
            "lifecycleState": "active",
            "loggedUser": "ABC\\p.shah",
            "name": "LON-WS01",
            "os": "Windows",
            "patchPolicyId": 3,
            "probeRole": false,
            "purgeAfter": null,
            "relayNode": {
              "id": "relay-lon-1",
              "publicWsUrl": "wss://relay-lon-1.example.test",
              "status": "online"
            },
            "serverTime": "2026-10-21T10:00:00.000Z",
            "site": {
              "customerId": 10,
              "id": 100,
              "name": "London"
            },
            "siteId": 100,
            "status": "online",
            "telemetry": {
              "network": [
                {
                  "gateway": "10.20.0.1",
                  "ipv4": "10.20.0.21",
                  "macAddress": "00-15-5D-01-02-03",
                  "name": "Ethernet",
                  "status": "Up"
                }
              ],
              "system": {
                "agentVersion": "2.4.0",
                "buildNumber": "22631",
                "osName": "Windows 11 Pro",
                "osVersion": "23H2",
                "publicIp": "203.0.113.24"
              }
            },
            "type": "workstation",
            "uninstalledAt": null,
            "updatedAt": "2026-10-21T09:59:00.000Z",
            "uptime": "3d 4h"
          },
          {
            "accessCapabilities": {},
            "agentBuild": "pkg:a1b2c3d4",
            "approvalState": "active",
            "bootMode": null,
            "createdAt": "2026-07-23T10:00:00.000Z",
            "deviceUid": "DWN-7K2M-9QX4-RB3H-T6W1",
            "displayName": null,
            "effectiveName": "LON-WS02",
            "enforceSignedControl": true,
            "id": 1002,
            "lastSeen": "2026-10-19T10:00:00.000Z",
            "lifecycleState": "active",
            "loggedUser": null,
            "name": "LON-WS02",
            "os": "Windows",
            "patchPolicyId": null,
            "probeRole": false,
            "purgeAfter": null,
            "relayNode": null,
            "serverTime": "2026-10-21T10:00:00.000Z",
            "site": {
              "customerId": 10,
              "id": 100,
              "name": "London"
            },
            "siteId": 100,
            "status": "offline",
            "telemetry": {
              "network": [
                {
                  "gateway": "10.20.0.1",
                  "ipv4": "10.20.0.21",
                  "macAddress": "00-15-5D-01-02-03",
                  "name": "Ethernet",
                  "status": "Up"
                }
              ],
              "system": {
                "agentVersion": "2.4.0",
                "buildNumber": "22631",
                "osName": "Windows 11 Pro",
                "osVersion": "23H2",
                "publicIp": "203.0.113.24"
              }
            },
            "type": "workstation",
            "uninstalledAt": null,
            "updatedAt": "2026-10-21T09:59:00.000Z",
            "uptime": null
          },
          {
            "accessCapabilities": {},
            "agentBuild": "pkg:a1b2c3d4",
            "approvalState": "active",
            "bootMode": null,
            "createdAt": "2026-07-23T10:00:00.000Z",
            "deviceUid": "DWN-7K2M-9QX4-RB3H-T6W2",
            "displayName": null,
            "effectiveName": "LON-SRV01",
            "enforceSignedControl": true,
            "id": 1003,
            "lastSeen": "2026-10-21T09:59:00.000Z",
            "lifecycleState": "active",
            "loggedUser": null,
            "name": "LON-SRV01",
            "os": "Windows",
            "patchPolicyId": null,
            "probeRole": true,
            "purgeAfter": null,
            "relayNode": null,
            "serverTime": "2026-10-21T10:00:00.000Z",
            "site": {
              "customerId": 10,
              "id": 100,
              "name": "London"
            },
            "siteId": 100,
            "status": "online",
            "telemetry": {
              "network": [
                {
                  "gateway": "10.20.0.1",
                  "ipv4": "10.20.0.21",
                  "macAddress": "00-15-5D-01-02-03",
                  "name": "Ethernet",
                  "status": "Up"
                }
              ],
              "system": {
                "agentVersion": "2.4.0",
                "buildNumber": "22631",
                "osName": "Windows 11 Pro",
                "osVersion": "23H2",
                "publicIp": "203.0.113.24"
              }
            },
            "type": "server",
            "uninstalledAt": null,
            "updatedAt": "2026-10-21T09:59:00.000Z",
            "uptime": null
          }
        ],
        "id": 100,
        "kind": "normal",
        "name": "London",
        "notes": "",
        "postcode": "EC2V 8AU",
        "preferredClusterId": "eu-west",
        "region": null,
        "requireSessionConsent": true,
        "siteRef": "LON-01",
        "updatedAt": "2026-10-01T10:00:00.000Z"
      },
      {
        "addressLine1": null,
        "addressLine2": null,
        "city": null,
        "contactEmail": null,
        "contactName": null,
        "contactPhone": null,
        "country": null,
        "createdAt": "2026-04-04T10:00:00.000Z",
        "customerId": 10,
        "devices": [
          {
            "accessCapabilities": {},
            "agentBuild": "pkg:a1b2c3d4",
            "approvalState": "pending",
            "bootMode": null,
            "createdAt": "2026-07-23T10:00:00.000Z",
            "deviceUid": "DWN-7K2M-9QX4-RB3H-T6W3",
            "displayName": null,
            "effectiveName": "DESKTOP-NEW",
            "enforceSignedControl": true,
            "id": 1004,
            "lastSeen": "2026-10-21T09:59:00.000Z",
            "lifecycleState": "active",
            "loggedUser": null,
            "name": "DESKTOP-NEW",
            "os": "Windows",
            "patchPolicyId": null,
            "probeRole": false,
            "purgeAfter": null,
            "relayNode": null,
            "serverTime": "2026-10-21T10:00:00.000Z",
            "site": {
              "customerId": 10,
              "id": 101,
              "name": "Unassigned"
            },
            "siteId": 101,
            "status": "online",
            "telemetry": {
              "network": [
                {
                  "gateway": "10.20.0.1",
                  "ipv4": "10.20.0.21",
                  "macAddress": "00-15-5D-01-02-03",
                  "name": "Ethernet",
                  "status": "Up"
                }
              ],
              "system": {
                "agentVersion": "2.4.0",
                "buildNumber": "22631",
                "osName": "Windows 11 Pro",
                "osVersion": "23H2",
                "publicIp": "203.0.113.24"
              }
            },
            "type": "workstation",
            "uninstalledAt": null,
            "updatedAt": "2026-10-21T09:59:00.000Z",
            "uptime": null
          }
        ],
        "id": 101,
        "kind": "unassigned",
        "name": "Unassigned",
        "notes": "",
        "postcode": null,
        "preferredClusterId": null,
        "region": null,
        "requireSessionConsent": null,
        "siteRef": null,
        "updatedAt": "2026-10-01T10:00:00.000Z"
      }
    ],
    "tenantId": 1,
    "updatedAt": "2026-10-01T10:00:00.000Z",
    "website": "https://abc-ltd.example"
  },
  {
    "accountRef": "ACC-3HB8-T6WZ",
    "addressLine1": null,
    "addressLine2": null,
    "city": null,
    "contactEmail": null,
    "contactName": null,
    "contactPhone": null,
    "country": null,
    "createdAt": "2026-04-04T10:00:00.000Z",
    "id": 11,
    "name": "Northwind Traders",
    "notes": "Quarterly on-site visit.",
    "postcode": null,
    "region": null,
    "sites": [
      {
        "addressLine1": null,
        "addressLine2": null,
        "city": null,
        "contactEmail": null,
        "contactName": null,
        "contactPhone": null,
        "country": null,
        "createdAt": "2026-04-04T10:00:00.000Z",
        "customerId": 11,
        "devices": [
          {
            "accessCapabilities": {},
            "agentBuild": "pkg:a1b2c3d4",
            "approvalState": "active",
            "bootMode": null,
            "createdAt": "2026-07-23T10:00:00.000Z",
            "deviceUid": "DWN-7K2M-9QX4-RB3H-T6W4",
            "displayName": null,
            "effectiveName": "MAN-WS01",
            "enforceSignedControl": true,
            "id": 1005,
            "lastSeen": "2026-10-21T09:59:00.000Z",
            "lifecycleState": "active",
            "loggedUser": null,
            "name": "MAN-WS01",
            "os": "Windows",
            "patchPolicyId": null,
            "probeRole": false,
            "purgeAfter": null,
            "relayNode": null,
            "serverTime": "2026-10-21T10:00:00.000Z",
            "site": {
              "customerId": 11,
              "id": 102,
              "name": "Manchester"
            },
            "siteId": 102,
            "status": "online",
            "telemetry": {
              "network": [
                {
                  "gateway": "10.20.0.1",
                  "ipv4": "10.20.0.21",
                  "macAddress": "00-15-5D-01-02-03",
                  "name": "Ethernet",
                  "status": "Up"
                }
              ],
              "system": {
                "agentVersion": "2.4.0",
                "buildNumber": "22631",
                "osName": "Windows 11 Pro",
                "osVersion": "23H2",
                "publicIp": "203.0.113.24"
              }
            },
            "type": "workstation",
            "uninstalledAt": null,
            "updatedAt": "2026-10-21T09:59:00.000Z",
            "uptime": null
          }
        ],
        "id": 102,
        "kind": "normal",
        "name": "Manchester",
        "notes": "",
        "postcode": null,
        "preferredClusterId": null,
        "region": null,
        "requireSessionConsent": null,
        "siteRef": null,
        "updatedAt": "2026-10-01T10:00:00.000Z"
      }
    ],
    "tenantId": 1,
    "updatedAt": "2026-10-01T10:00:00.000Z",
    "website": null
  }
]
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/customers
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key cannot do this: revoked, expired, or its role lacks the permission. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/customers
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the data.

500 response of GET /api/customers
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/customers" \
  -H "Authorization: Bearer <your key>"

GET/api/hierarchy/search

Search the organisation tree

  • beta
  • Standard API key
  • Scope: read
  • Permission: devices.view
  • searchOrganisationTree

Case-insensitive name search across organisations, customers and sites, scoped exactly like the tree. Each hit carries its path and the ids of every level above it. At most 25 hits per level. A query under 2 characters returns no results and tooShort: true.

Parameters
Parameters of GET /api/hierarchy/search
NameInTypeDescription
qquerystringThe text to find in a name. At least 2 characters after trimming.
Responses
200

The matching nodes.

200 response of GET /api/hierarchy/search
FieldTypeDescription
query requiredstringThe search text as used, trimmed.
results requiredarray of HierarchySearchResultOrganisations first, then customers, then sites.
results[].context requiredobjectThe ids of every level above and including this node.
results[].context.customerId requiredinteger | null
results[].context.siteId requiredinteger | null
results[].context.tenantId requiredinteger
results[].id requiredinteger
results[].kind required"tenant" | "customer" | "site"
results[].name requiredstring
results[].path requiredarray of stringNames from the organisation down to this node, e.g. ["Acme MSP", "ABC Ltd", "Manchester"].
tooShortbooleanPresent (true) when the query was under 2 characters; results is then empty.
truncatedbooleanTrue when at least 25 nodes matched; each level returns at most 25. Absent when tooShort.
Example response
{
  "query": "an",
  "results": [
    {
      "context": {
        "customerId": 11,
        "siteId": 102,
        "tenantId": 1
      },
      "id": 102,
      "kind": "site",
      "name": "Manchester",
      "path": [
        "Acme MSP",
        "Northwind Traders",
        "Manchester"
      ]
    }
  ],
  "truncated": false
}
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/hierarchy/search
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key cannot do this: revoked, expired, or its role lacks the permission. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/hierarchy/search
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the data.

500 response of GET /api/hierarchy/search
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/hierarchy/search" \
  -H "Authorization: Bearer <your key>"

GET/api/hierarchy/tree

Get the organisation tree

  • beta
  • Standard API key
  • Scope: read
  • Permission: devices.view
  • getOrganisationTree

Organisations, their customers and their sites, each with live endpoint counts (uninstalled endpoints are not counted). A key sees exactly the organisation it belongs to, so tenants has one entry and scope is always tenant. A provider (global) key is refused (403 TENANT_REQUIRED, or PROVIDER_ACCESS_REQUIRED with the tenant header): provider keys reach platform administration only. A key whose role is confined to some sites sees only those (siteRestricted). Devices are not a level of the tree; list them from the Devices operations.

Responses
200

The tree.

200 response of GET /api/hierarchy/tree
FieldTypeDescription
isGlobalPrincipal requiredbooleanAlways false for an API key: provider keys are refused this route.
scope required"tenant"Always tenant for an API key: the key belongs to one organisation and exactly that one is listed. (A provider key, which belongs to no organisation, is refused 403 TENANT_REQUIRED before this route runs, so a key never receives any other value.)
siteRestricted requiredbooleanWhether the key's role is confined to some sites, so the tree shows only those.
tenants requiredarray of HierarchyTenantNodeOrganisation, customer and site levels. Devices are not a level; each site carries counts.
tenants[].children requiredarray of HierarchyCustomerNode
tenants[].children[].children requiredarray of HierarchySiteNodeThe customer's sites the caller may see.
tenants[].children[].children[].counts requiredobject
tenants[].children[].children[].counts.devices requiredintegerEndpoints at this site that are not uninstalled.
tenants[].children[].children[].counts.offline requiredintegerdevices - online.
tenants[].children[].children[].counts.online requiredinteger
tenants[].children[].children[].customerId requiredinteger
tenants[].children[].children[].id requiredinteger
tenants[].children[].children[].kind required"site"
tenants[].children[].children[].name requiredstring
tenants[].children[].children[].siteKind requiredstringnormal, or unassigned: the reserved site where endpoints land when their installer named no site. Endpoints there accept no commands until an administrator places them.
tenants[].children[].counts requiredobject
tenants[].children[].counts.devices requiredintegerThe sum of the sites' devices counts.
tenants[].children[].counts.online requiredinteger
tenants[].children[].counts.sites requiredinteger
tenants[].children[].id requiredinteger
tenants[].children[].kind required"customer"
tenants[].children[].name requiredstring
tenants[].children[].tenantId requiredinteger
tenants[].counts requiredobject
tenants[].counts.customers requiredinteger
tenants[].counts.devices requiredinteger
tenants[].counts.online requiredinteger
tenants[].counts.sites requiredinteger
tenants[].id requiredinteger
tenants[].kind required"tenant"
tenants[].name requiredstring
tenants[].status requiredstringThe organisation's status, e.g. active.
Example response
{
  "isGlobalPrincipal": false,
  "scope": "tenant",
  "siteRestricted": false,
  "tenants": [
    {
      "children": [
        {
          "children": [
            {
              "counts": {
                "devices": 3,
                "offline": 1,
                "online": 2
              },
              "customerId": 10,
              "id": 100,
              "kind": "site",
              "name": "London",
              "siteKind": "normal"
            },
            {
              "counts": {
                "devices": 1,
                "offline": 0,
                "online": 1
              },
              "customerId": 10,
              "id": 101,
              "kind": "site",
              "name": "Unassigned",
              "siteKind": "unassigned"
            }
          ],
          "counts": {
            "devices": 4,
            "online": 3,
            "sites": 2
          },
          "id": 10,
          "kind": "customer",
          "name": "ABC Ltd",
          "tenantId": 1
        },
        {
          "children": [
            {
              "counts": {
                "devices": 1,
                "offline": 0,
                "online": 1
              },
              "customerId": 11,
              "id": 102,
              "kind": "site",
              "name": "Manchester",
              "siteKind": "normal"
            }
          ],
          "counts": {
            "devices": 1,
            "online": 1,
            "sites": 1
          },
          "id": 11,
          "kind": "customer",
          "name": "Northwind Traders",
          "tenantId": 1
        }
      ],
      "counts": {
        "customers": 2,
        "devices": 5,
        "online": 4,
        "sites": 3
      },
      "id": 1,
      "kind": "tenant",
      "name": "Acme MSP",
      "status": "active"
    }
  ]
}
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/hierarchy/tree
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key cannot do this: revoked, expired, or its role lacks the permission. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/hierarchy/tree
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the data.

500 response of GET /api/hierarchy/tree
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/hierarchy/tree" \
  -H "Authorization: Bearer <your key>"

GET/api/sessions

List support sessions

  • beta
  • Standard API key
  • Scope: read
  • Permission: sessions.view
  • listSupportSessions

The ad-hoc support session queue of the key's organisation. By default only open sessions are returned, newest first, as a bare array. includeClosed=true adds closed ones (at most 200 in the unpaged form). Sending page, pageSize or sortBy switches to a paged envelope (rows, total, page, ...). A key whose role is confined to some sites sees no sessions: an ad-hoc session has no site, so a site-restricted caller sees only the sessions it raised, and a key raises none. The join code is never returned to an API key, because it admits whoever holds it, and search does not match it.

Parameters
Parameters of GET /api/sessions
NameInTypeDescription
includeClosedquery"true" | "false"true to include closed sessions.
statusquerystringOnly sessions with this stored status, e.g. connected.
searchquerystringCase-insensitive match on the client name, or the raising operator's name or email.
pagequerystring1-based page number. Sending it returns the paged envelope.
pageSizequerystringRows per page, 1 to 200 (default 50).
sortByquery"createdAt" | "status" | "clientName" | "connectedAt" | "closedAt"Sort column (default createdAt). An unknown value falls back to the default.
sortDirquery"asc" | "desc"Sort direction (default desc).
Responses
200

A bare array of sessions, or a paged envelope when any paging parameter is sent.

Body: array of SupportSession | SupportSessionPage

Example response
[
  {
    "clientName": null,
    "closedAt": null,
    "connectedAt": null,
    "createdAt": "2026-10-21T09:56:00.000Z",
    "createdBy": 5,
    "duration": 0,
    "endedReason": null,
    "hasHelperLog": false,
    "id": 71,
    "os": null,
    "phase": "waiting",
    "platform": null,
    "relayNodeId": null,
    "secondsRemaining": 360,
    "status": "waiting",
    "technicianEmail": "sam.taylor@acme-msp.example",
    "technicianName": "Sam Taylor",
    "tenantId": 1,
    "ttlMinutes": 10
  },
  {
    "clientName": "Reception PC",
    "closedAt": null,
    "connectedAt": "2026-10-21T09:40:00.000Z",
    "createdAt": "2026-10-21T09:35:00.000Z",
    "createdBy": 6,
    "duration": 540,
    "endedReason": null,
    "hasHelperLog": false,
    "id": 70,
    "os": "Windows 11 Pro",
    "phase": "connected",
    "platform": "windows",
    "relayNodeId": "relay-lon-1",
    "secondsRemaining": null,
    "status": "connected",
    "technicianEmail": "alex@acme-msp.example",
    "technicianName": "alex@acme-msp.example",
    "tenantId": 1,
    "ttlMinutes": 10
  }
]
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/sessions
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key cannot do this: revoked, expired, or its role lacks the permission. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/sessions
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the data.

500 response of GET /api/sessions
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/sessions" \
  -H "Authorization: Bearer <your key>"

Monitoring

Dashboard alerts and statistics, uptime checks and alert rules.

GET/api/alert-rules

List alert rules

  • beta
  • Standard API key
  • Scope: read
  • Permission: alertrules.view
  • listAlertRules

The alert rules that apply in the key's context, newest first: the organisation's own and the provider's (scope: global), which govern every organisation's endpoints. This is platform administration, so a provider (global) key may read it and sees the provider's rules only; with the tenant header it is refused 403 PROVIDER_ACCESS_REQUIRED. Unpaginated.

Responses
200

The rules.

200 response of GET /api/alert-rules
FieldTypeDescription
[].channel requiredstring | nullFor event-log rules: Security, System or Application.
[].consecutiveCount requiredintegerHow many consecutive readings must breach.
[].createdAt requiredstring (date-time)
[].durationSeconds requiredintegerHow long the condition must hold before the rule fires.
[].editable requiredbooleanWhether the caller's context may change it.
[].eventIds requiredstring | nullFor event-log rules: comma-separated event ids, e.g. 4625,4624.
[].eventProvider requiredstring | nullFor event-log rules: the provider name.
[].id requiredinteger
[].isEnabled requiredboolean
[].metric requiredstringWhat the rule watches, e.g. cpu, memory, disk, customEvent. GET /api/alert-rules/catalogue lists them.
[].name requiredstring
[].operator requiredstring>, < or =.
[].scope required"global" | "tenant"global for a provider-level rule, which applies to every organisation's endpoints.
[].severity requiredstringwarning or critical.
[].tenantId requiredinteger | nullThe owning organisation; null for a provider-level rule.
[].updatedAt requiredstring (date-time)
[].value requirednumberThe threshold.
Example response
[
  {
    "channel": "Security",
    "consecutiveCount": 1,
    "createdAt": "2026-10-19T10:00:00.000Z",
    "durationSeconds": 0,
    "editable": true,
    "eventIds": "4625",
    "eventProvider": "Microsoft-Windows-Security-Auditing",
    "id": 41,
    "isEnabled": true,
    "metric": "customEvent",
    "name": "Failed logons burst",
    "operator": ">",
    "scope": "tenant",
    "severity": "critical",
    "tenantId": 1,
    "updatedAt": "2026-10-19T10:00:00.000Z",
    "value": 5
  },
  {
    "channel": null,
    "consecutiveCount": 3,
    "createdAt": "2026-10-11T10:00:00.000Z",
    "durationSeconds": 300,
    "editable": true,
    "eventIds": null,
    "eventProvider": null,
    "id": 40,
    "isEnabled": true,
    "metric": "disk",
    "name": "Disk nearly full",
    "operator": ">",
    "scope": "tenant",
    "severity": "warning",
    "tenantId": 1,
    "updatedAt": "2026-10-16T10:00:00.000Z",
    "value": 90
  },
  {
    "channel": null,
    "consecutiveCount": 1,
    "createdAt": "2026-07-23T10:00:00.000Z",
    "durationSeconds": 600,
    "editable": false,
    "eventIds": null,
    "eventProvider": null,
    "id": 1,
    "isEnabled": true,
    "metric": "cpu",
    "name": "CPU sustained above 90%",
    "operator": ">",
    "scope": "global",
    "severity": "warning",
    "tenantId": null,
    "updatedAt": "2026-07-23T10:00:00.000Z",
    "value": 90
  }
]
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/alert-rules
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key cannot do this: revoked, expired, or its role lacks the permission. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/alert-rules
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the data.

500 response of GET /api/alert-rules
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/alert-rules" \
  -H "Authorization: Bearer <your key>"

GET/api/dashboard/alerts

List the latest critical alerts

  • beta
  • Standard API key
  • Scope: read
  • Permission: devices.view
  • listDashboardAlerts

The 10 most recent unresolved critical alerts in the key's scope, newest first: alerts on managed (not uninstalled) endpoints, and website and port check alerts, which have no device. For a device's full alert history use the Devices operations.

Responses
200

Up to 10 alerts.

200 response of GET /api/dashboard/alerts
FieldTypeDescription
[].affectedComponent requiredstring | nullWhat raised it, e.g. a metric or check key.
[].assignedTo requiredstring | null
[].category requiredstringe.g. System, Security, Availability (website and port checks).
[].createdAt requiredstring (date-time)
[].description requiredstring | null
[].device requiredobject | nullThe endpoint's name; null when deviceId is null.
[].device.name requiredstring
[].deviceId requiredinteger | nullThe endpoint it is about; null for a website or port check alert.
[].id requiredinteger
[].isResolved requiredbooleanAlways false on this route.
[].maintenanceWindowIdinteger | nullThe alert maintenance window that held back its notifications, if one did.
[].message requiredstring
[].rootCause requiredstring | null
[].severity requiredstringAlways critical on this route.
[].tenantId requiredinteger | nullThe organisation of a device-less alert; may be null on device alerts.
Example response
[
  {
    "affectedComponent": "uptime:301",
    "assignedTo": null,
    "category": "Availability",
    "createdAt": "2026-10-21T09:30:00.000Z",
    "description": "HTTP check failed 2 times in a row: expected any 2xx/3xx, got 503.",
    "device": null,
    "deviceId": null,
    "id": 502,
    "isResolved": false,
    "message": "https://shop.abc-ltd.example is down",
    "rootCause": null,
    "severity": "critical",
    "tenantId": 1
  },
  {
    "affectedComponent": "disk",
    "assignedTo": null,
    "category": "System",
    "createdAt": "2026-10-21T08:00:00.000Z",
    "description": "Free space on C: fell below 5%.",
    "device": {
      "name": "LON-SRV01"
    },
    "deviceId": 1003,
    "id": 501,
    "isResolved": false,
    "message": "Disk C: is 96% full on LON-SRV01",
    "rootCause": null,
    "severity": "critical",
    "tenantId": null
  }
]
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/dashboard/alerts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key cannot do this: revoked, expired, or its role lacks the permission. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/dashboard/alerts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the data.

500 response of GET /api/dashboard/alerts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/dashboard/alerts" \
  -H "Authorization: Bearer <your key>"

GET/api/dashboard/stats

Get headline estate figures

  • beta
  • Standard API key
  • Scope: read
  • Permission: devices.view
  • getDashboardStats

Endpoint, session and critical-alert counts for the estate in the key's scope. Uninstalled endpoints and alerts about them are excluded, so criticalAlerts counts the same set GET /api/dashboard/alerts lists from.

Responses
200

The figures.

200 response of GET /api/dashboard/stats
FieldTypeDescription
activeSessions requiredintegerSupport sessions currently connected.
avgDuration requiredintegerMean duration of ended support sessions, in whole seconds (0 when there are none).
completedToday requiredintegerSupport sessions ended since midnight (server time).
criticalAlerts requiredintegerUnresolved critical alerts: the same set GET /api/dashboard/alerts lists from.
onlineDevices requiredintegerOf those, how many are online now.
totalEndpoints requiredintegerManaged endpoints in scope (uninstalled ones excluded).
Example response
{
  "activeSessions": 1,
  "avgDuration": 1260,
  "completedToday": 1,
  "criticalAlerts": 2,
  "onlineDevices": 4,
  "totalEndpoints": 5
}
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/dashboard/stats
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key cannot do this: revoked, expired, or its role lacks the permission. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/dashboard/stats
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the data.

500 response of GET /api/dashboard/stats
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/dashboard/stats" \
  -H "Authorization: Bearer <your key>"

GET/api/uptime-checks

List website and port checks

  • beta
  • Standard API key
  • Scope: read
  • Permission: uptimechecks.view
  • listUptimeChecks

Every check the key's context owns, by name, with its latest state and its availability over 24 hours and 7 days: the checks of the key's organisation. A provider (global) key is refused (403 TENANT_REQUIRED). SNMP community strings and passwords are never returned; hasCommunity, hasSnmpAuthPassword and hasSnmpPrivPassword say whether one is stored. Unpaginated.

Responses
200

The checks.

200 response of GET /api/uptime-checks
FieldTypeDescription
[].compareOp required">" | "<" | "=" | "!=" | nullSNMP: how the reading is compared with compareValue.
[].compareValue requirednumber | null
[].consecutiveFailures requiredinteger
[].createdAt requiredstring (date-time)
[].editable requiredbooleanWhether the caller's context may change it.
[].expectStatus requiredintegerHTTP: the status expected; 0 accepts any 2xx/3xx.
[].failThreshold requiredintegerConsecutive failures before an alert is raised.
[].hasCommunity requiredbooleanWhether an SNMP community is stored. The value itself is never returned.
[].hasSnmpAuthPassword requiredbooleanWhether an SNMPv3 auth password is stored. Never returned.
[].hasSnmpPrivPassword requiredbooleanWhether an SNMPv3 privacy password is stored. Never returned.
[].host requiredstring
[].id requiredinteger
[].intervalSec requiredinteger
[].isEnabled requiredboolean
[].kind required"http" | "tcp" | "snmp"snmp checks run on a probe endpoint only.
[].lastCode requiredinteger | nullHTTP: the last status code.
[].lastError requiredstring | null
[].lastLatencyMs requiredinteger | null
[].lastOkAt requiredstring (date-time) | null
[].lastRunAt requiredstring (date-time) | null
[].lastState requiredstringup, down or unknown.
[].lastText requiredstring | nullSNMP: the last reading as text.
[].lastValue requirednumber | nullSNMP: the last numeric reading.
[].name requiredstring
[].oid requiredstring | nullSNMP: the OID read.
[].openAlertId requiredinteger | nullThe alert currently open for this check, if any.
[].port requiredinteger
[].probeDeviceId requiredinteger | nullThe endpoint that runs the check; null when the server runs it.
[].scope required"global" | "tenant"global for a provider-level check, tenant for an organisation's own.
[].severity requiredstringThe severity of the alert it raises: warning or critical.
[].snmpAuthProtocol requiredstring | null
[].snmpContext requiredstring | null
[].snmpCredentialId requiredstring | nullSNMP: a stored credential the check uses instead of its own.
[].snmpPrivProtocol requiredstring | null
[].snmpSecurityLevel requiredstring | null
[].snmpUsername requiredstring | null
[].snmpVersion requiredinteger | null
[].target requiredstringWhat the check probes, as entered (URL or host:port).
[].tenantId requiredinteger | nullThe owning organisation; null for a provider-level check.
[].timeoutMs requiredinteger
[].updatedAt requiredstring (date-time)
[].uptime24h requirednumber | nullPercentage of successful results over 24 hours; null with no results.
[].uptime7d requirednumber | nullPercentage of successful results over 7 days; null with no results.
Example response
[
  {
    "compareOp": null,
    "compareValue": null,
    "consecutiveFailures": 3,
    "createdAt": "2026-09-21T10:00:00.000Z",
    "editable": true,
    "expectStatus": 0,
    "failThreshold": 2,
    "hasCommunity": false,
    "hasSnmpAuthPassword": false,
    "hasSnmpPrivPassword": false,
    "host": "shop.abc-ltd.example",
    "id": 301,
    "intervalSec": 300,
    "isEnabled": true,
    "kind": "http",
    "lastCode": 503,
    "lastError": "expected any 2xx/3xx, got 503",
    "lastLatencyMs": 212,
    "lastOkAt": "2026-10-21T09:20:00.000Z",
    "lastRunAt": "2026-10-21T09:58:00.000Z",
    "lastState": "down",
    "lastText": null,
    "lastValue": null,
    "name": "ABC Ltd web shop",
    "oid": null,
    "openAlertId": 502,
    "port": 443,
    "probeDeviceId": null,
    "scope": "tenant",
    "severity": "critical",
    "snmpAuthProtocol": null,
    "snmpContext": null,
    "snmpCredentialId": null,
    "snmpPrivProtocol": null,
    "snmpSecurityLevel": null,
    "snmpUsername": null,
    "snmpVersion": 2,
    "target": "https://shop.abc-ltd.example",
    "tenantId": 1,
    "timeoutMs": 10000,
    "updatedAt": "2026-10-20T10:00:00.000Z",
    "uptime24h": 75,
    "uptime7d": 85
  },
  {
    "compareOp": "=",
    "compareValue": 1,
    "consecutiveFailures": 0,
    "createdAt": "2026-09-21T10:00:00.000Z",
    "editable": true,
    "expectStatus": 0,
    "failThreshold": 2,
    "hasCommunity": true,
    "hasSnmpAuthPassword": false,
    "hasSnmpPrivPassword": false,
    "host": "10.20.0.1",
    "id": 302,
    "intervalSec": 300,
    "isEnabled": true,
    "kind": "snmp",
    "lastCode": null,
    "lastError": null,
    "lastLatencyMs": null,
    "lastOkAt": "2026-10-21T09:58:00.000Z",
    "lastRunAt": "2026-10-21T09:58:00.000Z",
    "lastState": "up",
    "lastText": "up(1)",
    "lastValue": 1,
    "name": "LON core switch uplink",
    "oid": "1.3.6.1.2.1.2.2.1.8.1",
    "openAlertId": null,
    "port": 161,
    "probeDeviceId": 1003,
    "scope": "tenant",
    "severity": "warning",
    "snmpAuthProtocol": null,
    "snmpContext": null,
    "snmpCredentialId": null,
    "snmpPrivProtocol": null,
    "snmpSecurityLevel": null,
    "snmpUsername": null,
    "snmpVersion": 2,
    "target": "10.20.0.1",
    "tenantId": 1,
    "timeoutMs": 10000,
    "updatedAt": "2026-10-20T10:00:00.000Z",
    "uptime24h": 100,
    "uptime7d": 100
  },
  {
    "compareOp": null,
    "compareValue": null,
    "consecutiveFailures": 0,
    "createdAt": "2026-09-21T10:00:00.000Z",
    "editable": true,
    "expectStatus": 0,
    "failThreshold": 2,
    "hasCommunity": false,
    "hasSnmpAuthPassword": false,
    "hasSnmpPrivPassword": false,
    "host": "mail.northwind.example",
    "id": 303,
    "intervalSec": 300,
    "isEnabled": false,
    "kind": "tcp",
    "lastCode": null,
    "lastError": null,
    "lastLatencyMs": 38,
    "lastOkAt": null,
    "lastRunAt": null,
    "lastState": "unknown",
    "lastText": null,
    "lastValue": null,
    "name": "Northwind mail",
    "oid": null,
    "openAlertId": null,
    "port": 25,
    "probeDeviceId": null,
    "scope": "tenant",
    "severity": "warning",
    "snmpAuthProtocol": null,
    "snmpContext": null,
    "snmpCredentialId": null,
    "snmpPrivProtocol": null,
    "snmpSecurityLevel": null,
    "snmpUsername": null,
    "snmpVersion": 2,
    "target": "mail.northwind.example:25",
    "tenantId": 1,
    "timeoutMs": 10000,
    "updatedAt": "2026-10-20T10:00:00.000Z",
    "uptime24h": null,
    "uptime7d": null
  }
]
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/uptime-checks
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key cannot do this: revoked, expired, or its role lacks the permission. A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/uptime-checks
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
500

The server could not read the data.

500 response of GET /api/uptime-checks
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/uptime-checks" \
  -H "Authorization: Bearer <your key>"

Patching

Patch policies, compliance and application update rollouts.

GET/api/app-update-rollouts

List application update rollouts

  • beta
  • Standard API key
  • Scope: read
  • Permission: patch.view
  • listAppUpdateRollouts

Phased (canary, soak, waves) third-party application update rollouts in the key's organisation. Provider-wide rollouts are not listed to an organisation's key: they span other organisations' endpoints.

Parameters
Parameters of GET /api/app-update-rollouts
NameInTypeDescription
statusquery"canary" | "soaking" | "rolling" | "completed" | "halted" | "cancelled"Only rollouts in this state.
Responses
200

The rollouts, newest first.

200 response of GET /api/app-update-rollouts
FieldTypeDescription
[]._count requiredobjectHow many per-device deployments the rollout has dispatched.
[]._count.deployments requiredinteger
[].batchSize requiredintegerDevices per wave.
[].canaryDeviceIds requiredarray of integerDevices in the canary phase.
[].canaryGroupIds requiredarray of integerDevice groups in the canary phase.
[].completedAt requiredstring (date-time) | null
[].createdAt requiredstring (date-time)
[].currentWave requiredinteger
[].detail requiredstring | nullWhy the rollout is in its current state, e.g. the halt reason.
[].haltThresholdPercent requirednumberFailure percentage in a wave that halts the rollout.
[].id requiredinteger
[].name requiredstring
[].operatorEmail requiredstring | nullWho started the rollout.
[].operatorId requiredinteger | null
[].requestedPackages requiredarray of objectThe application updates the rollout installs.
[].requestedPackages[].fromVersionstring
[].requestedPackages[].id requiredstringThe package identifier (winget id or catalogue id).
[].requestedPackages[].namestring
[].requestedPackages[].toVersionstring
[].soakExpiresAt requiredstring (date-time) | null
[].soakHours requiredintegerHours the canary soaks before the waves start.
[].startedAt requiredstring (date-time) | null
[].status required"canary" | "soaking" | "rolling" | "completed" | "halted" | "cancelled"
[].targetDeviceIds requiredarray of integerDevices the rollout waves target explicitly.
[].targetGroupIds requiredarray of integerDevice groups the rollout waves target.
[].tenantId requiredinteger | nullThe owning organisation; null for a provider-wide rollout.
[].totalWaves requiredinteger
[].updatedAt requiredstring (date-time)
Example response
[
  {
    "_count": {
      "deployments": 1
    },
    "batchSize": 10,
    "canaryDeviceIds": [
      102
    ],
    "canaryGroupIds": [],
    "completedAt": null,
    "createdAt": "2026-10-21T08:00:00.000Z",
    "currentWave": 0,
    "detail": "Canary succeeded; soaking for 24 h.",
    "haltThresholdPercent": 10,
    "id": 41,
    "name": "Firefox 131",
    "operatorEmail": "ops@acme.example",
    "operatorId": 1,
    "requestedPackages": [
      {
        "fromVersion": "130.0.1",
        "id": "Mozilla.Firefox",
        "name": "Mozilla Firefox",
        "toVersion": "131.0.2"
      }
    ],
    "soakExpiresAt": "2026-10-22T08:00:00.000Z",
    "soakHours": 24,
    "startedAt": "2026-10-21T08:00:00.000Z",
    "status": "soaking",
    "targetDeviceIds": [],
    "targetGroupIds": [
      5
    ],
    "tenantId": 2,
    "totalWaves": 2,
    "updatedAt": "2026-10-21T08:20:00.000Z"
  },
  {
    "_count": {
      "deployments": 1
    },
    "batchSize": 10,
    "canaryDeviceIds": [
      101,
      103
    ],
    "canaryGroupIds": [],
    "completedAt": null,
    "createdAt": "2026-10-18T10:00:00.000Z",
    "currentWave": 0,
    "detail": "Halted: canary failure rate 50% exceeds 20%.",
    "haltThresholdPercent": 20,
    "id": 40,
    "name": "7-Zip 24.08",
    "operatorEmail": "ops@acme.example",
    "operatorId": 1,
    "requestedPackages": [
      {
        "fromVersion": "23.01",
        "id": "7zip.7zip",
        "name": "7-Zip",
        "toVersion": "24.08"
      }
    ],
    "soakExpiresAt": null,
    "soakHours": 24,
    "startedAt": "2026-10-18T10:00:00.000Z",
    "status": "halted",
    "targetDeviceIds": [],
    "targetGroupIds": [],
    "tenantId": 2,
    "totalWaves": 0,
    "updatedAt": "2026-10-18T11:05:00.000Z"
  }
]
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/app-update-rollouts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key was recognised but may not do this (revoked, expired, or its role lacks patch.view). A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/app-update-rollouts
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/app-update-rollouts" \
  -H "Authorization: Bearer <your key>"

GET/api/app-update-rollouts/{id}

Get an application update rollout

  • beta
  • Standard API key
  • Scope: read
  • Permission: patch.view
  • getAppUpdateRollout

One rollout with every per-device deployment it has dispatched.

Parameters
Parameters of GET /api/app-update-rollouts/{id}
NameInTypeDescription
id requiredpathintegerThe rollout id.
Responses
200

The rollout and its deployments.

200 response of GET /api/app-update-rollouts/{id}
FieldTypeDescription
batchSize requiredintegerDevices per wave.
canaryDeviceIds requiredarray of integerDevices in the canary phase.
canaryGroupIds requiredarray of integerDevice groups in the canary phase.
completedAt requiredstring (date-time) | null
createdAt requiredstring (date-time)
currentWave requiredinteger
deployments requiredarray of AppUpdateDeploymentEvery per-device deployment, newest first.
deployments[].completedAt requiredstring (date-time) | null
deployments[].createdAt requiredstring (date-time)
deployments[].detail requiredstring | null
deployments[].deviceId requiredinteger
deployments[].deviceUid requiredstring | null
deployments[].id requiredinteger
deployments[].operatorEmail requiredstring | null
deployments[].operatorId requiredinteger | null
deployments[].rebootRequired requiredboolean
deployments[].requestId requiredstringThe signed command's request id.
deployments[].requestedPackages requiredarray of objectWhat the server signed for this device.
deployments[].requestedPackages[].fromVersionstring
deployments[].requestedPackages[].id requiredstringThe package identifier (winget id or catalogue id).
deployments[].requestedPackages[].namestring
deployments[].requestedPackages[].toVersionstring
deployments[].results requiredarray of object | nullPer-package outcome reported by the agent (versions, exit code, reboot, reason); null until it reports.
deployments[].results[].id requiredstring
deployments[].results[].status requiredstring
deployments[].rolloutId requiredinteger | null
deployments[].scheduledFor requiredstring (date-time) | null
deployments[].scope requiredstringmachine or user.
deployments[].startedAt requiredstring (date-time) | null
deployments[].status requiredstringdeferred, queued, accepted, installing, succeeded, partial, failed or abandoned.
deployments[].updatedAt requiredstring (date-time)
detail requiredstring | nullWhy the rollout is in its current state, e.g. the halt reason.
haltThresholdPercent requirednumberFailure percentage in a wave that halts the rollout.
id requiredinteger
name requiredstring
operatorEmail requiredstring | nullWho started the rollout.
operatorId requiredinteger | null
requestedPackages requiredarray of objectThe application updates the rollout installs.
requestedPackages[].fromVersionstring
requestedPackages[].id requiredstringThe package identifier (winget id or catalogue id).
requestedPackages[].namestring
requestedPackages[].toVersionstring
soakExpiresAt requiredstring (date-time) | null
soakHours requiredintegerHours the canary soaks before the waves start.
startedAt requiredstring (date-time) | null
status required"canary" | "soaking" | "rolling" | "completed" | "halted" | "cancelled"
targetDeviceIds requiredarray of integerDevices the rollout waves target explicitly.
targetGroupIds requiredarray of integerDevice groups the rollout waves target.
tenantId requiredinteger | nullThe owning organisation; null for a provider-wide rollout.
totalWaves requiredinteger
updatedAt requiredstring (date-time)
Example response
{
  "batchSize": 10,
  "canaryDeviceIds": [
    102
  ],
  "canaryGroupIds": [],
  "completedAt": null,
  "createdAt": "2026-10-21T08:00:00.000Z",
  "currentWave": 0,
  "deployments": [
    {
      "completedAt": "2026-10-21T08:06:00.000Z",
      "createdAt": "2026-10-21T08:00:30.000Z",
      "detail": null,
      "deviceId": 102,
      "deviceUid": "D-4HPA-2RTB",
      "id": 901,
      "operatorEmail": "ops@acme.example",
      "operatorId": 1,
      "rebootRequired": false,
      "requestId": "f1d2c3b4-0000-4000-8000-000000000901",
      "requestedPackages": [
        {
          "fromVersion": "130.0.1",
          "id": "Mozilla.Firefox",
          "name": "Mozilla Firefox",
          "toVersion": "131.0.2"
        }
      ],
      "results": [
        {
          "exitCode": 0,
          "fromVersion": "130.0.1",
          "id": "Mozilla.Firefox",
          "installedVersion": "131.0.2",
          "rebootRequired": false,
          "status": "succeeded",
          "toVersion": "131.0.2"
        }
      ],
      "rolloutId": 41,
      "scheduledFor": null,
      "scope": "machine",
      "startedAt": "2026-10-21T08:01:00.000Z",
      "status": "succeeded",
      "updatedAt": "2026-10-21T08:06:00.000Z"
    }
  ],
  "detail": "Canary succeeded; soaking for 24 h.",
  "haltThresholdPercent": 10,
  "id": 41,
  "name": "Firefox 131",
  "operatorEmail": "ops@acme.example",
  "operatorId": 1,
  "requestedPackages": [
    {
      "fromVersion": "130.0.1",
      "id": "Mozilla.Firefox",
      "name": "Mozilla Firefox",
      "toVersion": "131.0.2"
    }
  ],
  "soakExpiresAt": "2026-10-22T08:00:00.000Z",
  "soakHours": 24,
  "startedAt": "2026-10-21T08:00:00.000Z",
  "status": "soaking",
  "targetDeviceIds": [],
  "targetGroupIds": [
    5
  ],
  "tenantId": 2,
  "totalWaves": 2,
  "updatedAt": "2026-10-21T08:20:00.000Z"
}
400

The id is not a positive integer.

400 response of GET /api/app-update-rollouts/{id}
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/app-update-rollouts/{id}
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key was recognised but may not do this (revoked, expired, or its role lacks patch.view). A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/app-update-rollouts/{id}
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
404

No such rollout within the key's reach (another organisation's, or a provider-wide one).

404 response of GET /api/app-update-rollouts/{id}
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/app-update-rollouts/<id>" \
  -H "Authorization: Bearer <your key>"

GET/api/patch-policies

List patch policies

  • beta
  • Standard API key
  • Scope: read
  • Permission: patch.view
  • listPatchPolicies

Every patch policy the key's organisation owns, plus the provider-wide policies (tenantId null) that also govern its endpoints. deviceCount counts only endpoints pinned to a policy; the policy that actually governs each endpoint, after targeting, is in the compliance report.

Responses
200

The policies, newest first.

200 response of GET /api/patch-policies
FieldTypeDescription
[].alertOnFailure requiredbooleanRaise one alert when an install still fails after its retries.
[].approvedClassifications requiredarray of stringClassifications auto-approval applies to, e.g. ["Security","Critical"].
[].autoApprove requiredbooleanDiscovered updates matching the rules become approved automatically.
[].autoReboot requiredbooleanRestart the endpoint (signed power_action) when an install it reported needs one, in the reboot window.
[].classificationRules requiredarray of objectPer-classification overrides; empty means the policy-wide rules apply to every classification.
[].classificationRules[].approve requiredboolean
[].classificationRules[].classification requiredstringSecurity, Critical, Definition, Cumulative, Driver, Feature or Other.
[].classificationRules[].days requiredarray of integerWeekdays this classification may install on, 0 (Sunday) to 6; empty for the maintenance window days, or every day.
[].classificationRules[].deferDays requiredintegerApprove only once the update has been published this many days.
[].classificationRules[].end requiredstring | null24-hour "HH:MM", or null when not set.
[].classificationRules[].start requiredstring | nullWith end: this classification installs in its own window, in place of the maintenance window.
[].createdAt requiredstring (date-time)
[].deferDays requiredinteger
[].description requiredstring | null
[].deviceCount requiredintegerEndpoints pinned to this policy explicitly (not those matched by targeting).
[].enabled requiredboolean
[].excludeKBs requiredarray of stringKB numbers never approved automatically.
[].id requiredinteger
[].includeKBs requiredarray of stringKB numbers always approved, overriding the classification rules.
[].installRetries requiredintegerHow many times a failed install is retried, with a doubling backoff from 30 minutes, inside the window (0 to 10).
[].kevAutoApprove requiredbooleanA missing update that fixes an open CISA KEV finding on the endpoint is approved whatever autoApprove and the classification rules say. excludeKBs still wins. It only approves: installs still need an operator, an automation or a patch ring.
[].kevBypassDeferral requiredbooleanKEV fixes skip the deferDays hold period.
[].kevEnabledById requiredinteger | nullThe user who last switched kevAutoApprove on, for the audit trail; null when never.
[].maintenanceDays requiredarray of integerMaintenance weekdays, 0 (Sunday) to 6.
[].maintenanceEnabled requiredboolean
[].maintenanceEnd requiredstring | null24-hour "HH:MM", or null when not set.
[].maintenanceStart requiredstring | null24-hour "HH:MM", or null when not set.
[].minSeverity requiredstring | nullHighest MSRC severity auto-approved (Critical, Important, Moderate, Low); null for any.
[].name requiredstring
[].priority requiredintegerTie-break between equally specific policies; higher wins.
[].rebootDeadlineHours requiredintegerHours after the install after which the restart happens regardless of the window and of who is signed in; 0 for no deadline.
[].rebootDeferralsAllowed requiredintegerStored but not honoured: the restart notice the agent shows cannot be postponed by the user.
[].rebootPrompt requiredbooleanWarn the signed-in user before a reboot.
[].rebootWindowEnd requiredstring | null24-hour "HH:MM", or null when not set.
[].rebootWindowStart requiredstring | nullReboot window start; with rebootWindowEnd, replaces the maintenance window for restarts.
[].scanIntervalHours requiredintegerHours between automatic patch scans of the endpoints this policy governs (1 to 168).
[].siteId requiredinteger | nullThe site the policy targets, or null.
[].targetDeviceTypes requiredarray of stringserver, workstation and/or laptop; empty for any kind of machine.
[].tenantId requiredinteger | nullThe owning organisation; null for a provider-wide policy, which every organisation sees.
[].timezone requiredstring | nullIANA time zone the windows are read in.
[].updatedAt requiredstring (date-time)
Example response
[
  {
    "alertOnFailure": true,
    "approvedClassifications": [
      "Security",
      "Critical",
      "Cumulative"
    ],
    "autoApprove": true,
    "autoReboot": true,
    "classificationRules": [
      {
        "approve": false,
        "classification": "Feature",
        "days": [
          6
        ],
        "deferDays": 90,
        "end": "05:00",
        "start": "01:00"
      }
    ],
    "createdAt": "2026-10-01T09:00:00.000Z",
    "deferDays": 7,
    "description": null,
    "deviceCount": 0,
    "enabled": true,
    "excludeKBs": [
      "KB5034441"
    ],
    "id": 11,
    "includeKBs": [],
    "installRetries": 1,
    "kevAutoApprove": false,
    "kevBypassDeferral": true,
    "kevEnabledById": null,
    "maintenanceDays": [],
    "maintenanceEnabled": false,
    "maintenanceEnd": null,
    "maintenanceStart": null,
    "minSeverity": null,
    "name": "Acme servers",
    "priority": 10,
    "rebootDeadlineHours": 24,
    "rebootDeferralsAllowed": 3,
    "rebootPrompt": true,
    "rebootWindowEnd": "04:00",
    "rebootWindowStart": "02:00",
    "scanIntervalHours": 24,
    "siteId": null,
    "targetDeviceTypes": [
      "server"
    ],
    "tenantId": 2,
    "timezone": "UTC",
    "updatedAt": "2026-10-12T15:30:00.000Z"
  },
  {
    "alertOnFailure": true,
    "approvedClassifications": [
      "Security",
      "Critical"
    ],
    "autoApprove": true,
    "autoReboot": false,
    "classificationRules": [],
    "createdAt": "2026-09-01T09:00:00.000Z",
    "deferDays": 0,
    "description": "Security and critical updates, nightly.",
    "deviceCount": 1,
    "enabled": true,
    "excludeKBs": [],
    "id": 10,
    "includeKBs": [],
    "installRetries": 1,
    "kevAutoApprove": false,
    "kevBypassDeferral": true,
    "kevEnabledById": null,
    "maintenanceDays": [
      1,
      2,
      3,
      4,
      5
    ],
    "maintenanceEnabled": true,
    "maintenanceEnd": "04:00",
    "maintenanceStart": "22:00",
    "minSeverity": null,
    "name": "Provider baseline",
    "priority": 0,
    "rebootDeadlineHours": 24,
    "rebootDeferralsAllowed": 3,
    "rebootPrompt": true,
    "rebootWindowEnd": null,
    "rebootWindowStart": null,
    "scanIntervalHours": 24,
    "siteId": null,
    "targetDeviceTypes": [],
    "tenantId": null,
    "timezone": "UTC",
    "updatedAt": "2026-09-01T09:00:00.000Z"
  }
]
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/patch-policies
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key was recognised but may not do this (revoked, expired, or its role lacks patch.view). A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/patch-policies
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/patch-policies" \
  -H "Authorization: Bearer <your key>"

GET/api/patch-policies/compliance

Get patch compliance

  • beta
  • Standard API key
  • Scope: read
  • Permission: patch.view
  • getPatchCompliance

Read-only compliance for every active endpoint in scope, computed from each endpoint's last patch scan and its recent deployments, with the policy that governs it and why. Nothing is scanned or installed. An empty estate reports compliancePercent 0 with total 0, never 100.

Responses
200

The fleet roll-up and one entry per endpoint.

200 response of GET /api/patch-policies/compliance
FieldTypeDescription
devices requiredarray of PatchComplianceDeviceOne entry per active endpoint in scope. Not paginated.
devices[].approvedPending requiredintegerOf those, updates the governing policy approves and neither a patch ring nor a patch-risk pause is holding back.
devices[].criticalMissing requiredintegerOf those, critical or security updates.
devices[].deviceId requiredinteger
devices[].deviceUid requiredstring | nullThe endpoint's permanent identity.
devices[].failedDeployments requiredintegerFailed patch deployments in the last 30 days.
devices[].kevAssessed requiredbooleanWhether the endpoint has a CISA KEV assessment (vulnerability matching has run for it).
devices[].kevFixNotOffered requiredarray of string | nullKB numbers that would fix an open KEV finding but which Windows Update does not offer the endpoint; null when not assessed.
devices[].kevFixesExcluded requiredarray of string | nullKB numbers that fix an open KEV finding on the endpoint but which the governing policy excludes; null when not assessed.
devices[].lastScan requiredstring (date-time) | nullWhen the last patch scan finished, or null if it never has.
devices[].missing requiredintegerUpdates the last scan found missing.
devices[].name requiredstringDisplay name, or the hostname when none is set.
devices[].online requiredboolean
devices[].policyId requiredinteger | nullThe policy that governs the endpoint after targeting is resolved; null when none applies.
devices[].policyName requiredstring | null
devices[].policyReason requiredstringpolicySource in plain English.
devices[].policySource required"pinned" | "site+type" | "site" | "tenant+type" | "tenant" | "type" | "default" | "none"Why that policy applies: pinned to the endpoint, or the most specific targeting rule that matched.
devices[].rebootRequired requiredboolean
devices[].ringHeld requiredintegerMissing updates a patch ring is holding back from this endpoint until its ring is due. They are not in approvedPending; criticalMissing still counts them.
devices[].riskHeld requiredintegerMissing updates paused because their install failure rate jumped across the fleet (patch risk). Not in approvedPending or ringHeld; criticalMissing still counts them.
devices[].scanStale requiredbooleanNo scan, or the last one is too old to trust.
devices[].scanStatus requiredstring | null
devices[].status required"compliant" | "non-compliant" | "unknown"unknown when the scan failed or is stale; non-compliant with critical or approved updates outstanding or a reboot pending.
fleet requiredPatchComplianceFleetThe fleet roll-up of patch compliance.
fleet.compliancePercent requiredintegercompliant / total as a whole percentage; 0 when total is 0.
fleet.compliant requiredinteger
fleet.criticalMissing requiredintegerCritical or security updates missing, summed over every endpoint.
fleet.failedDeployments requiredintegerEndpoints with a failed deployment in the last 30 days.
fleet.governed requiredintegerEndpoints a policy governs.
fleet.governedCompliant requiredinteger
fleet.governedPercent requiredinteger | nullgovernedCompliant / governed as a whole percentage; null when nothing is governed.
fleet.kevAssessed requiredinteger | nullEndpoints with a KEV assessment; null when this server has no KEV data at all.
fleet.kevFixNotOffered requiredinteger | nullEndpoints with at least one KEV fix Windows Update does not offer; null when this server has no KEV data.
fleet.kevFixesExcluded requiredinteger | nullEndpoints with at least one KEV fix excluded by their policy; null when this server has no KEV data.
fleet.nonCompliant requiredinteger
fleet.policies requiredintegerPolicies visible to the key.
fleet.rebootRequired requiredinteger
fleet.ringHeld requiredinteger | nullUpdates held back by patch rings, summed over every endpoint; null when the ring state could not be read.
fleet.riskHeld requiredinteger | nullUpdates paused by patch risk, summed over every endpoint; null when the patch-risk state could not be read.
fleet.staleScans requiredinteger
fleet.total requiredintegerActive endpoints in scope.
fleet.unassigned requiredintegerEndpoints no policy governs.
fleet.unknown requiredinteger
Example response
{
  "devices": [
    {
      "approvedPending": 0,
      "criticalMissing": 0,
      "deviceId": 101,
      "deviceUid": "D-7K3M-9QX2",
      "failedDeployments": 0,
      "kevAssessed": false,
      "kevFixNotOffered": null,
      "kevFixesExcluded": null,
      "lastScan": "2026-10-21T07:00:00.000Z",
      "missing": 0,
      "name": "ACME-DC01",
      "online": true,
      "policyId": 11,
      "policyName": "Acme servers",
      "policyReason": "Matched: device type for this customer",
      "policySource": "tenant+type",
      "rebootRequired": false,
      "ringHeld": 0,
      "riskHeld": 0,
      "scanStale": false,
      "scanStatus": "ok",
      "status": "compliant"
    },
    {
      "approvedPending": 1,
      "criticalMissing": 1,
      "deviceId": 102,
      "deviceUid": "D-4HPA-2RTB",
      "failedDeployments": 1,
      "kevAssessed": false,
      "kevFixNotOffered": null,
      "kevFixesExcluded": null,
      "lastScan": "2026-10-21T05:00:00.000Z",
      "missing": 2,
      "name": "Reception PC",
      "online": true,
      "policyId": 10,
      "policyName": "Provider baseline",
      "policyReason": "Pinned to this endpoint",
      "policySource": "pinned",
      "rebootRequired": true,
      "ringHeld": 0,
      "riskHeld": 0,
      "scanStale": false,
      "scanStatus": "ok",
      "status": "non-compliant"
    },
    {
      "approvedPending": 0,
      "criticalMissing": 0,
      "deviceId": 103,
      "deviceUid": "D-8WNE-5CJD",
      "failedDeployments": 0,
      "kevAssessed": false,
      "kevFixNotOffered": null,
      "kevFixesExcluded": null,
      "lastScan": "2026-10-09T10:00:00.000Z",
      "missing": 0,
      "name": "ACME-LT007",
      "online": false,
      "policyId": 10,
      "policyName": "Provider baseline",
      "policyReason": "Matched: fleet default",
      "policySource": "default",
      "rebootRequired": false,
      "ringHeld": 0,
      "riskHeld": 0,
      "scanStale": true,
      "scanStatus": "ok",
      "status": "unknown"
    }
  ],
  "fleet": {
    "compliancePercent": 33,
    "compliant": 1,
    "criticalMissing": 1,
    "failedDeployments": 1,
    "governed": 3,
    "governedCompliant": 1,
    "governedPercent": 33,
    "kevAssessed": 0,
    "kevFixNotOffered": 0,
    "kevFixesExcluded": 0,
    "nonCompliant": 1,
    "policies": 2,
    "rebootRequired": 1,
    "ringHeld": 0,
    "riskHeld": 0,
    "staleScans": 1,
    "total": 3,
    "unassigned": 0,
    "unknown": 1
  }
}
401

No credential was recognised: no Authorization header, or a key-shaped bearer (dwn_…) that is malformed, unknown or has the wrong secret. (Any other unrecognised bearer is 403; see 403.)

401 response of GET /api/patch-policies/compliance
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
403

The key was recognised but may not do this (revoked, expired, or its role lacks patch.view). A bearer that is not shaped like a key (dwn_…) and is not a valid console session token is also 403, with error Forbidden. A provider (global) key is also refused here, with error set to a code: TENANT_REQUIRED (provider keys reach platform administration only, never an organisation's data), PROVIDER_ACCESS_REQUIRED (it sent the tenant header; no organisation grants access to a key) or PROVIDER_IP_REFUSED (it called from an address the server does not allow provider credentials from).

403 response of GET /api/patch-policies/compliance
FieldTypeDescription
codestringA machine-readable code, where one exists (e.g. TOKEN_EXPIRED).
detailsarray of objectOn a 400 "Validation failed": one entry per rejected field.
details[].field requiredstring
details[].message requiredstring
error requiredstringWhat went wrong. Usually a sentence for a person; for tenant-context and provider-gate refusals it is a code (e.g. TENANT_CONTEXT_UNAVAILABLE, TENANT_REQUIRED, PROVIDER_ACCESS_REQUIRED, PROVIDER_IP_REFUSED) and message carries the sentence.
messagestringA sentence for a person, when error is a code.
permissionstringOn a 403 from a permission check: the permission that was required.
reasonstringOn some 403s: why, as a code (e.g. global-only, api-key-refused).
rolestring | nullOn a 403 from a permission check: the role the caller acts as.
Request
curl "https://<your instance>/api/patch-policies/compliance" \
  -H "Authorization: Bearer <your key>"

Webhooks

The requests Denwen RMM sends to an integration’s own endpoint, with what each carries and how to answer it.

POSTaction.completed

A ticket action reached its final state (once per action)

  • Contract 1.10.0
  • webhookActionCompleted

Sent once when an action created by POST /v1/actions reaches a terminal state, to the integration that created it whatever its eventTypes. Never held, coalesced or dropped. Write the result as an internal note on the ticket; if it has not arrived by timeoutSec + 5 minutes, poll GET /v1/actions/{id} (contract §18.7). Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST action.completed
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST action.completed
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.action requiredContractActionA ticket action (contract §18.6).
data.action.consoleUrl requiredstring | nullThe device in the RMM console; link it only when it starts with the pinned base URL.
data.action.createdAt requiredstring (date-time)When the action was accepted.
data.action.device requiredobject
data.action.device.customerId requiredinteger
data.action.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.action.device.id requiredinteger
data.action.device.siteId requiredinteger
data.action.finishedAt requiredstring (date-time) | null
data.action.id requiredstring
data.action.idempotencyKey requiredstring
data.action.kind required"script" | "reboot" | "patch_scan" | "diagnostics"Open set on the wire: show an unknown kind generically.
data.action.reboot requiredobject | null
data.action.reboot.backOnlineAt requiredstring (date-time) | null
data.action.reboot.noticeSeconds requiredintegerThe notice actually given.
data.action.reboot.userLoggedOn requiredboolean
data.action.refusal requiredenum (25 values) | nullSet only for refused and abandoned. Open set.
data.action.requestedBy requiredobject
data.action.requestedBy.email requiredstring
data.action.requestedBy.flowmspSub requiredstringThe Denwen MSP user (sub) who asked. (Denwen MSP, formerly FlowMSP: the field name keeps its contract v1 name, and the RMM sends only this name; it never reads it from a request, where sub comes from the signed action assertion.)
data.action.requestedBy.rmmUserId requiredintegerThe LINKED RMM user the action runs as.
data.action.result requiredobject | nullNull until a terminal state other than refused.
data.action.result.exitCode requiredinteger | null
data.action.result.redactions requiredintegerHow many credential-looking spans were replaced before sending.
data.action.result.stderr requiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days.
data.action.result.stdout requiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days.
data.action.result.summary requiredstring
data.action.result.truncated requiredboolean
data.action.script requiredobject | null
data.action.script.id requiredinteger
data.action.script.name requiredstring
data.action.script.version requiredinteger
data.action.startedAt requiredstring (date-time) | null
data.action.state required"queued" | "running" | "succeeded" | "failed" | "timed_out" | "refused" | "abandoned"Closed set.
data.action.ticket requiredobject
data.action.ticket.id requiredstringDenwen MSP's ticket id.
data.action.ticket.number requiredstring | nullThe ticket number people use, e.g. 10523.
data.action.ticket.url requiredstringhttps, with the origin of the integration webhook URL.
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"action.completed"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "action": {
      "consoleUrl": "https://rmm.acme-msp.example/endpoint/1187",
      "createdAt": "2026-11-20T09:00:00.412Z",
      "device": {
        "customerId": 42,
        "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
        "id": 1187,
        "siteId": 310
      },
      "finishedAt": "2026-11-20T09:00:14.880Z",
      "id": "act_01M6RS0K55X7AQ28PXWTGBGKGZ",
      "idempotencyKey": "tka_01JD4M9ZQ8V2X0B1C2D3E4F5G6",
      "kind": "script",
      "reboot": null,
      "refusal": null,
      "requestedBy": {
        "email": "sam.taylor@acme-msp.example",
        "flowmspSub": "usr_01JC2Z8Q4M7V9X0B1C2D3E4F5G",
        "rmmUserId": 12
      },
      "result": {
        "exitCode": 0,
        "redactions": 0,
        "stderr": "",
        "stdout": "Stopping Print Spooler...\nRemoved 3 job(s)\nStarted.",
        "summary": "Script 'Clear print queue' v3 exited with code 0.",
        "truncated": false
      },
      "script": {
        "id": 41,
        "name": "Clear print queue",
        "version": 3
      },
      "startedAt": "2026-11-06T15:53:06.472Z",
      "state": "succeeded",
      "ticket": {
        "id": "cm3tkt0nq8",
        "number": "10523",
        "url": "https://msp.example/tickets/cm3tkt0nq8"
      }
    }
  },
  "id": "evt_01M7W2YP50FZFBFKNQBH7HES2Y",
  "occurredAt": "2026-11-06T15:53:06.473Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "action.completed"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTalert.cleared

An alert was resolved; always delivered once the raise was

  • Contract 1.10.0
  • webhookAlertCleared

Delivered even when eventTypes omits it, to a suspended tenant, and before a late raise. A clear for an alert you never saw is a no-op. Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST alert.cleared
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST alert.cleared
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.alert requiredContractAlertAn RMM alert (contract §16.1).
data.alert.asset requiredContractHypervisorAlertAsset | ContractUptimeAlertAsset | null§16.11 (1.8.0): the hypervisor host or website/port check of a hypervisor / uptime alert; null for every other kind.
data.alert.autoClears requiredbooleanTrue when the RMM clears the alert itself once the condition ends.
data.alert.category requiredstring
data.alert.consoleUrl requiredstring | null
data.alert.description requiredstring | null
data.alert.device requiredContractDeviceRef | nullAt the time of the event. Null for site_outage, hypervisor and uptime.
data.alert.device.customerId requiredinteger
data.alert.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.alert.device.id requiredinteger
data.alert.device.siteId requiredinteger
data.alert.id requiredinteger
data.alert.key requiredstringStable de-duplication key for the condition.
data.alert.lastOccurredAt requiredstring (date-time)The latest occurrence.
data.alert.raisedAt requiredstring (date-time)When the alert was raised.
data.alert.resolvedAt requiredstring (date-time) | null
data.alert.severity requiredstringwarning or critical today. Open set; read an unknown value as warning.
data.alert.site requiredobject | nullNull only for hypervisor and uptime (§16.11), which are sent only to an integration that opted in.
data.alert.site.customerId requiredinteger
data.alert.site.id requiredinteger
data.alert.source requiredobject
data.alert.source.kind requiredenum (9 values)Open set on the wire.
data.alert.source.ruleId requiredinteger | null
data.alert.source.ruleName requiredstring | null
data.alert.state required"open" | "resolved"
data.alert.title requiredstring
data.resolution requiredstring | null
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"alert.cleared"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "alert": {
      "asset": null,
      "autoClears": false,
      "category": "Events",
      "consoleUrl": "https://rmm.acme-msp.example/endpoint/1187",
      "description": "Event 4625 (Microsoft-Windows-Security-Auditing) matched rule \"Repeated failed sign-ins\": An account failed to log on. Account name: reception. Failure reason: unknown user name or bad password.",
      "device": {
        "customerId": 42,
        "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
        "id": 1187,
        "siteId": 310
      },
      "id": 812,
      "key": "DWN-XYGW-9KF9-ZBEK-B8N$|eventRule:31:Security:4625",
      "lastOccurredAt": "2026-11-06T15:53:05.561Z",
      "raisedAt": "2026-10-22T09:12:40.000Z",
      "resolvedAt": "2026-11-06T15:53:05.561Z",
      "severity": "warning",
      "site": {
        "customerId": 42,
        "id": 310
      },
      "source": {
        "kind": "event_rule",
        "ruleId": 31,
        "ruleName": "Repeated failed sign-ins"
      },
      "state": "resolved",
      "title": "Repeated failed sign-ins on CD-RECEPTION01"
    },
    "resolution": "Denwen MSP ticket #10523 was closed"
  },
  "id": "evt_01M6RS0J8TD2GV6AX8Q4ZFYZHJ",
  "occurredAt": "2026-11-06T15:53:05.561Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "alert.cleared"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTalert.raised

An alert was raised that passes the integration alert filter

  • Contract 1.10.0
  • webhookAlertRaised

Open a ticket and write the link back with POST /v1/alert-tickets. Raises above the storm brake are dropped and an integration.resync_suggested (alert_storm) follows; re-read GET /v1/alerts (contract §16). Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST alert.raised
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST alert.raised
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.alert requiredContractAlertAn RMM alert (contract §16.1).
data.alert.asset requiredContractHypervisorAlertAsset | ContractUptimeAlertAsset | null§16.11 (1.8.0): the hypervisor host or website/port check of a hypervisor / uptime alert; null for every other kind.
data.alert.autoClears requiredbooleanTrue when the RMM clears the alert itself once the condition ends.
data.alert.category requiredstring
data.alert.consoleUrl requiredstring | null
data.alert.description requiredstring | null
data.alert.device requiredContractDeviceRef | nullAt the time of the event. Null for site_outage, hypervisor and uptime.
data.alert.device.customerId requiredinteger
data.alert.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.alert.device.id requiredinteger
data.alert.device.siteId requiredinteger
data.alert.id requiredinteger
data.alert.key requiredstringStable de-duplication key for the condition.
data.alert.lastOccurredAt requiredstring (date-time)The latest occurrence.
data.alert.raisedAt requiredstring (date-time)When the alert was raised.
data.alert.resolvedAt requiredstring (date-time) | null
data.alert.severity requiredstringwarning or critical today. Open set; read an unknown value as warning.
data.alert.site requiredobject | nullNull only for hypervisor and uptime (§16.11), which are sent only to an integration that opted in.
data.alert.site.customerId requiredinteger
data.alert.site.id requiredinteger
data.alert.source requiredobject
data.alert.source.kind requiredenum (9 values)Open set on the wire.
data.alert.source.ruleId requiredinteger | null
data.alert.source.ruleName requiredstring | null
data.alert.state required"open" | "resolved"
data.alert.title requiredstring
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"alert.raised"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "alert": {
      "asset": null,
      "autoClears": false,
      "category": "Events",
      "consoleUrl": "https://rmm.acme-msp.example/endpoint/1187",
      "description": "Event 4625 (Microsoft-Windows-Security-Auditing) matched rule \"Repeated failed sign-ins\": An account failed to log on. Account name: reception. Failure reason: unknown user name or bad password.",
      "device": {
        "customerId": 42,
        "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
        "id": 1187,
        "siteId": 310
      },
      "id": 812,
      "key": "DWN-XYGW-9KF9-ZBEK-B8N$|eventRule:31:Security:4625",
      "lastOccurredAt": "2026-10-22T09:12:40.000Z",
      "raisedAt": "2026-10-22T09:12:40.000Z",
      "resolvedAt": null,
      "severity": "warning",
      "site": {
        "customerId": 42,
        "id": 310
      },
      "source": {
        "kind": "event_rule",
        "ruleId": 31,
        "ruleName": "Repeated failed sign-ins"
      },
      "state": "open",
      "title": "Repeated failed sign-ins on CD-RECEPTION01"
    }
  },
  "id": "evt_01M6RS0J8RKYRZVEKJXZWVPD6A",
  "occurredAt": "2026-10-22T09:12:40.000Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "alert.raised"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTalert.updated

An open alert recurred or changed (held and coalesced per alert)

  • Contract 1.10.0
  • webhookAlertUpdated

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST alert.updated
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST alert.updated
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.alert requiredContractAlertAn RMM alert (contract §16.1).
data.alert.asset requiredContractHypervisorAlertAsset | ContractUptimeAlertAsset | null§16.11 (1.8.0): the hypervisor host or website/port check of a hypervisor / uptime alert; null for every other kind.
data.alert.autoClears requiredbooleanTrue when the RMM clears the alert itself once the condition ends.
data.alert.category requiredstring
data.alert.consoleUrl requiredstring | null
data.alert.description requiredstring | null
data.alert.device requiredContractDeviceRef | nullAt the time of the event. Null for site_outage, hypervisor and uptime.
data.alert.device.customerId requiredinteger
data.alert.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.alert.device.id requiredinteger
data.alert.device.siteId requiredinteger
data.alert.id requiredinteger
data.alert.key requiredstringStable de-duplication key for the condition.
data.alert.lastOccurredAt requiredstring (date-time)The latest occurrence.
data.alert.raisedAt requiredstring (date-time)When the alert was raised.
data.alert.resolvedAt requiredstring (date-time) | null
data.alert.severity requiredstringwarning or critical today. Open set; read an unknown value as warning.
data.alert.site requiredobject | nullNull only for hypervisor and uptime (§16.11), which are sent only to an integration that opted in.
data.alert.site.customerId requiredinteger
data.alert.site.id requiredinteger
data.alert.source requiredobject
data.alert.source.kind requiredenum (9 values)Open set on the wire.
data.alert.source.ruleId requiredinteger | null
data.alert.source.ruleName requiredstring | null
data.alert.state required"open" | "resolved"
data.alert.title requiredstring
data.changes requiredarray of stringInformational; an open set drawn from lastOccurredAt, description.
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"alert.updated"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "alert": {
      "asset": null,
      "autoClears": false,
      "category": "Events",
      "consoleUrl": "https://rmm.acme-msp.example/endpoint/1187",
      "description": "Event 4625 (Microsoft-Windows-Security-Auditing) matched rule \"Repeated failed sign-ins\": An account failed to log on. Account name: reception. Failure reason: unknown user name or bad password.",
      "device": {
        "customerId": 42,
        "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
        "id": 1187,
        "siteId": 310
      },
      "id": 812,
      "key": "DWN-XYGW-9KF9-ZBEK-B8N$|eventRule:31:Security:4625",
      "lastOccurredAt": "2026-11-06T15:53:05.561Z",
      "raisedAt": "2026-10-22T09:12:40.000Z",
      "resolvedAt": null,
      "severity": "warning",
      "site": {
        "customerId": 42,
        "id": 310
      },
      "source": {
        "kind": "event_rule",
        "ruleId": 31,
        "ruleName": "Repeated failed sign-ins"
      },
      "state": "open",
      "title": "Repeated failed sign-ins on CD-RECEPTION01"
    },
    "changes": [
      "lastOccurredAt",
      "description"
    ]
  },
  "id": "evt_01M6RS0J8S7PAQN6J9EA64KEVX",
  "occurredAt": "2026-11-06T15:53:05.561Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "alert.updated"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTcustomer.created

A customer was created

  • Contract 1.10.0
  • webhookCustomerCreated

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST customer.created
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST customer.created
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.customer requiredContractCustomerA customer (contract §6.3).
data.customer.accountRef requiredstringThe customer's immutable account reference (6 digits).
data.customer.address requiredContractAddressA postal address. Every part is a string or null.
data.customer.address.city requiredstring | nullTown or city.
data.customer.address.country requiredstring | nullCountry.
data.customer.address.line1 requiredstring | nullAddress line 1.
data.customer.address.line2 requiredstring | nullAddress line 2.
data.customer.address.postalCode requiredstring | nullPostcode or ZIP.
data.customer.address.region requiredstring | nullCounty, state or region.
data.customer.contactEmail requiredstring | nullContact email.
data.customer.contactName requiredstring | nullContact name.
data.customer.contactPhone requiredstring | nullContact phone.
data.customer.deleted requiredbooleanTrue only for a tombstone (contract §7).
data.customer.externalRefs requiredContractCustomerRefs | null1.10.0, additive: what the paired Denwen MSP recorded here (contract §23). Null when nothing is, and on a tombstone.
data.customer.externalRefs.organizationId requiredstringThe Denwen MSP organization this customer is mapped to.
data.customer.externalRefs.organizationName requiredstring | nullThe organization's name in Denwen MSP.
data.customer.externalRefs.updatedAt requiredstring (date-time)The change time Denwen MSP sent; a write older than it is ignored.
data.customer.externalRefs.url requiredstring | nullThe organization's page in Denwen MSP. https, on the origin of the integration webhook URL.
data.customer.id requiredintegerCustomer id. Never reused.
data.customer.kind required"normal" | "unassigned"unassigned for the tenant's reserved holding customer; exclude it from auto-matching.
data.customer.name requiredstringUnique within the tenant.
data.customer.updatedAt requiredstring (date-time)Last change; for a tombstone, the deletion time.
data.customer.website requiredstring | nullWebsite.
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"customer.created"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "customer": {
      "accountRef": "482915",
      "address": {
        "city": "London",
        "country": "United Kingdom",
        "line1": "12 Harley Street",
        "line2": null,
        "postalCode": "W1G 9PF",
        "region": "Greater London"
      },
      "contactEmail": "it@contosodental.example",
      "contactName": "Priya Shah",
      "contactPhone": "+44 20 7946 0123",
      "deleted": false,
      "externalRefs": {
        "organizationId": "cm2k7r4pa0000lb08x9d1q5v2",
        "organizationName": "Contoso Dental",
        "updatedAt": "2026-10-20T14:03:00.000Z",
        "url": "https://msp.example/organizations/cm2k7r4pa0000lb08x9d1q5v2"
      },
      "id": 42,
      "kind": "normal",
      "name": "Contoso Dental Ltd",
      "updatedAt": "2026-10-20T14:03:11.482Z",
      "website": "https://www.contosodental.example"
    }
  },
  "id": "evt_01M6RS0J8NG4D49H34Y6EYTAD6",
  "occurredAt": "2026-11-06T15:53:05.557Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "customer.created"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTcustomer.deleted

A customer was deleted

  • Contract 1.10.0
  • webhookCustomerDeleted

data.customer is the last known customer, with deleted: true. Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST customer.deleted
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST customer.deleted
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.customer requiredContractCustomerA customer (contract §6.3).
data.customer.accountRef requiredstringThe customer's immutable account reference (6 digits).
data.customer.address requiredContractAddressA postal address. Every part is a string or null.
data.customer.address.city requiredstring | nullTown or city.
data.customer.address.country requiredstring | nullCountry.
data.customer.address.line1 requiredstring | nullAddress line 1.
data.customer.address.line2 requiredstring | nullAddress line 2.
data.customer.address.postalCode requiredstring | nullPostcode or ZIP.
data.customer.address.region requiredstring | nullCounty, state or region.
data.customer.contactEmail requiredstring | nullContact email.
data.customer.contactName requiredstring | nullContact name.
data.customer.contactPhone requiredstring | nullContact phone.
data.customer.deleted requiredbooleanTrue only for a tombstone (contract §7).
data.customer.externalRefs requiredContractCustomerRefs | null1.10.0, additive: what the paired Denwen MSP recorded here (contract §23). Null when nothing is, and on a tombstone.
data.customer.externalRefs.organizationId requiredstringThe Denwen MSP organization this customer is mapped to.
data.customer.externalRefs.organizationName requiredstring | nullThe organization's name in Denwen MSP.
data.customer.externalRefs.updatedAt requiredstring (date-time)The change time Denwen MSP sent; a write older than it is ignored.
data.customer.externalRefs.url requiredstring | nullThe organization's page in Denwen MSP. https, on the origin of the integration webhook URL.
data.customer.id requiredintegerCustomer id. Never reused.
data.customer.kind required"normal" | "unassigned"unassigned for the tenant's reserved holding customer; exclude it from auto-matching.
data.customer.name requiredstringUnique within the tenant.
data.customer.updatedAt requiredstring (date-time)Last change; for a tombstone, the deletion time.
data.customer.website requiredstring | nullWebsite.
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"customer.deleted"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "customer": {
      "accountRef": "482915",
      "address": {
        "city": "London",
        "country": "United Kingdom",
        "line1": "12 Harley Street",
        "line2": null,
        "postalCode": "W1G 9PF",
        "region": "Greater London"
      },
      "contactEmail": "it@contosodental.example",
      "contactName": "Priya Shah",
      "contactPhone": "+44 20 7946 0123",
      "deleted": true,
      "externalRefs": null,
      "id": 42,
      "kind": "normal",
      "name": "Contoso Dental Ltd",
      "updatedAt": "2026-10-21T10:15:00.000Z",
      "website": "https://www.contosodental.example"
    }
  },
  "id": "evt_01M6RS0J8N2TEBVJ89VKNTTCCZ",
  "occurredAt": "2026-11-06T15:53:05.557Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "customer.deleted"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTcustomer.updated

A customer was edited

  • Contract 1.10.0
  • webhookCustomerUpdated

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST customer.updated
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST customer.updated
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.customer requiredContractCustomerA customer (contract §6.3).
data.customer.accountRef requiredstringThe customer's immutable account reference (6 digits).
data.customer.address requiredContractAddressA postal address. Every part is a string or null.
data.customer.address.city requiredstring | nullTown or city.
data.customer.address.country requiredstring | nullCountry.
data.customer.address.line1 requiredstring | nullAddress line 1.
data.customer.address.line2 requiredstring | nullAddress line 2.
data.customer.address.postalCode requiredstring | nullPostcode or ZIP.
data.customer.address.region requiredstring | nullCounty, state or region.
data.customer.contactEmail requiredstring | nullContact email.
data.customer.contactName requiredstring | nullContact name.
data.customer.contactPhone requiredstring | nullContact phone.
data.customer.deleted requiredbooleanTrue only for a tombstone (contract §7).
data.customer.externalRefs requiredContractCustomerRefs | null1.10.0, additive: what the paired Denwen MSP recorded here (contract §23). Null when nothing is, and on a tombstone.
data.customer.externalRefs.organizationId requiredstringThe Denwen MSP organization this customer is mapped to.
data.customer.externalRefs.organizationName requiredstring | nullThe organization's name in Denwen MSP.
data.customer.externalRefs.updatedAt requiredstring (date-time)The change time Denwen MSP sent; a write older than it is ignored.
data.customer.externalRefs.url requiredstring | nullThe organization's page in Denwen MSP. https, on the origin of the integration webhook URL.
data.customer.id requiredintegerCustomer id. Never reused.
data.customer.kind required"normal" | "unassigned"unassigned for the tenant's reserved holding customer; exclude it from auto-matching.
data.customer.name requiredstringUnique within the tenant.
data.customer.updatedAt requiredstring (date-time)Last change; for a tombstone, the deletion time.
data.customer.website requiredstring | nullWebsite.
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"customer.updated"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "customer": {
      "accountRef": "482915",
      "address": {
        "city": "London",
        "country": "United Kingdom",
        "line1": "12 Harley Street",
        "line2": null,
        "postalCode": "W1G 9PF",
        "region": "Greater London"
      },
      "contactEmail": "it@contosodental.example",
      "contactName": "Priya Shah",
      "contactPhone": "+44 20 7946 0123",
      "deleted": false,
      "externalRefs": {
        "organizationId": "cm2k7r4pa0000lb08x9d1q5v2",
        "organizationName": "Contoso Dental",
        "updatedAt": "2026-10-20T14:03:00.000Z",
        "url": "https://msp.example/organizations/cm2k7r4pa0000lb08x9d1q5v2"
      },
      "id": 42,
      "kind": "normal",
      "name": "Contoso Dental Ltd",
      "updatedAt": "2026-10-20T14:03:11.482Z",
      "website": "https://www.contosodental.example"
    }
  },
  "id": "evt_01M6RS0J8N3CKSJ73MD6SND595",
  "occurredAt": "2026-11-06T15:53:05.557Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "customer.updated"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTdevice.enrolled

A device enrolled, re-enrolled, or was claimed into this tenant

  • Contract 1.10.0
  • webhookDeviceEnrolled

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST device.enrolled
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST device.enrolled
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.device requiredContractDeviceRefA device as it was at the time of an event (contract §8.4).
data.device.customerId requiredinteger
data.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.device.id requiredinteger
data.device.siteId requiredinteger
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"device.enrolled"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "device": {
      "customerId": 42,
      "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
      "id": 1187,
      "siteId": 310
    }
  },
  "id": "evt_01M6RS0J8MCK4JDS52ZHB3EAHZ",
  "occurredAt": "2026-10-21T09:00:00.000Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "device.enrolled"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTdevice.lifecycle

A device was uninstalled, quarantined, restored or purged

  • Contract 1.10.0
  • webhookDeviceLifecycle

A purge carries lifecycleState: purged; the device then answers as a tombstone for 35 days. Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST device.lifecycle
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST device.lifecycle
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.device requiredContractDeviceRefA device as it was at the time of an event (contract §8.4).
data.device.customerId requiredinteger
data.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.device.id requiredinteger
data.device.siteId requiredinteger
data.lifecycleState requiredstringuninstalled, quarantined, active (restore) or purged. Open set.
data.previousLifecycleState requiredstring | null
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"device.lifecycle"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "device": {
      "customerId": 42,
      "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
      "id": 1187,
      "siteId": 310
    },
    "lifecycleState": "purged",
    "previousLifecycleState": "uninstalled"
  },
  "id": "evt_01M6RS0J8NZM3BJ4PDTFWPSKW1",
  "occurredAt": "2026-10-21T10:15:00.000Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "device.lifecycle"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTdevice.status

A device went online or offline (held 20 s and coalesced)

  • Contract 1.10.0
  • webhookDeviceStatus

A flap inside the hold window becomes one event carrying the final state. The only event whose data Denwen MSP applies directly; apply it only if occurredAt is newer than the last status applied (contract §8.5). Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST device.status
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST device.status
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.device requiredContractDeviceRefA device as it was at the time of an event (contract §8.4).
data.device.customerId requiredinteger
data.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.device.id requiredinteger
data.device.siteId requiredinteger
data.lastSeen requiredstring (date-time) | null
data.loggedUser requiredstring | null
data.status required"online" | "offline"
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"device.status"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "device": {
      "customerId": 42,
      "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
      "id": 1187,
      "siteId": 310
    },
    "lastSeen": "2026-10-21T08:59:52.310Z",
    "loggedUser": "CONTOSO\\reception",
    "status": "online"
  },
  "id": "evt_01M6RS0J8NRARPZDZ63F4FTT68",
  "occurredAt": "2026-10-21T09:00:00.000Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "device.status"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTdevice.updated

A device was renamed, moved, approved, re-keyed, or changed OS or type

  • Contract 1.10.0
  • webhookDeviceUpdated

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST device.updated
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST device.updated
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.changes requiredarray of stringInformational, not exhaustive; an open set drawn from name, displayName, os, type, siteId, customerId, approvalState, publicKeyFingerprint.
data.device requiredContractDeviceRefA device as it was at the time of an event (contract §8.4).
data.device.customerId requiredinteger
data.device.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.device.id requiredinteger
data.device.siteId requiredinteger
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"device.updated"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "changes": [
      "displayName",
      "siteId",
      "customerId"
    ],
    "device": {
      "customerId": 42,
      "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
      "id": 1187,
      "siteId": 310
    }
  },
  "id": "evt_01M6RS0J8M13MRXRJX40EMWTM1",
  "occurredAt": "2026-10-21T09:00:00.000Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "device.updated"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTintegration.ping

A test event sent from the RMM (no hold, one attempt, no retry)

  • Contract 1.10.0
  • webhookIntegrationPing

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST integration.ping
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST integration.ping
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.message requiredstring | null
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"integration.ping"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "message": "Test from admin@acme-msp.example"
  },
  "id": "evt_01M6RS0J8PM9G59JVQX6VHZWY0",
  "occurredAt": "2026-11-06T15:53:05.558Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "integration.ping"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTintegration.resync_suggested

The RMM dropped events; run a delta sync of the named resources

  • Contract 1.10.0
  • webhookIntegrationResyncSuggested

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST integration.resync_suggested
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST integration.resync_suggested
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.reason required"status_storm" | "backlog_dropped" | "alert_storm"Open set.
data.resources requiredarray of "customers" | "sites" | "devices" | "alerts"Open set.
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"integration.resync_suggested"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "reason": "backlog_dropped",
    "resources": [
      "devices"
    ]
  },
  "id": "evt_01M6RS0J8Q43PM5K6Z7XBV5D2P",
  "occurredAt": "2026-11-06T15:53:05.559Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "integration.resync_suggested"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTsession.ended

A technician remote session ended

  • Contract 1.10.0
  • webhookSessionEnded

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST session.ended
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST session.ended
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.session requiredContractSessionA technician remote session (contract §8.4).
data.session.client required"native" | "web" | null1.3.0: the viewer the technician used, native or web; null for a tools session or a relay that does not report it.
data.session.deviceId requiredinteger
data.session.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.session.endedAt requiredstring (date-time) | null
data.session.id requiredstringSession id (UUID).
data.session.kind required"attended" | "unattended"attended when end-user consent was required.
data.session.launchId requiredstring | nullThe launch that produced this session (1.1).
data.session.purpose requiredstring | nulldesktop, tools or adhoc.
data.session.rmmUserId requiredinteger | nullThe operator's user id; null for an API or system actor.
data.session.startedAt requiredstring (date-time)When the session started.
data.session.technicianEmail requiredstring | nullThe operator's email. May be provider staff working under a support grant.
data.session.via required"flowmsp" | nullflowmsp when the session came from a Denwen MSP launch (1.1). (Denwen MSP, formerly FlowMSP: the value keeps its contract v1 spelling, and the RMM sends only this value.)
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"session.ended"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "session": {
      "client": null,
      "deviceId": 1187,
      "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
      "endedAt": "2026-10-21T09:31:47.905Z",
      "id": "6f1d2c4e-9b8a-4c3d-8e7f-0a1b2c3d4e5f",
      "kind": "attended",
      "launchId": null,
      "purpose": "desktop",
      "rmmUserId": 3,
      "startedAt": "2026-10-21T09:12:03.120Z",
      "technicianEmail": "sam.taylor@acme-msp.example",
      "via": null
    }
  },
  "id": "evt_01M6RS0J8PXPB115JKS2C1WVAB",
  "occurredAt": "2026-10-21T09:31:47.905Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "session.ended"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTsession.started

A technician remote session started

  • Contract 1.10.0
  • webhookSessionStarted

Re-joins do not repeat it. A session from a Denwen MSP launch has via: "flowmsp" (the contract v1 value, from when Denwen MSP was FlowMSP) and its launchId. Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST session.started
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST session.started
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.session requiredContractSessionA technician remote session (contract §8.4).
data.session.client required"native" | "web" | null1.3.0: the viewer the technician used, native or web; null for a tools session or a relay that does not report it.
data.session.deviceId requiredinteger
data.session.deviceUid requiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.
data.session.endedAt requiredstring (date-time) | null
data.session.id requiredstringSession id (UUID).
data.session.kind required"attended" | "unattended"attended when end-user consent was required.
data.session.launchId requiredstring | nullThe launch that produced this session (1.1).
data.session.purpose requiredstring | nulldesktop, tools or adhoc.
data.session.rmmUserId requiredinteger | nullThe operator's user id; null for an API or system actor.
data.session.startedAt requiredstring (date-time)When the session started.
data.session.technicianEmail requiredstring | nullThe operator's email. May be provider staff working under a support grant.
data.session.via required"flowmsp" | nullflowmsp when the session came from a Denwen MSP launch (1.1). (Denwen MSP, formerly FlowMSP: the value keeps its contract v1 spelling, and the RMM sends only this value.)
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"session.started"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "session": {
      "client": null,
      "deviceId": 1187,
      "deviceUid": "DWN-XYGW-9KF9-ZBEK-B8N$",
      "endedAt": null,
      "id": "6f1d2c4e-9b8a-4c3d-8e7f-0a1b2c3d4e5f",
      "kind": "unattended",
      "launchId": null,
      "purpose": "desktop",
      "rmmUserId": 3,
      "startedAt": "2026-10-21T09:12:03.120Z",
      "technicianEmail": "sam.taylor@acme-msp.example",
      "via": null
    }
  },
  "id": "evt_01M6RS0J8P7A0MY3BSHBFM1GAR",
  "occurredAt": "2026-10-21T09:12:03.120Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "session.started"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTsite.created

A site was created

  • Contract 1.10.0
  • webhookSiteCreated

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST site.created
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST site.created
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.site requiredContractSiteA site (contract §6.4).
data.site.address requiredContractAddressA postal address. Every part is a string or null.
data.site.address.city requiredstring | nullTown or city.
data.site.address.country requiredstring | nullCountry.
data.site.address.line1 requiredstring | nullAddress line 1.
data.site.address.line2 requiredstring | nullAddress line 2.
data.site.address.postalCode requiredstring | nullPostcode or ZIP.
data.site.address.region requiredstring | nullCounty, state or region.
data.site.customerId requiredinteger
data.site.deleted requiredbooleanTrue only for a tombstone (contract §7).
data.site.id requiredintegerSite id. Never reused.
data.site.kind required"normal" | "unassigned"Every customer may have one unassigned site; the tenant's holding area is the unassigned site of the unassigned customer.
data.site.name requiredstringUnique within the customer.
data.site.siteRef requiredstring | nullOptional site reference.
data.site.updatedAt requiredstring (date-time)Last change; for a tombstone, the deletion time.
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"site.created"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "site": {
      "address": {
        "city": "London",
        "country": "United Kingdom",
        "line1": "12 Harley Street",
        "line2": "Second floor",
        "postalCode": "W1G 9PF",
        "region": "Greater London"
      },
      "customerId": 42,
      "deleted": false,
      "id": 310,
      "kind": "normal",
      "name": "Harley Street surgery",
      "siteRef": "HS-01",
      "updatedAt": "2026-10-20T14:05:40.017Z"
    }
  },
  "id": "evt_01M6RS0J8NEWD6HAA7J2Y33TBA",
  "occurredAt": "2026-11-06T15:53:05.557Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "site.created"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTsite.deleted

A site was deleted (one per site when its customer is deleted)

  • Contract 1.10.0
  • webhookSiteDeleted

data.site is the last known site, with deleted: true. Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST site.deleted
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST site.deleted
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.site requiredContractSiteA site (contract §6.4).
data.site.address requiredContractAddressA postal address. Every part is a string or null.
data.site.address.city requiredstring | nullTown or city.
data.site.address.country requiredstring | nullCountry.
data.site.address.line1 requiredstring | nullAddress line 1.
data.site.address.line2 requiredstring | nullAddress line 2.
data.site.address.postalCode requiredstring | nullPostcode or ZIP.
data.site.address.region requiredstring | nullCounty, state or region.
data.site.customerId requiredinteger
data.site.deleted requiredbooleanTrue only for a tombstone (contract §7).
data.site.id requiredintegerSite id. Never reused.
data.site.kind required"normal" | "unassigned"Every customer may have one unassigned site; the tenant's holding area is the unassigned site of the unassigned customer.
data.site.name requiredstringUnique within the customer.
data.site.siteRef requiredstring | nullOptional site reference.
data.site.updatedAt requiredstring (date-time)Last change; for a tombstone, the deletion time.
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"site.deleted"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "site": {
      "address": {
        "city": "London",
        "country": "United Kingdom",
        "line1": "12 Harley Street",
        "line2": "Second floor",
        "postalCode": "W1G 9PF",
        "region": "Greater London"
      },
      "customerId": 42,
      "deleted": true,
      "id": 310,
      "kind": "normal",
      "name": "Harley Street surgery",
      "siteRef": "HS-01",
      "updatedAt": "2026-10-21T10:15:00.000Z"
    }
  },
  "id": "evt_01M6RS0J8P3H14NR6SG0Q7YS4D",
  "occurredAt": "2026-11-06T15:53:05.558Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "site.deleted"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.

POSTsite.updated

A site was edited

  • Contract 1.10.0
  • webhookSiteUpdated

Signed: X-Denwen-Signature: sha256=<lower hex HMAC-SHA256> over <X-Denwen-Timestamp>.<raw body>, keyed with the webhook secret exactly as pasted (UTF-8); reject a timestamp more than 300 s from your clock. Any 2xx acknowledges. 410 disables the integration; 408, 429, 5xx and timeouts (10 s per attempt) are retried, 8 attempts over about 45 hours; any other answer is permanent. Delivery is at-least-once and unordered: de-duplicate on the event id. data is a hint; re-read from the API (contract §8).

Headers
Parameters of POST site.updated
NameInTypeDescription
X-Denwen-Event requiredheaderstringRepeats type from the body (the body is authoritative).
X-Denwen-Event-Id requiredheaderstringRepeats id from the body; identical on every retry.
X-Denwen-Timestamp requiredheaderintegerUnix time in seconds, set on each attempt (a retry re-signs).
X-Denwen-Signature requiredheaderstringsha256=<lower hex>; exactly one signature.
X-Denwen-Delivery-Attempt requiredheaderintegerThe attempt number, from 1.
User-Agent requiredheader"DenwenRMM-Webhooks/1"Identifies the sender.
Payload
Body of POST site.updated
FieldTypeDescription
contractVersion requiredstringThe contract version (1.10.0).
data requiredobject
data.site requiredContractSiteA site (contract §6.4).
data.site.address requiredContractAddressA postal address. Every part is a string or null.
data.site.address.city requiredstring | nullTown or city.
data.site.address.country requiredstring | nullCountry.
data.site.address.line1 requiredstring | nullAddress line 1.
data.site.address.line2 requiredstring | nullAddress line 2.
data.site.address.postalCode requiredstring | nullPostcode or ZIP.
data.site.address.region requiredstring | nullCounty, state or region.
data.site.customerId requiredinteger
data.site.deleted requiredbooleanTrue only for a tombstone (contract §7).
data.site.id requiredintegerSite id. Never reused.
data.site.kind required"normal" | "unassigned"Every customer may have one unassigned site; the tenant's holding area is the unassigned site of the unassigned customer.
data.site.name requiredstringUnique within the customer.
data.site.siteRef requiredstring | nullOptional site reference.
data.site.updatedAt requiredstring (date-time)Last change; for a tombstone, the deletion time.
id requiredstringUnique per event, identical on every retry, never reused. De-duplicate on it.
occurredAt requiredstring (date-time)When the change happened in the RMM, not when it was sent.
tenant requiredobject
tenant.accountRef requiredstringThe RMM tenant; equals the one Denwen MSP pinned.
type required"site.updated"
Example payload
{
  "contractVersion": "1.10.0",
  "data": {
    "site": {
      "address": {
        "city": "London",
        "country": "United Kingdom",
        "line1": "12 Harley Street",
        "line2": "Second floor",
        "postalCode": "W1G 9PF",
        "region": "Greater London"
      },
      "customerId": 42,
      "deleted": false,
      "id": 310,
      "kind": "normal",
      "name": "Harley Street surgery",
      "siteRef": "HS-01",
      "updatedAt": "2026-10-20T14:05:40.017Z"
    }
  },
  "id": "evt_01M6RS0J8PEWWTS2D02V081WX1",
  "occurredAt": "2026-11-06T15:53:05.558Z",
  "tenant": {
    "accountRef": "TNT-4K7QP2"
  },
  "type": "site.updated"
}
Your response
2XX

Any 2xx acknowledges the delivery. Anything else is retried.