# 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)

```javascript
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 from `WORKSPACE_ONE_WEBHOOK_USERNAME` and
  `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.timingSafeEqual` on
  equal-length buffers or SHA-256 digests; Python: `hmac.compare_digest` on **bytes**, never
  `str`, 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 `401` challenge. 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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/workspace-one-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/workspace-one-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/workspace-one-webhooks/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

```bash
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):

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/workspace-one-webhooks/references/overview.md) - What Event Notifications are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/workspace-one-webhooks/references/setup.md) - Add an Event Notifications rule in the UEM console
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/workspace-one-webhooks/references/verification.md) - Basic auth check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/workspace-one-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests