Guide to Jamf Pro Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | Settings > Global > Webhooks > New |
| Subscription model | One event per webhook. Several webhooks can share a URL |
| Method | POST |
| Format | JSON or XML, chosen per webhook |
| Event identifier | webhook.webhookEvent |
| Event time | webhook.eventTimestamp, epoch milliseconds |
| Authentication | None, Basic, Header Authentication, Mutual TLS, or Hash Signature |
| Timeouts | Connection and read timeouts set per webhook |
| Retries | Not documented |
| Source IPs | Not published |
Common events
Jamf documents 22 events:
| Area | Events |
|---|---|
| Computers | ComputerAdded, ComputerCheckIn, ComputerInventoryCompleted, ComputerPolicyFinished, ComputerPatchPolicyCompleted, ComputerPushCapabilityChanged |
| Mobile devices | MobileDeviceEnrolled, MobileDeviceUnenrolled, MobileDeviceCheckIn, MobileDeviceCommandCompleted, MobileDeviceInventoryCompleted, MobileDevicePushSent |
| Smart groups | SmartGroupComputerMembershipChange, SmartGroupMobileDeviceMembershipChange, SmartGroupUserMembershipChange |
| Enrollment and patching | DeviceAddedToDEP, PatchSoftwareTitleUpdated |
| Server and API | JSSStartup, JSSShutdown, RestAPIOperation, PushSent |
| Certificates | SCEPChallenge |
A few have rules worth knowing:
ComputerAddeddoesn't fire when a computer that already has an inventory record re-enrolls.MobileDeviceEnrolledfires on enrolment and re-enrolment.ComputerPolicyFinishedandMobileDeviceCommandCompletedfire for failures as well as successes.ComputerCheckInfires on startup, login, logout, network state change, enrolment complete and recurring check-in, so it can be very busy.RestAPIOperationfires for every authorised request to a supported Classic API resource. Failed and unauthorised requests don't trigger it.PushSentdoesn't say whether the device was a computer or a mobile device, and Jamf notes the IDs aren't unique across the two.MobileDeviceInventoryCompletedneeds Jamf Pro 10.47.0 or later.- The section titled
MobileDeviceUnenrolledshowsMobileDeviceUnEnrolled, 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
- Generate a long random secret, for example with
openssl rand -hex 32. - In Jamf Pro, click Settings in the sidebar, then Webhooks in the Global section, then New.
- Enter a display name and your HTTPS endpoint URL.
- Choose Header Authentication and enter the header as JSON, for example
{"Authorization":"Bearer <secret>"}. Jamf doesn't allowContent-Type,User-Agent,Accept-Encoding,Content-LengthorHostas keys. - Enter the connection and read timeouts.
- Choose JSON as the format.
- Choose the event. For smart group events, also choose the group.
- 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.