Agent skill
Workspace ONE Webhooks Skill
Receive and authenticate Workspace ONE UEM (Omnissa, formerly VMware AirWatch) event notification webhooks. Use when setting up a Workspace ONE webhook endpoint, verifying the HTTP Basic credentials, debugging 401s, or handling device events like Device Enrollment, Device Compliance Status Change, Device Wipe and Device Delete.
Install this skill
npx skills add hookdeck/webhook-skills --skill workspace-one-webhooks
Workspace ONE UEM (Omnissa, formerly VMware) can call your endpoint when something happens to a managed device: it enrolls, changes compliance status, is wiped, is deleted. These are the Event Notifications in System Settings. Each rule has a Target URL, a User Name, a Password and a Format (JSON or XML). Notifications are not signed: Workspace ONE authenticates itself with those credentials using HTTP Basic auth, and your endpoint checks them.
When to Use This Skill
- How do I receive Workspace ONE UEM event notifications?
- How do I verify the Basic auth credentials on a Workspace ONE webhook?
- Does Workspace ONE sign webhook payloads? (No: HTTP Basic auth with a shared user name and password)
- How do I handle Device Enrollment, Device Compliance Status Change or Device Wipe events?
- Why does my Workspace ONE webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
const safeEqual = (a, b) => crypto.timingSafeEqual(
crypto.createHash('sha256').update(String(a)).digest(),
crypto.createHash('sha256').update(String(b)).digest());
function verifyWorkspaceOneRequest(headers, user, pass) {
if (!user || !pass) return false; // never accept when unconfigured
const m = /^basic\s+([A-Za-z0-9+/]+={0,2})$/i.exec((headers.authorization || '').trim());
if (!m) return false;
const decoded = Buffer.from(m[1], 'base64').toString('utf8');
const i = decoded.indexOf(':'); // split on the FIRST colon
if (i === -1) return false;
const userOk = safeEqual(decoded.slice(0, i), user); // evaluate both,
const passOk = safeEqual(decoded.slice(i + 1), pass); // no short-circuit
return userOk && passOk;
}
- Header:
Authorization: Basic base64(username:password), the User Name and Password you entered on the Event Notifications rule. Read them fromWORKSPACE_andONE_ WEBHOOK_ USERNAME WORKSPACE_.ONE_ WEBHOOK_ PASSWORD - The scheme is case-insensitive. Split the decoded value on the first colon only, since passwords can contain
:. - Compare username and password each in constant time (Node:
crypto.timingSafeEqualon equal-length buffers or SHA-256 digests; Python:hmac.compare_digeston bytes, neverstr, which raises on non-ASCII). - Missing or malformed header, or a mismatch:
401. Stored:200. - The Omnissa page doesn't show the header on a request, or say whether Basic credentials are sent on the first request or only after a
401challenge. Log the headers of a test notification to see what arrives. - Read the body raw (
express.raw({ type: '*/*' })) so any content type works, and authenticate before reading it.
A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Anyone who captures a request can resend it. Use HTTPS only, make handlers idempotent, and rotate the credential if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The Omnissa page lists these notification events by their UI labels (Groups & Settings > All Settings > System > Advanced > API > Event Notifications):
| Event | Sent When |
|---|---|
Check-in/Check-out | A multi-user device is checked in or out |
Device Attribute Change | A selected device attribute changes (asset number, friendly name, MCC, ownership, organization group ID, operating system, phone number, user email address) |
Device Compliance Status Change | A device's compliance status changes |
Device Compromised Status Change | A device's compromised status changes: an Android device is rooted or an iOS device is jailbroken |
Device Delete | A device is deleted from Workspace ONE UEM |
Device Enrollment | A device enrolls into Workspace ONE Intelligent Hub or Workspace ONE UEM |
Device Unenrolled Enterprise Wipe | A device is enterprise wiped |
Device Wipe | A device is wiped |
The page documents no payload and no wire-level event strings, so these labels are not values to switch on. The examples store the raw body and don't read any field. Capture real deliveries for each event you select (for example with the Hookdeck CLI), then write your parser from what your tenant sends. A rule per event type, each with its own Target URL path, tells your handler which event it selected without reading the body.
Environment Variables
WORKSPACE_ONE_WEBHOOK_USERNAME=your-notification-user
WORKSPACE_ONE_WEBHOOK_PASSWORD=your-notification-password
Both are the credentials you set on the Event Notifications rule. Rotate them in the console and in your environment together.
Local Development
Workspace ONE can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 workspace-one --path /webhooks/workspace-one
Use the printed URL as the Target URL, with the same User Name and Password configured on the Hookdeck source.
Resources
- references/overview.md - What Event Notifications are, events and payload shape
- references/setup.md - Add an Event Notifications rule in the UEM console
- references/verification.md - Basic auth check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests