# Guide to Cloudentity Webhooks: Features and Best Practices

Cloudentity webhooks report what happens inside your identity platform: users created, updated or deleted, sign-ins accepted or failed, tokens issued, consents granted or revoked, and applications and servers created. Cloudentity is now part of SecureAuth and sold as SecureAuth Connect, so its documentation lives at docs.secureauth.com, but the webhooks are the same feature. Teams use them to deprovision users in downstream systems, forward audit events to a SIEM, and react to Open Banking consent changes without polling an API.

This guide covers how Cloudentity webhooks are created, what the batched audit-event payload looks like, how to authenticate requests with the webhook's API key, and what the documentation leaves out that a production receiver has to plan for.

## What are Cloudentity webhooks?

A webhook belongs to one workspace, or more precisely to one authorization server. You give it a URL and choose the **Subscribed events**, and when one of those events happens SecureAuth sends an HTTP request to the URL. The events on offer depend on the server type: workspace, organization, admin or system.

Requests carry **batches** of audit events. A single delivery is a JSON object with an `audit_events` array, so one request can describe several things that happened.

Cloudentity does not sign webhook requests. When you create a webhook, SecureAuth generates a **Webhook API Key** and sends it in the `X-API-Key` header of every request. Its documentation tells you to "validate this key and accept only requests with the correct value".

## Cloudentity webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Extensions** > **Webhooks** > **Create Webhook** in the workspace, or the Webhook CRUD APIs |
| Scope | One authorization server. Available events depend on its type |
| Format | JSON batch: `{"audit_events": [ ... ]}` |
| Event identifiers | `subject.action` (for example `user.deleted`). The payload uses `subject_action` (`user_deleted`) |
| Authentication | `X-API-Key: <Webhook API Key>`, generated by SecureAuth |
| Signature and timestamp | None |
| Certificate checks | On by default. **Developer mode** disables them |
| HTTP method | Not stated |
| Retries and timeout | Not documented |
| Source IPs | "SecureAuth public IPs", not listed |

## Common events

| Category | Example events | Typically available on |
| --- | --- | --- |
| User lifecycle | `user.created`, `user.updated`, `user.deleted`, `user.authenticated` | Workspace, organization, admin, system |
| Groups | `group.created`, `group.updated`, `group.deleted`, plus group membership events | Workspace, organization, admin, system |
| Credentials and MFA | `credential.created`, `credential.updated`, `otp.sent`, `otp.accepted`, `totp.reset_completed`, `webauthn.add_completed` | Workspace, organization, admin, system |
| Passwords | `password.updated`, `password.requested`, `password.confirmed` | Workspace, organization, admin, system |
| Tokens and authorization | `access_token.issued`, `authorization_code.issued`, `scopes.granted`, `saml_assertion.issued` | Workspace, organization, admin, system |
| Sign-in | `login.attempted`, `login.accepted`, `login.failed` | Workspace, organization, admin, system |
| Consents | `consent.created`, `consent.accepted`, `consent.rejected`, `consent.revoked` | Workspace and organization, on Open Banking, CDR and FDX profiles |
| Dynamic client registration | `dcr.created`, `dcr.rejected` | Workspace, organization |
| Organizations | `organization.created`, `organization.updated`, `organization.deleted` | Workspace, organization, admin, system |
| Applications and configuration | `client.created`, `schema.created`, `pool.created`, `service.created`, `server.created`, `role.granted` | Admin |

SecureAuth heads this table "Example events", and the group membership events are mentioned without names, so treat it as a starting point. The **Subscribed events** list in the admin UI is the full set for your server.

The documented batch looks like this, with the rest of each audit event elided on SecureAuth's page:

```json
{
  "audit_events": [
    {
      "tenant_id": "example",
      "server_id": "demo",
      "event_id": "a6ea34a8ef24cfdb74da4ff9f3cb693e",
      "...": "..."
    },
    {
      "tenant_id": "example",
      "server_id": "demo",
      "event_id": "474cf3eeaf2a4f34a8e2dbf9c69b7ddf",
      "...": "..."
    }
  ]
}

```

SecureAuth says the payload "delivers the matching field in subject_action form", and its Kibana example reads a failed sign-in's cause from `audit_events.payload.login_failed.cause`. That suggests each audit event has a `payload` object with one key named after the event, such as `login_failed`. No complete audit event is published, so capture a real delivery before you write routing code.

> **Inspect Cloudentity webhooks as they arrive.** Point a webhook at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to see the full batch and the `X-API-Key` header SecureAuth sends, with no account or setup required.

## Setting up Cloudentity webhooks

1. In the target workspace, go to **Extensions** > **Webhooks** in the left sidebar.
2. Click **Create Webhook**.
3. Enter the **URL**.
4. Choose the **Subscribed events**.
5. Click **Create**, and copy the API key when prompted. Store it in your secret manager straight away.

The webhook is active as soon as it is created. To test against an endpoint with a self-signed certificate, SecureAuth offers **Developer mode**, which disables certificate validation for its requests. Leave it off in production.

To route through Hookdeck, create a source with the **Cloudentity (SecureAuth)** type and use its URL in step 3. The source has one field, **Webhook API Key**: paste the key from step 5. Hookdeck compares it with the `X-API-Key` header of each request. The source accepts `POST`; SecureAuth's pages say only "an HTTP request", so confirm the method on your first delivery.

## Securing Cloudentity webhooks

Compare the `X-API-Key` header with the stored key, in constant time. The value has no prefix, so compare it whole.

```javascript
const crypto = require('crypto');
const express = require('express');

const API_KEY = process.env.CLOUDENTITY_WEBHOOK_API_KEY;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/cloudentity', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('x-api-key');
  if (!API_KEY || typeof received !== 'string' || !safeEqual(received, API_KEY)) {
    return res.sendStatus(401);
  }

  let batch;
  try {
    batch = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!Array.isArray(batch.audit_events)) return res.sendStatus(400);

  // Queue each audit event, keyed on its event_id, then respond.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

API_KEY = os.environ.get("CLOUDENTITY_WEBHOOK_API_KEY", "")

def verify_cloudentity_request(headers) -> bool:
    if not API_KEY:
        return False  # never accept when unconfigured
    received = headers.get("x-api-key")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), API_KEY.encode("utf-8"))

```

A static API key authenticates the sender, not the body. It cannot show that a batch was not modified, and with no signature or timestamp a captured request can be replayed and still pass. Serve the endpoint over HTTPS only, keep Developer mode off, and treat the key like a password: anyone who has it can post audit events to your endpoint.

> **Make Cloudentity webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `X-API-Key` header, accepts each batch straight away, and queues it durably so your audit pipeline can catch up after an outage.

## Cloudentity webhook limitations and pain points

### Delivery behaviour is undocumented

**The Problem:** You cannot tell from the documentation whether a failed request is retried, how long SecureAuth waits for a response, or how large a batch can get.

**Why It Happens:** SecureAuth's webhook and external-logging pages describe setup and the key check, and stop there.

**Workarounds:**

* Respond quickly and do the work afterwards, so a timeout is unlikely whatever the limit is.
* Reconcile important state, such as user deletions, against the SecureAuth APIs on a schedule rather than relying on every webhook arriving.

**How Hookdeck Can Help:** Hookdeck accepts each request and stores it, so your service's availability no longer decides whether a batch survives. It then delivers to you with [retries](/docs/retries) on your schedule, and you can replay anything after a fix.

### One request, many events

**The Problem:** A handler that processes a batch in one transaction fails on one bad event and rejects the rest.

**Why It Happens:** Every delivery is an `audit_events` array, and a busy workspace puts several events in one request.

**Workarounds:**

* Split the batch on arrival and queue each audit event separately.
* Deduplicate on `event_id`, so reprocessing a batch does not repeat work.

**How Hookdeck Can Help:** A [transformation](/docs/transformations) can reshape the batch before delivery, and Hookdeck keeps the original request so you can replay it after a fix.

### The event type is a key, not a value

**The Problem:** Routing code looks for a `type` field and finds nothing.

**Why It Happens:** SecureAuth names events `subject.action` in the UI and docs but says the payload carries a `subject_action` field. Its Kibana example points to a key inside each event's `payload` object rather than a value.

**Workarounds:**

* Capture a delivery for each event you care about before writing the router.
* Route on the presence of the `subject_action` key, and log events whose keys you do not recognise.

**How Hookdeck Can Help:** Every request is stored with its full body in Hookdeck, so you can inspect real batches and set [filters](/docs/filters) on the fields you find.

### A key that is easy to lose

**The Problem:** You lose the API key, or need to rotate it, and the documentation does not say how.

**Why It Happens:** SecureAuth generates the key and asks you to copy it when you create the webhook. Each webhook has its own key.

**Workarounds:**

* Copy the key into a secret manager at creation time.
* To change it, create a new webhook with the same URL and events, update your receiver, then delete the old webhook.

**How Hookdeck Can Help:** Hookdeck's Cloudentity (SecureAuth) source compares `X-API-Key` with the key you paste. Hookdeck answers SecureAuth with a `200` either way, but a request with a missing or wrong key is recorded as failed verification and never reaches your service.

## Best practices

### Check the key before parsing

Read the raw body, compare `X-API-Key` in constant time, and only then parse the JSON.

### Process audit events one at a time

Split `audit_events` into individual jobs and deduplicate on `event_id`. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Subscribe per server

Events differ by server type, and `user.deleted` is available at workspace and organization level as well as admin and system. Create the webhook on the server where the events happen.

### Keep certificate validation on

Use Developer mode only against test endpoints, and serve production endpoints with a valid certificate.

### Acknowledge fast and process asynchronously

With no documented timeout or retry policy, respond as soon as the batch is stored. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Cloudentity, now SecureAuth Connect, sends batches of audit events with a SecureAuth-generated key in the `X-API-Key` header and no signature. Store the key when you create the webhook, compare it in constant time, split each batch into individual events, and confirm the payload shape and HTTP method on a real delivery, because SecureAuth documents neither in full.

[Hookdeck Event Gateway](https://hookdeck.com) checks the key, stores every batch as it arrives, and delivers it to your service with retries and replay.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Cloudentity webhooks reliably in minutes.