Skip to Content

Webhooks API

Webhooks allow your application to receive real-time HTTP callbacks when events occur in Auris. When a subscribed event fires (e.g., a user logs in, a role is assigned), Auris sends a POST request to your configured URL with a JSON payload describing the event.

Every webhook delivery is signed with HMAC-SHA256 using a per-subscription signing secret. This allows your server to verify that the payload originated from Auris and was not tampered with in transit.

All webhook management endpoints require the admin:all permission and the x-tenant header.

The data model is one EventSubscription per (tenantId, eventType, webhookUrl) triplet — not a named endpoint that subscribes to multiple events. To receive five event types at the same URL, create five separate subscriptions.

Webhook CRUD

List Subscriptions

GET/api/event-subscriptionsRequires: admin:all

List all event subscriptions for the tenant. The signing secret is never returned in list responses.

Query parameters

ParameterTypeDefaultDescription
limitinteger50Maximum items to return (max 100)
skipinteger0Number of items to skip (offset)
eventTypestring—Filter by event type
enabledboolean—Filter by enabled state (true or false)

Success response

{ "subscriptions": [ { "id": "clxyz1234", "tenantId": "ten_abc", "eventType": "user.created", "webhookUrl": "https://api.yourapp.com/webhooks/auris", "maxRetries": 3, "retryDelayMs": 5000, "enabled": true, "signingSecret": null, "createdAt": "2025-01-15T10:00:00Z", "updatedAt": "2025-01-15T10:00:00Z" } ], "pagination": { "total": 5, "limit": 50, "skip": 0, "remaining": 0 } }

The signingSecret field is included in list responses as-is from the database (it is stored hashed or null depending on your schema). Copy it at creation time instead.

Create Subscription

POST/api/event-subscriptionsRequires: admin:all

Register a new event subscription. A signing secret is automatically generated with a whsec_ prefix. The secret is returned inline in the creation response — store it securely, as rotating it is the only way to get a new one.

Request body

{ "eventType": "user.created", "webhookUrl": "https://api.yourapp.com/webhooks/auris", "maxRetries": 3, "retryDelayMs": 5000 }
FieldTypeRequiredDescription
eventTypestringYesThe single event type to subscribe to (see Event Types)
webhookUrlstringYesHTTPS endpoint URL that will receive POST requests
maxRetriesintegerNoNumber of retries on failure, 0–10 (default: 3)
retryDelayMsintegerNoDelay between retries in ms, 1000–60000 (default: 5000)

Webhook URLs must use HTTPS. HTTP URLs are rejected to prevent secrets and event data from being transmitted in plaintext. Internal/private IP ranges are also blocked (SSRF protection).

Success response

The raw EventSubscription row is returned directly:

{ "id": "clxyz5678", "tenantId": "ten_abc", "eventType": "user.created", "webhookUrl": "https://api.yourapp.com/webhooks/auris", "maxRetries": 3, "retryDelayMs": 5000, "enabled": true, "signingSecret": "whsec_7f3a8b2c4d5e6f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4", "createdAt": "2025-02-18T10:00:00Z", "updatedAt": "2025-02-18T10:00:00Z" }

Copy the signingSecret immediately. It is the only time this value is easily accessible without re-reading the database row directly. Use the rotate-secret endpoint to replace it if needed.

Error codes

CodeHTTPDescription
VALIDATION_ERROR400Missing required fields, invalid event type, or out-of-range retry values
INVALID_URL400URL is not a valid HTTPS endpoint or targets a private/internal address
SUBSCRIPTION_EXISTS409A subscription for this eventType + webhookUrl pair already exists for the tenant

Get Subscription

Not implemented: GET /api/event-subscriptions/[id] does not exist. The [id]/route.ts file only exports PATCH and DELETE. Use the list endpoint with the eventType filter to locate a specific subscription.

Update Subscription

PATCH/api/event-subscriptions/[id]Requires: admin:all

Update a subscription’s enabled state or retry settings. All fields are optional — only provided fields are updated. The eventType and webhookUrl of a subscription cannot be changed; delete and recreate instead.

Request body

{ "enabled": false, "maxRetries": 5, "retryDelayMs": 10000 }
FieldTypeDescription
enabledbooleanEnable or disable the subscription
maxRetriesintegerRetry count on failure, 0–10
retryDelayMsintegerDelay between retries in ms, 1000–60000

Success response

The raw updated EventSubscription row is returned directly:

{ "id": "clxyz5678", "tenantId": "ten_abc", "eventType": "user.created", "webhookUrl": "https://api.yourapp.com/webhooks/auris", "maxRetries": 5, "retryDelayMs": 10000, "enabled": false, "signingSecret": "whsec_...", "createdAt": "2025-02-18T10:00:00Z", "updatedAt": "2025-02-18T11:00:00Z" }

Delete Subscription

DELETE/api/event-subscriptions/[id]Requires: admin:all

Delete an event subscription. Once deleted, no further deliveries will be attempted.

Success response

{ "success": true }

Secret Rotation

POST/api/event-subscriptions/[id]/rotate-secretRequires: admin:all

Generate a new signing secret for the subscription. The old secret is immediately invalidated. All subsequent deliveries will be signed with the new secret.

Request: No body required.

Success response

{ "signingSecret": "whsec_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a" }

After rotating a secret, update your webhook handler to use the new signingSecret value immediately. Best practice: support verifying against both old and new secrets for a short grace period during rotation.

Testing

Not yet implemented: the /[id]/test endpoint is not available. Use a real subscribed event to verify your endpoint is reachable and correctly verifying signatures.

Delivery Log

Not yet implemented: the /[id]/deliveries endpoint and delivery retry are not yet available.

Event Types

The following event types are accepted by POST /api/event-subscriptions. Subscribing to any other string returns a 400 error.

User Events

EventDescription
user.createdNew user account created
user.updatedUser profile fields were modified
user.deletedUser account was deleted
user.loginUser successfully authenticated (any method)
user.logoutUser logged out (session invalidated)
user.password_changedUser changed their password

Role & Permission Events

EventDescription
role.createdNew role created
role.updatedRole metadata or permissions modified
role.deletedRole deleted
role.assignedRole assigned to a user
role.removedRole removed from a user
permission.changedA permission entry was modified

Session Events

EventDescription
session.revokedA session was explicitly revoked

MFA Events

EventDescription
mfa.enabledUser enabled a 2FA method
mfa.disabledUser disabled a 2FA method

Organization Events

EventDescription
organization.createdNew organization created
organization.updatedOrganization metadata modified
organization.deletedOrganization was deleted

Application Events

EventDescription
application.createdNew application registered
application.updatedApplication record modified
application.deletedApplication was deleted

Webhook Payload Format

Every webhook delivery sends a POST request with a JSON body:

{ "eventType": "user.created", "timestamp": "2025-02-18T10:00:00Z", "tenantId": "b3f1c2d4-5e6a-7b8c-9d0e-1f2a3b4c5d6e", "data": { "userId": "usr_abc123", "email": "[email protected]", "firstName": "Alice", "lastName": "Smith" } }
FieldTypeDescription
eventTypestringThe event type (e.g., user.created)
timestampstringISO 8601 timestamp of when the event occurred
tenantIdstringUUID of the tenant where the event occurred (not the realm slug)
dataobjectEvent-specific payload data

The shape of data varies by event type. User events include user fields, role events include role details, and login events include the email and IP address.

Signature Verification

Every delivery includes two headers for signature verification:

HeaderDescription
X-Auris-SignatureHMAC-SHA256 signature of the request body, prefixed with sha256=
X-Auris-TimestampUnix timestamp (seconds) of when the signature was generated

The signature header value is sha256=<hex>, not a bare hex string. Strip the sha256= prefix before decoding or comparing, otherwise verification fails for every delivery.

Verification Algorithm

  1. Read the raw request body as a UTF-8 string (do not parse JSON first).
  2. Read the X-Auris-Timestamp header.
  3. Concatenate: timestamp + "." + body
  4. Compute HMAC-SHA256 of the concatenated string using your subscription’s signingSecret.
  5. Strip the sha256= prefix from X-Auris-Signature, then compare the hex-encoded result with a timing-safe comparison.
  6. Optionally, reject deliveries where the timestamp is more than 5 minutes old (to prevent replay attacks).

Deliveries to Discord (discord.com, discordapp.com) and Slack (hooks.slack.com) URLs are rewritten into that platform’s own message format and are sent without signature or timestamp headers. Signature verification applies only to generic webhook endpoints.

Node.js Verification Example

import crypto from 'crypto'; function verifyWebhookSignature(rawBody, signature, timestamp, secret) { // 1. Check timestamp freshness (optional but recommended) const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - parseInt(timestamp)) > 300) { throw new Error('Webhook timestamp is too old'); } // 2. Compute expected signature const signedPayload = `${timestamp}.${rawBody}`; const expectedSignature = crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex'); // 3. Timing-safe comparison — the header is `sha256=<hex>`, strip the prefix const receivedHex = signature.startsWith('sha256=') ? signature.slice(7) : signature; const expected = Buffer.from(expectedSignature, 'hex'); const received = Buffer.from(receivedHex, 'hex'); if (expected.length !== received.length) { return false; } return crypto.timingSafeEqual(expected, received); } // Express.js handler app.post('/webhooks/auris', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-auris-signature']; const timestamp = req.headers['x-auris-timestamp']; const rawBody = req.body.toString('utf-8'); if (!verifyWebhookSignature(rawBody, signature, timestamp, process.env.AURIS_WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } const event = JSON.parse(rawBody); console.log(`Received ${event.eventType}:`, event.data); // Process the event... res.status(200).json({ received: true }); });

Python Verification Example

import hmac import hashlib import time def verify_webhook_signature(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool: # Check timestamp freshness current_time = int(time.time()) if abs(current_time - int(timestamp)) > 300: return False # Compute expected signature signed_payload = f"{timestamp}.{raw_body.decode('utf-8')}" expected = hmac.new( secret.encode('utf-8'), signed_payload.encode('utf-8'), hashlib.sha256 ).hexdigest() # Timing-safe comparison — the header is "sha256=<hex>", strip the prefix received = signature[7:] if signature.startswith("sha256=") else signature return hmac.compare_digest(expected, received)

The @auris/js SDK includes a built-in webhook verification utility: import { verifyWebhookSignature } from '@auris/js'. It handles timestamp validation, HMAC computation, and timing-safe comparison. See the SDK documentation for details.

Delivery Behavior

The retry policy and automatic-disabling behavior described below are the intended design based on the maxRetries and retryDelayMs fields stored on each subscription. They are not yet fully implemented in the current API layer — a background delivery worker is required to enforce them. Treat this section as aspirational until confirmed otherwise.

Timeouts

Auris intends to wait up to 30 seconds for a response from your webhook endpoint. If your endpoint does not respond within this window, the delivery is marked as failed.

Retry Policy

Failed deliveries are intended to be retried according to the maxRetries and retryDelayMs values configured on each subscription (defaults: 3 retries, 5000 ms delay).

Automatic Disabling

The enabled flag on a subscription can be set to false via PATCH /api/event-subscriptions/[id] with { "enabled": false }. Automatic disabling after consecutive failures is not yet implemented.

Idempotency

Your webhook handler should be idempotent. In rare cases (network issues, retries), the same event may be delivered more than once. Use the timestamp and event fields to deduplicate if needed.

Best Practices

  1. Respond quickly: Return a 2xx status code as soon as possible. Process the event asynchronously (e.g., queue it) rather than doing heavy work in the request handler.
  2. Verify signatures: Always verify the HMAC-SHA256 signature before processing the payload. Never trust a webhook payload without verification.
  3. Use HTTPS: Auris rejects non-HTTPS webhook URLs. Use a valid TLS certificate.
  4. Handle retries: Design your handler to be idempotent. The same event may be delivered more than once.
  5. Rotate secrets periodically: Use the rotate-secret endpoint to generate a new signing secret on a regular schedule (e.g., quarterly).
  6. One subscription per event: The model stores one row per event type. To fan out a single event to multiple URLs, create one subscription per URL.