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:
- Read the raw request body as a string (do not re-serialize parsed JSON).
- Parse
X-Cliodot-Signaturefort(timestamp) andv1(hex digest). - Reject if the timestamp is too old (recommended tolerance: 5 minutes).
- Compute
HMAC-SHA256(signing_secret, "{t}.{raw_body}")as lowercase hex. - Compare the computed digest to
v1using 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 |