Gareth Wilson Gareth Wilson

Guide to Cloudentity Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationExtensions > Webhooks > Create Webhook in the workspace, or the Webhook CRUD APIs
ScopeOne authorization server. Available events depend on its type
FormatJSON batch: {"audit_events": [ ... ]}
Event identifierssubject.action (for example user.deleted). The payload uses subject_action (user_deleted)
AuthenticationX-API-Key: <Webhook API Key>, generated by SecureAuth
Signature and timestampNone
Certificate checksOn by default. Developer mode disables them
HTTP methodNot stated
Retries and timeoutNot documented
Source IPs"SecureAuth public IPs", not listed

Common events

CategoryExample eventsTypically available on
User lifecycleuser.created, user.updated, user.deleted, user.authenticatedWorkspace, organization, admin, system
Groupsgroup.created, group.updated, group.deleted, plus group membership eventsWorkspace, organization, admin, system
Credentials and MFAcredential.created, credential.updated, otp.sent, otp.accepted, totp.reset_completed, webauthn.add_completedWorkspace, organization, admin, system
Passwordspassword.updated, password.requested, password.confirmedWorkspace, organization, admin, system
Tokens and authorizationaccess_token.issued, authorization_code.issued, scopes.granted, saml_assertion.issuedWorkspace, organization, admin, system
Sign-inlogin.attempted, login.accepted, login.failedWorkspace, organization, admin, system
Consentsconsent.created, consent.accepted, consent.rejected, consent.revokedWorkspace and organization, on Open Banking, CDR and FDX profiles
Dynamic client registrationdcr.created, dcr.rejectedWorkspace, organization
Organizationsorganization.created, organization.updated, organization.deletedWorkspace, organization, admin, system
Applications and configurationclient.created, schema.created, pool.created, service.created, server.created, role.grantedAdmin

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:

{
  "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 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.

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:

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 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 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 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 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.

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.

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 checks the key, stores every batch as it arrives, and delivers it to your service with retries and replay.

Get started with Hookdeck for free and handle Cloudentity webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.