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
/api/event-subscriptionsRequires: admin:allList all event subscriptions for the tenant. The signing secret is never returned in list responses.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Maximum items to return (max 100) |
skip | integer | 0 | Number of items to skip (offset) |
eventType | string | — | Filter by event type |
enabled | boolean | — | 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
/api/event-subscriptionsRequires: admin:allRegister 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
}| Field | Type | Required | Description |
|---|---|---|---|
eventType | string | Yes | The single event type to subscribe to (see Event Types) |
webhookUrl | string | Yes | HTTPS endpoint URL that will receive POST requests |
maxRetries | integer | No | Number of retries on failure, 0–10 (default: 3) |
retryDelayMs | integer | No | Delay 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
| Code | HTTP | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Missing required fields, invalid event type, or out-of-range retry values |
INVALID_URL | 400 | URL is not a valid HTTPS endpoint or targets a private/internal address |
SUBSCRIPTION_EXISTS | 409 | A 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
/api/event-subscriptions/[id]Requires: admin:allUpdate 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
}| Field | Type | Description |
|---|---|---|
enabled | boolean | Enable or disable the subscription |
maxRetries | integer | Retry count on failure, 0–10 |
retryDelayMs | integer | Delay 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
/api/event-subscriptions/[id]Requires: admin:allDelete an event subscription. Once deleted, no further deliveries will be attempted.
Success response
{
"success": true
}Secret Rotation
/api/event-subscriptions/[id]/rotate-secretRequires: admin:allGenerate 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
| Event | Description |
|---|---|
user.created | New user account created |
user.updated | User profile fields were modified |
user.deleted | User account was deleted |
user.login | User successfully authenticated (any method) |
user.logout | User logged out (session invalidated) |
user.password_changed | User changed their password |
Role & Permission Events
| Event | Description |
|---|---|
role.created | New role created |
role.updated | Role metadata or permissions modified |
role.deleted | Role deleted |
role.assigned | Role assigned to a user |
role.removed | Role removed from a user |
permission.changed | A permission entry was modified |
Session Events
| Event | Description |
|---|---|
session.revoked | A session was explicitly revoked |
MFA Events
| Event | Description |
|---|---|
mfa.enabled | User enabled a 2FA method |
mfa.disabled | User disabled a 2FA method |
Organization Events
| Event | Description |
|---|---|
organization.created | New organization created |
organization.updated | Organization metadata modified |
organization.deleted | Organization was deleted |
Application Events
| Event | Description |
|---|---|
application.created | New application registered |
application.updated | Application record modified |
application.deleted | Application 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"
}
}| Field | Type | Description |
|---|---|---|
eventType | string | The event type (e.g., user.created) |
timestamp | string | ISO 8601 timestamp of when the event occurred |
tenantId | string | UUID of the tenant where the event occurred (not the realm slug) |
data | object | Event-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:
| Header | Description |
|---|---|
X-Auris-Signature | HMAC-SHA256 signature of the request body, prefixed with sha256= |
X-Auris-Timestamp | Unix 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
- Read the raw request body as a UTF-8 string (do not parse JSON first).
- Read the
X-Auris-Timestampheader. - Concatenate:
timestamp + "." + body - Compute HMAC-SHA256 of the concatenated string using your subscription’s
signingSecret. - Strip the
sha256=prefix fromX-Auris-Signature, then compare the hex-encoded result with a timing-safe comparison. - 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
- Respond quickly: Return a
2xxstatus code as soon as possible. Process the event asynchronously (e.g., queue it) rather than doing heavy work in the request handler. - Verify signatures: Always verify the HMAC-SHA256 signature before processing the payload. Never trust a webhook payload without verification.
- Use HTTPS: Auris rejects non-HTTPS webhook URLs. Use a valid TLS certificate.
- Handle retries: Design your handler to be idempotent. The same event may be delivered more than once.
- Rotate secrets periodically: Use the rotate-secret endpoint to generate a new signing secret on a regular schedule (e.g., quarterly).
- 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.
Related
- Setting Up Webhooks — Step-by-step guide to creating webhook receivers
- Managing Webhooks — Configure webhooks from the Console
- Actions Engine API — Custom logic hooks that complement webhooks
- JavaScript SDK —
verifyWebhookSignature()for signature verification