Gareth Wilson Gareth Wilson

Guide to NinjaOne Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationPUT /v2/webhook on your region's API host, per API application
Who can configureSystem administrators only. Others get 403
DisableDELETE /v2/webhook
FiltersActivity types and organisation IDs
Expansionsdevice, organization, location, policy, rolePolicy, role
Method and bodyPOST, JSON similar to the Activity object
Event typeactivityType in the body, refined by statusCode
SignatureNone
AuthenticationCustom headers you define, such as Authorization
Retries, timeout, success codeNot documented
Delivery headersNone documented beyond your own
Source IPsNot documented for webhooks

The API host depends on where your NinjaOne account lives:

RegionHost
North Americahttps://app.ninjarmm.com
EMEAhttps://eu.ninjarmm.com
APAChttps://oc.ninjarmm.com

What an activity contains

NinjaOne publishes a JSON Schema for the payload but no example delivery. The fields are:

FieldDescription
idActivity identifier
activityTimeActivity timestamp, as a number
deviceIdDevice identifier
severityNONE, MINOR, MODERATE, MAJOR or CRITICAL
priorityNONE, LOW, MEDIUM or HIGH
seriesUidThe job or condition UID the activity belongs to
activityTypeActivity type code, such as CONDITION, ACTION, ACTIONSET, PATCH_MANAGEMENT, SCRIPTING or SYSTEM
statusCodeActivity status code, such as TRIGGERED, RESET, STARTED, COMPLETED, NODE_CREATED or NODE_UPDATED
statusStatus description
activityResultSUCCESS, FAILURE, UNSUPPORTED or UNCOMPLETED
sourceConfigUid, sourceNameThe policy element or configuration that produced the activity
subject, messageText describing the activity
userIdUser identifier
typeActivity type description
dataAdditional 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

  1. Get an access token for your NinjaOne API application. The webhook is configured for that application, and only system administrators can configure it.
  2. Generate a long random secret to authenticate deliveries, for example 32 bytes from a secure random generator.
  3. Call PUT /v2/webhook on 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>" }
  ]
}
  1. Expect 204 No Content. A 403 means 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/webhook with 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 200 quickly 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.