Gareth Wilson Gareth Wilson

Guide to Jamf Pro Webhooks: Features and Best Practices

Published


Jamf Pro webhooks report what's happening across your Apple fleet: a Mac enrolls, a device checks in or finishes an inventory update, a policy runs, a smart group gains or loses members, or the Jamf Pro server itself starts or shuts down. If you manage devices with Jamf Pro, webhooks are how those events reach your other IT systems without polling the Jamf Pro API.

This guide covers how Jamf Pro webhooks are configured, the authentication options and which one to use, the events and payload structure, and the practices that keep a Jamf Pro receiver reliable.

What are Jamf Pro webhooks?

Jamf Pro webhooks "allow you to subscribe to specific events on a Jamf Pro instance. When an event occurs, an HTTP POST payload is sent to a specified URL." Each webhook listens for exactly one event, so you create one webhook per event you need, and several can share a URL.

Each webhook also sets its own authentication, timeouts and format. The format is JSON or XML. Every JSON payload has two parts: webhook, which identifies the webhook and the event, and event, which carries the data.

Jamf Pro offers five authentication types. Header Authentication sends headers you define, as JSON key/value pairs, on every request, and your receiver compares one of them with the value it expects. That's the option this guide uses. Jamf Pro also has a Hash Signature option that signs webhooks with SHA-256 or SHA-512, but Jamf doesn't publicly document which header carries the signature or exactly what is signed, so you can't build a verifier for it from the documentation.

Jamf Pro webhook features

FeatureDetails
ConfigurationSettings > Global > Webhooks > New
Subscription modelOne event per webhook. Several webhooks can share a URL
MethodPOST
FormatJSON or XML, chosen per webhook
Event identifierwebhook.webhookEvent
Event timewebhook.eventTimestamp, epoch milliseconds
AuthenticationNone, Basic, Header Authentication, Mutual TLS, or Hash Signature
TimeoutsConnection and read timeouts set per webhook
RetriesNot documented
Source IPsNot published

Common events

Jamf documents 22 events:

AreaEvents
ComputersComputerAdded, ComputerCheckIn, ComputerInventoryCompleted, ComputerPolicyFinished, ComputerPatchPolicyCompleted, ComputerPushCapabilityChanged
Mobile devicesMobileDeviceEnrolled, MobileDeviceUnenrolled, MobileDeviceCheckIn, MobileDeviceCommandCompleted, MobileDeviceInventoryCompleted, MobileDevicePushSent
Smart groupsSmartGroupComputerMembershipChange, SmartGroupMobileDeviceMembershipChange, SmartGroupUserMembershipChange
Enrollment and patchingDeviceAddedToDEP, PatchSoftwareTitleUpdated
Server and APIJSSStartup, JSSShutdown, RestAPIOperation, PushSent
CertificatesSCEPChallenge

A few have rules worth knowing:

  • ComputerAdded doesn't fire when a computer that already has an inventory record re-enrolls. MobileDeviceEnrolled fires on enrolment and re-enrolment.
  • ComputerPolicyFinished and MobileDeviceCommandCompleted fire for failures as well as successes.
  • ComputerCheckIn fires on startup, login, logout, network state change, enrolment complete and recurring check-in, so it can be very busy.
  • RestAPIOperation fires for every authorised request to a supported Classic API resource. Failed and unauthorised requests don't trigger it.
  • PushSent doesn't say whether the device was a computer or a mobile device, and Jamf notes the IDs aren't unique across the two.
  • MobileDeviceInventoryCompleted needs Jamf Pro 10.47.0 or later.
  • The section titled MobileDeviceUnenrolled shows MobileDeviceUnEnrolled, with a capital E, as the value sent. Match the value you actually receive.

Here is the shape of a ComputerAdded payload, trimmed. Jamf's own samples show data types ("string", integer, epoch) rather than values, so this version fills in example values:

{
  "event": {
    "deviceName": "Ada's MacBook Pro",
    "jssID": 52,
    "model": "MacBookPro18,3",
    "osVersion": "14.5",
    "serialNumber": "C02XXXXXXXXX",
    "udid": "55900BDC-347C-58B1-D249-F32244B11D30",
    "username": "ada",
    "managementId": "1a2b3c4d-0000-0000-0000-000000000000"
  },
  "webhook": {
    "eventTimestamp": 1729853000123,
    "id": 7,
    "name": "Computer added",
    "webhookEvent": "ComputerAdded"
  }
}

jssID is the Jamf Pro ID of the object, the same one you see in the Jamf Pro URL for that record. webhook.id and webhook.name identify the webhook that sent the event, which helps when several webhooks point at one URL. With XML, the same data arrives inside a <JSSEvent> element.

Setting up Jamf Pro webhooks

  1. Generate a long random secret, for example with openssl rand -hex 32.
  2. In Jamf Pro, click Settings in the sidebar, then Webhooks in the Global section, then New.
  3. Enter a display name and your HTTPS endpoint URL.
  4. Choose Header Authentication and enter the header as JSON, for example {"Authorization":"Bearer <secret>"}. Jamf doesn't allow Content-Type, User-Agent, Accept-Encoding, Content-Length or Host as keys.
  5. Enter the connection and read timeouts.
  6. Choose JSON as the format.
  7. Choose the event. For smart group events, also choose the group.
  8. Click Save, and repeat for each event you need.

In Hookdeck. Choose Header Authentication on the Jamf Pro webhook, then create a source with the Jamf Pro type and enter the same header: the JSON key in API Key Header and the JSON value in API Key, including any Bearer prefix. Use the source URL as the webhook URL. Hookdeck matches the name case-insensitively and the value exactly. The source accepts POST. It doesn't check Basic Authentication, Mutual TLS or Hash Signature, so a webhook using one of those needs Header Authentication for Hookdeck to verify it. Choose JSON as the format, so Hookdeck's filters and transformations can work with the fields.

For local development, hookdeck listen 3000 jamf-pro --path /webhooks/jamf-pro with the Hookdeck CLI gives you a public HTTPS URL, with no account required.

Securing Jamf Pro webhooks

Read the header you defined in Header Authentication and compare its whole value with the secret in constant time.

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

const app = express();

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on buffers of different lengths
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/jamf-pro', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.JAMF_PRO_WEBHOOK_SECRET; // the JSON value, e.g. "Bearer 4e8d..."
  if (!secret) return res.sendStatus(500); // fail closed when unconfigured

  const name = process.env.JAMF_PRO_WEBHOOK_HEADER || 'Authorization'; // the JSON key
  const received = req.get(name); // case-insensitive lookup
  if (typeof received !== 'string' || !safeEqual(received, secret)) {
    return res.sendStatus(401);
  }

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400); // e.g. a webhook still set to XML
  }

  const { webhookEvent, id, eventTimestamp } = payload.webhook || {};
  // Queue { webhookEvent, id, eventTimestamp, event: payload.event } for a worker.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os


def verify_jamf_pro_request(headers) -> bool:
    secret = os.environ.get("JAMF_PRO_WEBHOOK_SECRET")
    if not secret:
        return False  # fail closed
    name = os.environ.get("JAMF_PRO_WEBHOOK_HEADER", "Authorization")
    received = headers.get(name)  # case-insensitive in Starlette and Flask
    if not received:
        return False
    return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))

A matching header proves the sender knows the value. It doesn't prove the body is unchanged, and a captured request can be replayed because nothing is signed. eventTimestamp is in every payload, so your own receiver can reject events older than you're willing to accept, but it isn't covered by any check, so it's a sanity test rather than proof of freshness. Use HTTPS and a long random value, and update the webhook and your receiver together if it may have leaked.

Make Jamf Pro webhooks production-ready. Hookdeck Event Gateway checks your Header Authentication value on every Jamf Pro webhook, stores each device and policy event durably, and retries delivery to your service on a schedule you control.

Jamf Pro webhook limitations and pain points

SCEPChallenge needs an answer

The Problem: A SCEPChallenge webhook routed through a queue or gateway breaks certificate distribution.

Why It Happens: "The SCEPChallenge event is the only event that currently supports the ability for Jamf Pro to interpret a response. A valid response should consist of the dynamic challenge to be used within the SCEP payload." Every other event is fire-and-forget, but this one expects your service to answer with data.

Workarounds:

  • Point SCEPChallenge webhooks directly at the service that issues challenges.
  • Keep that service fast and highly available, because device enrolment waits on it.

How Hookdeck Can Help: Hookdeck acknowledges each request itself and delivers it to your service afterwards, so your service's response never reaches Jamf Pro. Send SCEPChallenge straight to your challenge service, and route the other 21 events through Hookdeck.

Retries aren't documented

The Problem: You can't tell whether Jamf Pro tries again if your endpoint is down.

Why It Happens: Jamf documents per-webhook connection and read timeouts, but not retries or which response codes count as success.

Workarounds:

  • Respond quickly and keep the timeouts reasonable.
  • Reconcile state from the Jamf Pro API after an outage, especially for enrolment and unenrolment.

How Hookdeck Can Help: Hookdeck accepts each Jamf Pro webhook as it arrives, then retries delivery to your service and lets you replay any event, so an outage on your side doesn't depend on Jamf Pro sending again.

Hash Signature can't be verified from the docs

The Problem: You turn on Hash Signature for stronger security, then can't write the code to check it.

Why It Happens: Jamf describes the option and its SHA-256 or SHA-512 algorithms, but not the header name, the encoding or what's signed.

Workarounds:

  • Use Header Authentication, with a long random value, over HTTPS.
  • If you capture a signed delivery and work out the scheme, treat it as undocumented behaviour that may change.

How Hookdeck Can Help: Hookdeck's Jamf Pro source compares the Header Authentication header you name with the exact value you enter. Hookdeck answers Jamf Pro with a 200 either way, then records a request without the right header as failed verification, so it never reaches your destination.

High-volume events

The Problem: ComputerCheckIn or RestAPIOperation floods a receiver built for occasional events.

Why It Happens: Every managed computer checks in on startup, login, logout, network changes and on its recurring schedule, and RestAPIOperation fires on every authorised Classic API request.

Workarounds:

  • Only subscribe to these events if you need them, and on a dedicated endpoint.
  • Acknowledge immediately and process in a queue.

How Hookdeck Can Help: Hookdeck absorbs bursts and delivers to your service at a rate you set, so a fleet-wide check-in wave doesn't overwhelm it.

Best practices

Choose JSON

Set every webhook to JSON unless a consumer needs XML. JSON is easier to route on and is what most tooling expects.

Route on webhook.webhookEvent

Since one URL can serve many webhooks, switch on webhookEvent and ignore events you don't recognise with a 200. Match the exact strings you receive, such as MobileDeviceUnEnrolled.

Deduplicate deliveries

Payloads have no event ID. Build a key from webhook.id, webhook.eventTimestamp and the object's jssID or udid, and store it before processing. See our guide to webhook idempotency.

Re-read before destructive actions

For unenrolment or smart group changes that remove access, confirm the device's current state through the Jamf Pro API before acting.

Acknowledge fast and process asynchronously

Verify, store, respond with 200, then do the work. See why to process webhooks asynchronously.

Conclusion

Jamf Pro sends one event per webhook as a POST, with webhook metadata and event data, and Header Authentication is the documented way to authenticate it. Compare your header in constant time over HTTPS, choose JSON, route on webhookEvent, deduplicate without an event ID, and keep SCEPChallenge on a direct, synchronous path.

Hookdeck Event Gateway sits in front of your receiver for every other event, checks the header, keeps each event, and retries delivery to your service, which fills the gap left by Jamf Pro's undocumented retry behaviour.

Get started with Hookdeck for free and handle Jamf Pro 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.