Guide to NinjaOne Webhooks: Features and Best Practices
NinjaOne, formerly NinjaRMM, is an endpoint management platform used by managed service providers and IT teams to monitor and patch devices, run scripts and handle tickets. Its API webhook pushes those activities to your own services as they happen: a monitoring condition firing or resetting, a script finishing, a patch failing, a device being added. If you sync NinjaOne with a PSA, a data warehouse or your own alerting, webhooks are how you avoid polling the activity log.
This guide covers how the NinjaOne API webhook is configured, how to authenticate it without a signature, what an activity payload looks like, and the gaps in NinjaOne's documentation that you should design around.
What are NinjaOne webhooks?
NinjaOne has two things called webhooks, and they behave differently:
- The API webhook. You configure it through the NinjaOne Public API with
PUT /v2/webhook. It sends activity notifications as JSON, and it can add custom headers to every request. - Webhook notification channels. These are set up in the NinjaOne console to send alert notifications to a URL. Only the URL can be configured.
This guide is about the API webhook. Because a notification channel can't send a custom header, and NinjaOne doesn't sign either kind, there is no way to authenticate notification channel requests.
The API webhook belongs to the API application, or client, that configures it. Each delivery is a POST whose body is, in NinjaOne's words, "very similar to the Activity object" from the API.
NinjaOne webhook features
| Feature | Details |
|---|---|
| Configuration | PUT /v2/webhook on your region's API host, per API application |
| Who can configure | System administrators only. Others get 403 |
| Disable | DELETE /v2/webhook |
| Filters | Activity types and organisation IDs |
| Expansions | device, organization, location, policy, rolePolicy, role |
| Method and body | POST, JSON similar to the Activity object |
| Event type | activityType in the body, refined by statusCode |
| Signature | None |
| Authentication | Custom headers you define, such as Authorization |
| Retries, timeout, success code | Not documented |
| Delivery headers | None documented beyond your own |
| Source IPs | Not documented for webhooks |
The API host depends on where your NinjaOne account lives:
| Region | Host |
|---|---|
| North America | https://app.ninjarmm.com |
| EMEA | https://eu.ninjarmm.com |
| APAC | https://oc.ninjarmm.com |
What an activity contains
NinjaOne publishes a JSON Schema for the payload but no example delivery. The fields are:
| Field | Description |
|---|---|
id | Activity identifier |
activityTime | Activity timestamp, as a number |
deviceId | Device identifier |
severity | NONE, MINOR, MODERATE, MAJOR or CRITICAL |
priority | NONE, LOW, MEDIUM or HIGH |
seriesUid | The job or condition UID the activity belongs to |
activityType | Activity type code, such as CONDITION, ACTION, ACTIONSET, PATCH_MANAGEMENT, SCRIPTING or SYSTEM |
statusCode | Activity status code, such as TRIGGERED, RESET, STARTED, COMPLETED, NODE_CREATED or NODE_UPDATED |
status | Status description |
activityResult | SUCCESS, FAILURE, UNSUPPORTED or UNCOMPLETED |
sourceConfigUid, sourceName | The policy element or configuration that produced the activity |
subject, message | Text describing the activity |
userId | User identifier |
type | Activity type description |
data | Additional activity data, as an object |
Each expansion you request adds the full referenced object to the payload, such as the device record or the organisation.
activityType has 38 documented values, and statusCode has several hundred, covering conditions, patching, scripting, backups, remote tools, ticketing and integrations.
Setting up NinjaOne webhooks
- Get an access token for your NinjaOne API application. The webhook is configured for that application, and only system administrators can configure it.
- Generate a long random secret to authenticate deliveries, for example 32 bytes from a secure random generator.
- Call
PUT /v2/webhookon your region's host:
{
"url": "https://example.com/webhooks/ninjaone",
"activities": {
"CONDITION": ["*"],
"PATCH_MANAGEMENT": ["*"]
},
"organizationIds": [1, 2],
"expand": ["device", "organization"],
"headers": [
{ "name": "Authorization", "value": "Bearer <your-secret>" }
]
}
- Expect
204 No Content. A403means the token doesn't belong to a system administrator.
The value for each activity type is "reserved for future more granular activity filtering", so use ["*"]. Calling PUT /v2/webhook again replaces the configuration, and DELETE /v2/webhook disables it.
Securing NinjaOne webhooks
NinjaOne doesn't sign webhooks. There is no signature header, no HMAC and no signing secret. The only authentication available is a header you add through the headers array, which NinjaOne describes as "Custom HTTP Headers (i.e. Authorization)". Your receiver compares that header with the value it expects, in constant time.
const crypto = require('crypto');
const express = require('express');
function verifyNinjaOneHeader(headerValue, expectedValue) {
if (!headerValue || !expectedValue) return false;
const received = Buffer.from(headerValue, 'utf8');
const expected = Buffer.from(expectedValue, 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
app.post('/webhooks/ninjaone', express.raw({ type: '*/*' }), (req, res) => {
const expected_value = process.env.NINJAONE_WEBHOOK_AUTH; // e.g. "Bearer <your-secret>"
if (!expected_value) return res.sendStatus(500); // fail closed
if (!verifyNinjaOneHeader(req.get('authorization'), expected_value)) {
return res.sendStatus(401);
}
const activity = JSON.parse(req.body.toString('utf8'));
// activity.activityType, activity.statusCode, activity.id ...
// Store it, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hmac
def verify_ninjaone_header(header_value, expected_value: str) -> bool:
if not header_value or not expected_value:
return False
return hmac.compare_digest(header_value.encode("utf-8"), expected_value.encode("utf-8"))
# In FastAPI:
# verify_ninjaone_header(request.headers.get("authorization"),
# os.environ["NINJAONE_WEBHOOK_AUTH"])
A static header proves the request came from someone who knows the secret. It doesn't protect the body from changes in transit or stop a captured request from being replayed, so always use an HTTPS URL and keep the secret out of logs.
In Hookdeck, the NinjaOne source type checks the header for you. Enter the header name, such as Authorization, and the exact value you put in the headers array, including any Bearer prefix.
Make NinjaOne webhooks production-ready. Hookdeck Event Gateway checks your secret header, stores every activity as it arrives, and delivers it to your service with retries you control.
NinjaOne webhook limitations and pain points
No signature
The Problem: Anyone who obtains your secret header can send requests that look like NinjaOne's, with any body they like.
Why It Happens: NinjaOne only lets you add static headers. Nothing in the request is computed from the body.
Workarounds:
- Use a long random secret, HTTPS only, and rotate it by calling
PUT /v2/webhookwith a new value. - Treat the payload as a notification. Before you take a significant action, fetch the activity or device from the NinjaOne API.
- Use the console's notification channels only for alerts where an unauthenticated request does no harm.
How Hookdeck Can Help: Hookdeck checks the header at the source and records requests that fail, so you can see when something other than NinjaOne is calling your URL.
Delivery behaviour is undocumented
The Problem: You can't tell whether NinjaOne retries a failed delivery, how long it waits for a response, or which status codes count as success.
Why It Happens: The webhook documentation covers configuration and the payload schema only. It says nothing about retries, timeouts, ordering or delivery IDs.
Workarounds:
- Respond with
200quickly and do the work afterwards. - Assume a failed delivery may be lost, and reconcile by reading activities from the API on a schedule.
How Hookdeck Can Help: Hookdeck accepts each activity at its edge and stores it, so the gap you need to worry about is only between NinjaOne and Hookdeck. From there, Hookdeck delivers to your service with retries and lets you replay any event.
Large payloads from expansions
The Problem: Payloads get large, and every activity repeats the same device and organisation data.
Why It Happens: Each expansion embeds a full object. NinjaOne warns that payloads become large depending on the expansions you choose, and encourages you to keep a local cache of core entities loaded from the Public API instead.
Workarounds:
- Request only the expansions you need, or none.
- Cache devices, organisations and policies locally, keyed by the IDs in the activity.
How Hookdeck Can Help: Transformations can strip fields you don't need before an event reaches your service.
Conflicting lists of activity types
The Problem: You can't be sure which activity types you can subscribe to.
Why It Happens: The payload schema lists 38 activity types, but the filter in PUT /v2/webhook lists only 30. The other eight, including NINJA_TICKETING, DOCUMENTATION, MICROSOFT_INTUNE and DYNAMIC_POLICY, appear in the payload schema but not in the filter, and NinjaOne doesn't say whether they can be subscribed to.
Workarounds:
- Log and acknowledge activity types you don't handle instead of rejecting them.
- Test a subscription to any of the eight before you depend on it.
How Hookdeck Can Help: Hookdeck filters can route on activityType and statusCode, so new or unexpected types can go to a separate destination for review.
Best practices
Compare the header in constant time
Use crypto.timingSafeEqual or hmac.compare_digest, check the length first in Node, and fail closed if the expected value isn't configured.
Route on activityType and statusCode
activityType tells you which part of NinjaOne produced the activity, and statusCode tells you what happened, such as TRIGGERED or RESET for a condition. Use activityResult when you need to know whether a job succeeded.
Deduplicate on the activity id
NinjaOne documents no delivery ID or retry behaviour. The activity's id identifies the activity, so record it before processing and skip repeats. See our guide to webhook idempotency.
Keep expansions small
Fewer expansions mean smaller requests and faster handling. Cache the entities you need and refresh them from the API.
Acknowledge fast and process asynchronously
Check the header, store the activity, return 200, and process afterwards. See why to process webhooks asynchronously.
Conclusion
The NinjaOne API webhook is configured with a single API call, filtered by activity type and organisation, and authenticated only by a header you add yourself. With no signature, no published example payload and nothing documented about retries, the safe approach is to compare the header carefully, treat each activity as a notification, and reconcile against the API.
Hookdeck Event Gateway sits in front of your receiver, checks your secret header, stores every activity, and delivers it to your service with retries you control.
Get started with Hookdeck for free and handle NinjaOne webhooks reliably in minutes.