# Cloudentity (SecureAuth) Webhooks

Cloudentity is now SecureAuth (cloudentity.com redirects to secureauth.com); this skill keeps the
`cloudentity` name. SecureAuth Connect workspace webhooks send **batches of audit events** as JSON
(`{ "audit_events": [ ... ] }`) when subscribed events happen, such as `user.created` or
`user.authenticated`. Webhooks are **not signed**. Each request carries an `X-API-Key` header with
the Webhook API Key, generated by SecureAuth and shown once when you create the webhook.

> **A static shared credential authenticates the sender, not the body, and gives no replay
> protection** (no signature, no timestamp). Use HTTPS, and rotate the key if it leaks.

## When to Use This Skill

* How do I receive Cloudentity or SecureAuth webhooks?
* How do I verify the `X-API-Key` header on a SecureAuth Connect webhook?
* Does Cloudentity sign webhook payloads? (No: use the Webhook API Key)
* How do I handle `user.created`, `user.deleted` or `login.failed` audit events?
* Why does my Cloudentity webhook endpoint return 401?

## Verification (core)

```javascript
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 verifyCloudentityRequest(headers, apiKey = process.env.CLOUDENTITY_WEBHOOK_API_KEY) {
  if (!apiKey) return false;                // never accept when unconfigured
  const received = headers['x-api-key'];    // whole value, plain text
  return Boolean(received) && safeEqual(received, apiKey);
}

```

* Header: `X-API-Key: <Webhook API Key>` (case-insensitive name, whole value compared).
* Python: `hmac.compare_digest(received.encode(), key.encode())` on bytes, never on `str`.
* Return `401` on a missing or wrong key, `400` for a body without an `audit_events` array,
  `200` once handled.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cloudentity-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cloudentity-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cloudentity-webhooks/examples/fastapi/).

## Event Types

Event identifiers use a `subject.action` form (`user.deleted`); the docs say the payload delivers
the matching field in `subject_action` form (`user_deleted`). **The docs don't name the field that
carries the event type.** The external-logging guide reads `audit_events.payload.login_failed.cause`,
so the examples infer that each audit event's `payload` object holds one key named for the event
and dispatch on that key. This is an inference, not documented: confirm it against a real delivery.
Available events depend on the server type.

| Event | Sent When |
| --- | --- |
| `user.created` | A user is created |
| `user.updated` | A user is updated |
| `user.deleted` | A user is deleted (also at workspace and organization level) |
| `user.authenticated` | A user authenticates |
| `group.created` | A group is created |
| `login.accepted` / `login.failed` | A sign-in attempt succeeds / fails |
| `consent.created` / `consent.revoked` | An Open Banking, CDR or FDX consent is created / revoked |
| `client.created` | An application (client) is created |

Other documented events include `access_token.issued`, `password.updated`, `otp.sent` and
`organization.created`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cloudentity-webhooks/references/overview.md).

## Environment Variables

```bash
CLOUDENTITY_WEBHOOK_API_KEY=your-webhook-api-key   # shown once when the webhook is created

```

## Local Development

SecureAuth can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 cloudentity --path /webhooks/cloudentity

```

Use the printed URL as the webhook URL in SecureAuth.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cloudentity-webhooks/references/overview.md) - What the webhooks are, events and batch payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cloudentity-webhooks/references/setup.md) - Create the webhook and copy the API key
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/cloudentity-webhooks/references/verification.md) - Key check details, limits, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/cloudentity-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests