Agent skill
Jamf Pro Webhooks Skill
Receive and authenticate Jamf Pro webhooks. Use when setting up a Jamf Pro webhook endpoint, checking the Header Authentication header, debugging 401s, or handling Apple device management events like ComputerAdded, ComputerCheckIn, ComputerInventoryCompleted, MobileDeviceEnrolled and JSSStartup.
Install this skill
npx skills add hookdeck/webhook-skills --skill jamf-pro-webhooks
Jamf Pro (Apple device management) sends a webhook when something happens on the server: a computer enrolls or checks in, a mobile device enrolls, an inventory update completes, the server starts. A webhook can send JSON or XML; this skill assumes JSON. Webhooks are not signed in the way this skill covers. To authenticate them, choose Header Authentication on the Jamf Pro webhook: you enter a JSON object of header names and values, Jamf Pro sends those headers on every request, and your endpoint compares one of them with the value you stored.
Choose Header Authentication on the Jamf Pro webhook and enter the same header name and value here.
Basic authentication, mTLS and the Hash Signature option are not covered. The Hash Signature header and what it signs aren't publicly documented, so this skill doesn't verify it.
When to Use This Skill
- How do I receive Jamf Pro webhooks?
- How do I authenticate a Jamf Pro webhook with Header Authentication?
- Does Jamf Pro sign webhook payloads? (Not in a documented way: use a shared header value)
- How do I handle
ComputerAdded,ComputerCheckInorMobileDeviceEnrolledevents? - Why does my Jamf Pro webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyJamfProRequest(headers, name = process.env.JAMF_PRO_WEBHOOK_HEADER || 'Authorization',
secret = process.env.JAMF_PRO_WEBHOOK_SECRET) {
if (!secret) return false; // never accept when unconfigured
const received = headers[name.toLowerCase()]; // header names are case-insensitive
return Boolean(received) && safeEqual(received, secret); // compare the WHOLE value
}
- The header name is whatever you typed as the JSON key in Header Authentication, so read it from
JAMF_PRO_WEBHOOK_HEADER(defaultAuthorization). Look it up case-insensitively. - The expected value is the exact, whole header value, including any
Bearerprefix you typed (e.g.{"Authorization": "Bearer <token>"}). Do not strip or parse the prefix. - Return
401when the header is missing or doesn't match,400for a body that isn't a JSON object,200once handled. - Read the body raw (
express.raw({ type: '*/*' })) so any content type works, and authenticate before parsing it. - Python:
hmac.compare_digeston bytes (encode both sides;strraises on non-ASCII).
What this does and doesn't give you: a static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, keep the value secret, and rotate it in both Jamf Pro and your endpoint if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The body is { webhook: { id, name, webhookEvent, eventTimestamp }, event: { ... } }. The event name is webhook.webhookEvent; the shape of event depends on it.
webhookEvent | Sent When |
|---|---|
ComputerAdded | A computer is enrolled into Jamf Pro |
ComputerCheckIn | A managed computer checks in with the server |
ComputerInventoryCompleted | A computer finishes submitting inventory |
ComputerPatchPolicyCompleted | A computer patch policy completes |
ComputerPolicyFinished | A policy finishes on a computer |
ComputerPushCapabilityChanged | A computer's push capability changes |
DeviceAddedToDEP | A device is added to Automated Device Enrollment |
JSSStartup / JSSShutdown | The Jamf Pro server starts / shuts down |
MobileDeviceEnrolled | A mobile device is enrolled |
MobileDeviceUnEnrolled | A mobile device's MDM profile is removed or it is unmanaged |
MobileDeviceCheckIn | A mobile device checks in |
MobileDeviceCommandCompleted | An MDM command completes on a mobile device |
MobileDeviceInventoryCompleted | A mobile device finishes submitting inventory |
MobileDevicePushSent | A push notification is sent to a mobile device |
PatchSoftwareTitleUpdated | A patch software title is updated |
PushSent | A push notification is sent |
RestAPIOperation | An authorized Classic API request is made |
SCEPChallenge | A SCEP challenge is requested |
SmartGroupComputerMembershipChange | A computer smart group's membership changes |
SmartGroupMobileDeviceMembershipChange | A mobile device smart group's membership changes |
SmartGroupUserMembershipChange | A user smart group's membership changes |
Environment Variables
JAMF_PRO_WEBHOOK_HEADER=Authorization # the JSON key you entered
JAMF_PRO_WEBHOOK_SECRET=Bearer your-long-random-secret # the exact, whole JSON value
Local Development
Jamf Pro can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 jamf-pro --path /webhooks/jamf-pro
Use the printed URL as the webhook URL in Jamf Pro.
Resources
- references/overview.md - What Jamf Pro webhooks are, events and payload shape
- references/setup.md - Create the webhook and enable Header Authentication
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests