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, ComputerCheckIn or MobileDeviceEnrolled events?
  • 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 (default Authorization). Look it up case-insensitively.
  • The expected value is the exact, whole header value, including any Bearer prefix you typed (e.g. {"Authorization": "Bearer <token>"}). Do not strip or parse the prefix.
  • Return 401 when the header is missing or doesn't match, 400 for a body that isn't a JSON object, 200 once handled.
  • Read the body raw (express.raw({ type: '*/*' })) so any content type works, and authenticate before parsing it.
  • Python: hmac.compare_digest on bytes (encode both sides; str raises 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.

webhookEventSent When
ComputerAddedA computer is enrolled into Jamf Pro
ComputerCheckInA managed computer checks in with the server
ComputerInventoryCompletedA computer finishes submitting inventory
ComputerPatchPolicyCompletedA computer patch policy completes
ComputerPolicyFinishedA policy finishes on a computer
ComputerPushCapabilityChangedA computer's push capability changes
DeviceAddedToDEPA device is added to Automated Device Enrollment
JSSStartup / JSSShutdownThe Jamf Pro server starts / shuts down
MobileDeviceEnrolledA mobile device is enrolled
MobileDeviceUnEnrolledA mobile device's MDM profile is removed or it is unmanaged
MobileDeviceCheckInA mobile device checks in
MobileDeviceCommandCompletedAn MDM command completes on a mobile device
MobileDeviceInventoryCompletedA mobile device finishes submitting inventory
MobileDevicePushSentA push notification is sent to a mobile device
PatchSoftwareTitleUpdatedA patch software title is updated
PushSentA push notification is sent
RestAPIOperationAn authorized Classic API request is made
SCEPChallengeA SCEP challenge is requested
SmartGroupComputerMembershipChangeA computer smart group's membership changes
SmartGroupMobileDeviceMembershipChangeA mobile device smart group's membership changes
SmartGroupUserMembershipChangeA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →