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
| Field | Type | Description |
|---|---|---|
assertion required | string | A 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; equalsrequestIdin an error body.
200 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description actionrequiredContractActionA ticket action (contract §18.6). action.consoleUrlrequiredstring | nullThe device in the RMM console; link it only when it starts with the pinned base URL. action.createdAtrequiredstring (date-time)When the action was accepted. action.devicerequiredobjectaction.device.customerIdrequiredintegeraction.device.deviceUidrequiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.action.device.idrequiredintegeraction.device.siteIdrequiredintegeraction.finishedAtrequiredstring (date-time) | nullaction.idrequiredstringaction.idempotencyKeyrequiredstringaction.kindrequired"script" | "reboot" | "patch_scan" | "diagnostics"Open set on the wire: show an unknown kind generically. action.rebootrequiredobject | nullaction.reboot.backOnlineAtrequiredstring (date-time) | nullaction.reboot.noticeSecondsrequiredintegerThe notice actually given. action.reboot.userLoggedOnrequiredbooleanaction.refusalrequiredenum (25 values) | nullSet only for refusedandabandoned. Open set.action.requestedByrequiredobjectaction.requestedBy.emailrequiredstringaction.requestedBy.flowmspSubrequiredstringThe 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, wheresubcomes from the signed action assertion.)action.requestedBy.rmmUserIdrequiredintegerThe LINKED RMM user the action runs as. action.resultrequiredobject | nullNull until a terminal state other than refused.action.result.exitCoderequiredinteger | nullaction.result.redactionsrequiredintegerHow many credential-looking spans were replaced before sending. action.result.stderrrequiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days. action.result.stdoutrequiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days. action.result.summaryrequiredstringaction.result.truncatedrequiredbooleanaction.scriptrequiredobject | nullaction.script.idrequiredintegeraction.script.namerequiredstringaction.script.versionrequiredintegeraction.startedAtrequiredstring (date-time) | nullaction.staterequired"queued" | "running" | "succeeded" | "failed" | "timed_out" | "refused" | "abandoned"Closed set. action.ticketrequiredobjectaction.ticket.idrequiredstringDenwen MSP's ticket id. action.ticket.numberrequiredstring | nullThe ticket number people use, e.g. 10523.action.ticket.urlrequiredstringhttps, with the origin of the integration webhook URL. contractVersionrequiredstringreplayedrequiredbooleanTrue 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; equalsrequestIdin an error body.
202 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description actionrequiredContractActionA ticket action (contract §18.6). action.consoleUrlrequiredstring | nullThe device in the RMM console; link it only when it starts with the pinned base URL. action.createdAtrequiredstring (date-time)When the action was accepted. action.devicerequiredobjectaction.device.customerIdrequiredintegeraction.device.deviceUidrequiredstringThe permanent Denwen device id, DWN-XXXX-XXXX-XXXX-XXXX. Denwen MSP's key for a device.action.device.idrequiredintegeraction.device.siteIdrequiredintegeraction.finishedAtrequiredstring (date-time) | nullaction.idrequiredstringaction.idempotencyKeyrequiredstringaction.kindrequired"script" | "reboot" | "patch_scan" | "diagnostics"Open set on the wire: show an unknown kind generically. action.rebootrequiredobject | nullaction.reboot.backOnlineAtrequiredstring (date-time) | nullaction.reboot.noticeSecondsrequiredintegerThe notice actually given. action.reboot.userLoggedOnrequiredbooleanaction.refusalrequiredenum (25 values) | nullSet only for refusedandabandoned. Open set.action.requestedByrequiredobjectaction.requestedBy.emailrequiredstringaction.requestedBy.flowmspSubrequiredstringThe 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, wheresubcomes from the signed action assertion.)action.requestedBy.rmmUserIdrequiredintegerThe LINKED RMM user the action runs as. action.resultrequiredobject | nullNull until a terminal state other than refused.action.result.exitCoderequiredinteger | nullaction.result.redactionsrequiredintegerHow many credential-looking spans were replaced before sending. action.result.stderrrequiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days. action.result.stdoutrequiredstring | nullRedacted, at most 16384 bytes; null when output is not sent or after 7 days. action.result.summaryrequiredstringaction.result.truncatedrequiredbooleanaction.scriptrequiredobject | nullaction.script.idrequiredintegeraction.script.namerequiredstringaction.script.versionrequiredintegeraction.startedAtrequiredstring (date-time) | nullaction.staterequired"queued" | "running" | "succeeded" | "failed" | "timed_out" | "refused" | "abandoned"Closed set. action.ticketrequiredobjectaction.ticket.idrequiredstringDenwen MSP's ticket id. action.ticket.numberrequiredstring | nullThe ticket number people use, e.g. 10523.action.ticket.urlrequiredstringhttps, with the origin of the integration webhook URL. contractVersionrequiredstringreplayedrequiredbooleanTrue 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 (detailsnames the field), or a bad claim in the assertion:details.action,details.ticket,details.idempotencyKey,details.parameters.<name>ordetails.grant; ordetails.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; equalsrequestIdin an error body.
400 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description errorrequiredobjecterror.coderequiredenum (9 values)error.detailsmap of anyerror.messagerequiredstringrequestIdrequiredstringAlso sent as the X-Request-Id response header. - 401
unauthorized: the key, or the assertion, withdetails.reasonone 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; equalsrequestIdin an error body.
401 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description errorrequiredobjecterror.coderequiredenum (9 values)error.detailsmap of anyerror.messagerequiredstringrequestIdrequiredstringAlso sent as the X-Request-Id response header. - 403
forbidden: the key is not a paired integration key or the tenant is suspended; ordetails.reasonone oflaunch_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; equalsrequestIdin an error body.
403 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description errorrequiredobjecterror.coderequiredenum (9 values)error.detailsmap of anyerror.messagerequiredstringrequestIdrequiredstringAlso 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; equalsrequestIdin an error body.
404 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description errorrequiredobjecterror.coderequiredenum (9 values)error.detailsmap of anyerror.messagerequiredstringrequestIdrequiredstringAlso sent as the X-Request-Id response header. - 409
forbidden:details.reasonone ofdevice_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; equalsrequestIdin an error body.
409 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description errorrequiredobjecterror.coderequiredenum (9 values)error.detailsmap of anyerror.messagerequiredstringrequestIdrequiredstringAlso 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; equalsrequestIdin an error body.
410 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description errorrequiredobjecterror.coderequiredenum (9 values)error.detailsmap of anyerror.messagerequiredstringrequestIdrequiredstringAlso 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 afterRetry-Afterseconds.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; equalsrequestIdin an error body.
429 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description errorrequiredobjecterror.coderequiredenum (9 values)error.detailsmap of anyerror.messagerequiredstringrequestIdrequiredstringAlso sent as the X-Request-Id response header. - 500
internal: unexpected. Retry with backoff.X-Request-Id: Correlates the request; equalsrequestIdin an error body.
500 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description errorrequiredobjecterror.coderequiredenum (9 values)error.detailsmap of anyerror.messagerequiredstringrequestIdrequiredstringAlso 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; equalsrequestIdin an error body.
503 response of POST /api/integrations/denwen-msp/v1/actions Field Type Description errorrequiredobjecterror.coderequiredenum (9 values)error.detailsmap of anyerror.messagerequiredstringrequestIdrequiredstringAlso sent as the X-Request-Id response header.
curl -X POST "https://<your instance>/api/integrations/denwen-msp/v1/actions" \
-H "Authorization: Bearer <your key>" \
-H "Content-Type: application/json" \
-d '{ … }'