# Guide to JumpCloud Webhooks: Features and Best Practices

JumpCloud webhooks send directory and device alerts to the tools your team already works in. A device runs low on disk space, unauthorised software is detected, a Directory Insights rule matches a suspicious event, or someone files an access request: a webhook channel can push each of those to a SIEM, a ticketing system or your own service as it happens.

This guide covers how JumpCloud webhook channels are set up and attached to rules, what the alert payload contains, how to authenticate requests with Basic authentication, the retry behaviour, and the best practices for production.

## What are JumpCloud webhooks?

In JumpCloud, a webhook is a **notification channel**: a URL plus authentication and headers, created once in the Admin Portal and then attached to whatever should notify it. Three kinds of events can use a channel:

* **Alert notifications** from alert rules, such as low disk space or unauthorised software.
* **Directory Insights events**, through an Insights rule.
* **Access Management events**, chosen in the Access Requests settings without a separate rule.

A channel can be reused across many rules and services. JumpCloud sends each notification as a `POST` with a JSON body, using a fixed schema per event type.

JumpCloud doesn't sign webhook requests. A channel can authenticate with **Basic Authentication** (a username and password), **Token Authentication**, or custom static headers. Basic Authentication is the one this guide uses and the one Hookdeck's JumpCloud source checks. JumpCloud doesn't document which header or scheme Token Authentication uses.

## JumpCloud webhook features

| Feature | Details |
| --- | --- |
| Configuration | Admin Portal: **Settings > Notification Channels > Webhooks** |
| Triggers | Alert rules, Insights rules and Access Request events |
| Method | `POST` with a JSON body |
| Payload | Fixed JSON schema per event type, not customisable |
| Authentication | None, Basic Authentication, Token Authentication, or up to 25 custom headers |
| Signature | None |
| URL secrets | Up to 10 sensitive values in the URL can be masked and encrypted |
| Retries | Three, with exponential backoff, on `5xx`, `408` or `429` |
| Failure logging | Failures are logged in Directory Insights |
| Testing | **Test Webhook** shows a simulated request and response |
| Source IPs | Listed in JumpCloud's Data Centers article |

## Common events

JumpCloud doesn't publish a list of event types or an example body. What it does document is the set of fields an alert notification carries:

| Field | Description |
| --- | --- |
| `alert_id` | ID of the alert |
| `alert_title` | Title of the alert |
| `category` | Category of the alert |
| `description` | Detailed description of the alert |
| `di_event` | Directory Insights event associated with the alert |
| `event_description` | Description of the event that triggered the alert |
| `priority` | `Critical`, `High`, `Medium` or `Low` |
| `status` | `Open`, `Acknowledged`, `Resolved` or `Auto-Resolved` |
| `previous_alert_status` | The alert's previous status |
| `rule_id` | ID of the rule that triggered the alert |
| `source_id`, `source_name`, `source_type`, `source_url` | The alert source |
| `initiated_by` | User who initiated the change |
| `occurred_at`, `update_at` | When the alert occurred and was last updated |
| `organization_name`, `in_console_alert` | Organisation name, and the alert as generated in the console |

There is no single event type field. Route on `rule_id`, `category`, `priority` or `status`, and use `status` with `previous_alert_status` to tell a new alert from a change to an existing one. JumpCloud doesn't show how these fields are nested or what values `category` and `di_event` take, and Access Request notifications aren't described at all, so capture real deliveries before writing strict parsing.

> **Inspect JumpCloud webhooks as they arrive.** Point a JumpCloud channel at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to inspect and replay real deliveries, with no account or setup required. With no example body in the docs, it's the quickest way to see what your rules actually send.

## Setting up JumpCloud webhooks

Pick a username and generate a long random password (`openssl rand -hex 32` works), and store both in your app's environment.

1. Log in to the JumpCloud Admin Portal. If your data is stored outside the US, use your region's login URL.
2. Go to **Settings > Notification Channels** and click **Webhooks**.
3. Click **+Add**. Leave **Webhook Status** enabled, and give the channel a unique name of up to 100 characters.
4. Enter your HTTPS endpoint in **URL**.
5. Under authentication, choose **Basic Authentication** and enter the username and password.
6. Click **Test Webhook** to send a simulated request, then **Save**.

Saving the channel doesn't send anything by itself. Attach it where events come from:

* For alerts, create or edit an alert rule and add the channel to it.
* For Directory Insights events, create an Insights rule that uses the channel.
* For access requests, go to **Access > Access Requests** and select the events to send to the channel.

Values marked as sensitive, in the URL or in headers, can't be viewed or changed after saving, so keep your own copy.

For local development, use the [Hookdeck CLI](/docs/cli): `hookdeck listen 3000 jumpcloud --path /webhooks/jumpcloud` gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required.

**With Hookdeck.** Create a JumpCloud source in Hookdeck, enter the same **Username** and **Password** as the channel, and use the source URL as the channel's URL. JumpCloud doesn't say whether it sends Basic credentials on the first request or only after a `401` challenge. Hookdeck never sends a challenge, so the source only works if JumpCloud sends the `Authorization` header up front. Send a **Test Webhook** and check that the request in Hookdeck carries an `Authorization: Basic ...` header before you rely on it.

## Securing JumpCloud webhooks

With Basic Authentication, every request carries `Authorization: Basic <base64 of username:password>`. Decode it, split on the first colon, and compare both parts against your stored values in constant time. Check lengths before calling `crypto.timingSafeEqual`, which throws on buffers of different lengths, and evaluate both comparisons so the timing doesn't reveal which one failed.

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

const app = express();

function safeEqual(a, b) {
  const x = Buffer.from(String(a), 'utf8');
  const y = Buffer.from(String(b), 'utf8');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

function verifyJumpCloudRequest(authorization) {
  const user = process.env.JUMPCLOUD_WEBHOOK_USERNAME;
  const pass = process.env.JUMPCLOUD_WEBHOOK_PASSWORD;
  if (!user || !pass) return false; // fail closed when unconfigured

  const match = /^basic\s+([A-Za-z0-9+/]+={0,2})$/i.exec((authorization || '').trim());
  if (!match) return false;

  const decoded = Buffer.from(match[1], 'base64').toString('utf8');
  const i = decoded.indexOf(':');
  if (i < 0) return false;

  const userOk = safeEqual(decoded.slice(0, i), user);
  const passOk = safeEqual(decoded.slice(i + 1), pass);
  return userOk && passOk;
}

app.post('/webhooks/jumpcloud', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyJumpCloudRequest(req.headers.authorization)) {
    return res.sendStatus(401);
  }

  let alert;
  try {
    alert = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  res.sendStatus(200);
  // Route on alert.rule_id, alert.priority or alert.status
});

```

The same check in Python:

```python
import base64
import binascii
import hmac
import os

def verify_jumpcloud_request(authorization) -> bool:
    user = os.environ.get("JUMPCLOUD_WEBHOOK_USERNAME")
    password = os.environ.get("JUMPCLOUD_WEBHOOK_PASSWORD")
    if not user or not password or not authorization:
        return False
    scheme, _, encoded = authorization.strip().partition(" ")
    if scheme.lower() != "basic":
        return False
    try:
        decoded = base64.b64decode(encoded.strip(), validate=True).decode("utf-8")
    except (binascii.Error, UnicodeDecodeError):
        return False
    got_user, sep, got_pass = decoded.partition(":")
    if not sep:
        return False
    user_ok = hmac.compare_digest(got_user.encode(), user.encode())
    pass_ok = hmac.compare_digest(got_pass.encode(), password.encode())
    return user_ok and pass_ok

```

Basic credentials prove the sender knows the password. They don't protect the body, and nothing stops a captured request being replayed, because there is no signature or timestamp. Base64 is an encoding, not encryption, so the endpoint must be HTTPS. Rotate the password in the channel and your environment if it might have leaked, and consider allowing only the addresses in JumpCloud's Data Centers article at your firewall.

> **Make JumpCloud webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the channel's Basic credentials on every request, stores each alert durably, and retries delivery to your service well beyond JumpCloud's three attempts.

## JumpCloud webhook limitations and pain points

### Three retries, then nothing

**The Problem:** If your endpoint is down for longer than a short backoff window, the alert is lost.

**Why It Happens:** JumpCloud retries three times with exponential backoff, and only for `5xx`, `408` and `429` responses. Other errors, such as a `401` from a misconfigured receiver or a `404` from a wrong path, aren't retried at all. Failures are logged in Directory Insights.

**Workarounds:**

* Return `5xx` for transient problems in your own code so JumpCloud at least retries.
* Monitor Directory Insights for webhook failures.
* For important alerts, check their state in the Admin Portal after an outage.

**How Hookdeck Can Help:** Hookdeck accepts each notification as soon as JumpCloud sends it, then delivers it to your service with [automatic retries](/docs/retries) on a schedule you control. [Issues](/docs/issues) alert you when deliveries fail, and you can replay any stored alert.

### Basic auth must be sent up front

**The Problem:** A receiver that only checks credentials after issuing a `401` challenge never sees them, if the sender waits for one.

**Why It Happens:** JumpCloud doesn't document whether its Basic Authentication is preemptive. Hookdeck verifies the request as it arrives and issues no challenge.

**Workarounds:**

* Send a **Test Webhook** and confirm the `Authorization` header is present on the first request.
* If it isn't, add a custom secret header instead and check that in your own receiver.

**How Hookdeck Can Help:** Hookdeck's JumpCloud source checks the Basic credentials on each request. A request without valid credentials still gets a 200 response, but Hookdeck marks it as failed verification and creates no event, so it never reaches your destination. The stored request shows its headers, which makes a missing `Authorization` header easy to spot.

### Thin payload documentation

**The Problem:** There's no example body, no list of `category` or `di_event` values, and no description of the Access Request payload.

**Why It Happens:** JumpCloud documents a field table for alerts and says payloads follow a fixed JSON schema per event type, without publishing those schemas.

**Workarounds:**

* Capture real deliveries for each rule and build your parser from them.
* Read fields defensively and log bodies you don't recognise instead of rejecting them.

**How Hookdeck Can Help:** Every request is stored with its body, so you can collect real examples and replay them against your handler. [Filters](/docs/filters) can route alerts by `priority` or `rule_id` to different destinations.

## Best practices

### Use a dedicated credential per channel

Give each channel its own username and password, so you can rotate one without touching the others and tell sources apart in logs.

### Treat alerts as state changes

An alert can be sent more than once as its `status` moves from `Open` to `Acknowledged` to `Resolved`. Key your records on `alert_id` and update the status rather than creating a new record each time.

### Acknowledge fast and process asynchronously

Return `200` once the credentials check out, then open tickets or page people from a background job. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent

Retries can repeat a notification. Deduplicate on `alert_id` with `status` and `update_at`. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

JumpCloud webhook channels send unsigned JSON alerts, Directory Insights events and access requests, authenticated with Basic credentials you set on the channel. Check those credentials in constant time over HTTPS, confirm they arrive on the first request, and plan for JumpCloud's short retry budget and thin payload documentation.

[Hookdeck Event Gateway](/event-gateway) checks the Basic credentials, stores every alert on arrival and retries delivery to your service, so an outage on your side doesn't mean missed security alerts.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle JumpCloud webhooks reliably in minutes.