# NinjaOne Webhooks

[NinjaOne](https://www.ninjaone.com/) is an RMM (remote monitoring and management) platform.
A NinjaOne webhook posts an **activity** (a monitoring condition firing, a script finishing,
a device being created, a patch failing) to your URL as JSON.

## When to Use This Skill

* How do I receive NinjaOne webhooks?
* How do I verify a NinjaOne webhook signature? (You can't. NinjaOne doesn't sign webhooks.
  Authenticate with a custom header instead.)
* How do I add an `Authorization` or `X-Webhook-Secret` header to a NinjaOne webhook?
* How do I handle a `CONDITION` activity with status `TRIGGERED` or `RESET`?
* Why can't I authenticate the webhook I created in the NinjaOne console?

## There Is No Signature

The NinjaOne API v2 spec describes no signature header, signing secret, timestamp or HMAC.
Don't invent an `X-NinjaOne-Signature` verifier: no real delivery would pass it.

The only authentication mechanism is the `headers` array on the webhook configuration
(`PUT /v2/webhook`, described as "Custom HTTP Headers (i.e. Authorization)"), which NinjaOne
sends back on every delivery. So you:

1. Generate a random secret yourself (`openssl rand -hex 32`).
2. Call `PUT /v2/webhook` with `"headers": [{ "name": "X-Webhook-Secret", "value": "<secret>" }]`.
3. On your endpoint, compare the `X-Webhook-Secret` header with the secret in constant time.
  Return `401` if it's missing or wrong.
4. Fail closed: if the secret isn't configured on your server, reject the request.

**Console-created webhooks can't be authenticated.** Notification-channel webhooks set up in
the NinjaOne console take a URL only, with no header field. Use the API configuration, or put
the endpoint behind something that can authenticate (for example a Hookdeck source).

This proves the caller knows the shared secret. It doesn't protect the body from tampering,
since nothing is signed over it, so the raw body isn't needed for verification.

## Verification (core)

```javascript
const crypto = require('crypto');

// NinjaOne sends the custom header you configured via PUT /v2/webhook.
function verifyNinjaOneSecret(headerValue, secret) {
  if (!secret) return false; // fail closed when NINJAONE_WEBHOOK_SECRET is unset
  if (typeof headerValue !== 'string') return false;
  // Hash both sides so timingSafeEqual always gets equal-length buffers
  const a = crypto.createHash('sha256').update(headerValue).digest();
  const b = crypto.createHash('sha256').update(secret).digest();
  return crypto.timingSafeEqual(a, b);
}

// verifyNinjaOneSecret(req.headers['x-webhook-secret'], process.env.NINJAONE_WEBHOOK_SECRET)

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ninjaone-webhooks/examples/express/),
> [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ninjaone-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ninjaone-webhooks/examples/fastapi/).

## Events: activityType + statusCode

NinjaOne has no single event-name field. Each delivery is an activity, identified by the pair
`activityType` and `statusCode`. Both are enums in the API's `Activity` schema. The spec lists the two enums separately and doesn't say which
combinations occur, so the pairings below are the common ones, not an exhaustive matrix:

| activityType | statusCode | Meaning |
| --- | --- | --- |
| `CONDITION` | `TRIGGERED` | A monitoring condition fired (an alert) |
| `CONDITION` | `RESET` | The condition cleared |
| `ACTION` | `STARTED`, `COMPLETED`, `CANCELLED` | A script or action ran on a device |
| `ACTIONSET` | `STARTED`, `COMPLETED`, `CANCELLED` | An automation (set of actions) ran |
| `SYSTEM` | `NODE_CREATED`, `NODE_UPDATED`, `NODE_DELETED` | Device lifecycle |

Other `activityType` values include `PATCH_MANAGEMENT`, `ANTIVIRUS`, `MONITOR`, `PSA` and
`MAINTENANCE_MODE`. The status code list has hundreds of values, so filter at the source with the
`activities` map in `PUT /v2/webhook` rather than receiving everything.

Payload fields: `id`, `activityTime`, `deviceId`, `severity` (`NONE`, `MINOR`, `MODERATE`,
`MAJOR`, `CRITICAL`), `priority`, `seriesUid`, `activityType`, `statusCode`, `status`,
`subject`, `message`, `data`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ninjaone-webhooks/references/overview.md).

## Environment Variables

```bash
# The secret YOU generated. NinjaOne must send: X-Webhook-Secret: <this value>
NINJAONE_WEBHOOK_SECRET=generate_with_openssl_rand_hex_32

```

## Local Development

```bash
npx hookdeck-cli listen 3000 ninjaone --path /webhooks/ninjaone

```

No account required. The CLI creates a guest account on first run and gives you a public URL
and a web UI for inspecting requests. Use port `8000` for the FastAPI example.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ninjaone-webhooks/references/overview.md) - What NinjaOne webhooks are, activity types and status codes, payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ninjaone-webhooks/references/setup.md) - Configuring the webhook with `PUT /v2/webhook`, the secret header, filters
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ninjaone-webhooks/references/verification.md) - Header authentication per framework, gotchas, debugging