OAuth webhooks

Verify signed OAuth lifecycle deliveries and process connection events safely.

PrerequisitesOAuth connections

Cliodot can deliver OAuth lifecycle events to an HTTPS endpoint you configure in the Cliodot portal. Webhook URLs, event subscriptions, and signing secrets are managed in the portal — there is no public API for webhook configuration.

When an subscribed event occurs, Cliodot POSTs a JSON payload to your endpoint with the headers below.

Delivery headers#

Header Description
Content-Type application/json
User-Agent Cliodot-OAuth-Webhook/1.0
X-Cliodot-Event-Delivery Unix timestamp (seconds) of the delivery attempt
X-Cliodot-Signature t=<unix>,v1=<hmac_hex>

Signature verification#

Verify every delivery before processing:

  1. Read the raw request body as a string (do not re-serialize parsed JSON).
  2. Parse X-Cliodot-Signature for t (timestamp) and v1 (hex digest).
  3. Reject if the timestamp is too old (recommended tolerance: 5 minutes).
  4. Compute HMAC-SHA256(signing_secret, "{t}.{raw_body}") as lowercase hex.
  5. Compare the computed digest to v1 using a constant-time comparison.

The signing_secret (YOUR_WEBHOOK_SIGNING_SECRET) is shown once when you create or rotate the webhook in the Cliodot portal.

Node.js example:

const crypto = require("crypto");
function verifyWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => part.trim().split("="))
  );
  const timestamp = Number(parts.t);
  const expected = parts.v1;
  if (!timestamp || !expected) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) return false;
  const signedPayload = `${timestamp}.${rawBody}`;
  const digest = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(expected));
}

Payload shape#

{
  "id": "oauth_audit_507f1f77bcf86cd799439015",
  "type": "oauth.connect.completed",
  "created_at": "2026-07-10T09:00:00.000Z",
  "oauth_app_id": "oauth_app_507f1f77bcf86cd799439011",
  "connection_id": "conn_507f1f77bcf86cd799439014",
  "actor": {
    "type": "end_user",
    "id": "108123456789012345678",
    "email": "user@example.com",
    "external_user_key": "user_12345"
  },
  "metadata": {
    "provider": "google"
  },
  "connection": {
    "id": "conn_507f1f77bcf86cd799439014",
    "provider": "google",
    "status": "active",
    "identity": {
      "email": "user@example.com"
    },
    "scopes_granted": ["openid", "email", "profile"]
  },
  "connection_details": {
    "access_token": "PROVIDER_ACCESS_TOKEN",
    "token_type": "Bearer",
    "expires_at": "2026-07-10T10:00:00.000Z"
  }
}
Field Description
id Unique audit log ID for this event
type Event type (see Event types)
created_at ISO 8601 timestamp
oauth_app_id OAuth App ID
connection_id Connection ID (when applicable)
actor Who triggered the event (user, system, customer_backend, end_user)
metadata Event-specific context (provider, error details, etc.)
connection Sanitized connection snapshot (included for connection-related events)
connection_details Token and exchange details (included for oauth.connect.completed and oauth.token.exchanged when token exposure policy allows)

Respond with 2xx to acknowledge receipt. Failed deliveries are retried with backoff.

Event types#

Subscribe to any subset of these event types in the Cliodot portal:

Event type Label Description
oauth.app.created App created OAuth app was created in the portal
oauth.app.updated App updated OAuth app settings were updated
oauth.app.disabled App disabled OAuth app was disabled or deleted
oauth.app.deleted App deleted OAuth app was permanently removed
oauth.credentials.rotated Credentials rotated App secret and API key were regenerated
oauth.provider.attached Provider attached An OAuth provider was attached to the app
oauth.provider.updated Provider updated An attached provider configuration was updated
oauth.provider.detached Provider detached A provider was removed from the app
oauth.connect.started Connect started An end-user OAuth connect flow was started
oauth.connect.completed Connect completed An end-user OAuth connect flow completed successfully
oauth.connect.failed Connect failed An end-user OAuth connect flow failed
oauth.token.exchanged Token exchanged A customer backend exchanged a connect code for connection metadata
oauth.token.refreshed Token refreshed A stored connection token was refreshed successfully
oauth.token.refresh_failed Token refresh failed A connection token refresh attempt failed
oauth.connection.revoked Connection revoked A connection was revoked
oauth.connection.deleted Connection deleted A connection was deleted