Webhooks API
Rivano delivers webhooks for identity and provisioning events via the /api/auth/webhook endpoint. Webhooks are sent by Zitadel (your OIDC provider) when users sign up, update their profiles, or are deleted. All payloads are signed with HMAC-SHA256.
Endpoint
POST /api/auth/webhook
This endpoint is called by Zitadel, not by your application. You configure it in your Zitadel instance as the webhook target. Rivano validates the signature and processes the event.
Payload format
{
"type": "user.created",
"createdAt": "2026-04-04T10:00:00Z",
"data": {
"userId": "zitadel_user_id_abc123",
"email": "alice@example.com",
"displayName": "Alice Smith",
"orgId": "zitadel_org_abc123"
}
}
| Field | Type | Description |
|---|---|---|
type | string | Event type (see below) |
createdAt | string | ISO 8601 timestamp of the event |
data | object | Event-specific payload |
data.userId | string | Zitadel user ID |
data.email | string | User’s email address |
data.displayName | string | User’s display name |
data.orgId | string | Zitadel organization ID |
Event types
| Event | When it fires |
|---|---|
user.created | A new user signs up via SSO for the first time |
user.updated | A user updates their profile (email, display name) |
user.deleted | A user is deleted from the identity provider |
user.deactivated | A user account is deactivated (suspended) |
user.reactivated | A deactivated user is reactivated |
session.terminated | A user session is explicitly terminated by an admin |
Rivano uses user.created to provision the user record and assign the default role. user.deleted and user.deactivated revoke access.
HMAC-SHA256 verification
Every webhook request includes a signature in the X-Zitadel-Signature header. Rivano verifies this signature using the ZITADEL_WEBHOOK_SECRET configured in the control plane.
If you are building a custom integration that calls /api/auth/webhook, you must sign the payload using the same method:
import { createHmac } from 'node:crypto';
function signWebhookPayload(
body: string,
secret: string,
timestamp: string
): string {
const message = `${timestamp}.${body}`;
return createHmac('sha256', secret)
.update(message)
.digest('hex');
}
// Build the request
const body = JSON.stringify({ type: 'user.created', ... });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = signWebhookPayload(body, process.env.ZITADEL_WEBHOOK_SECRET!, timestamp);
await fetch('https://api.rivano.ai/api/auth/webhook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Zitadel-Signature': `t=${timestamp},v1=${signature}`,
},
body,
});
Verification logic
Rivano extracts the timestamp and signature from the header, recomputes the HMAC, and compares using a constant-time comparison. Requests with a timestamp older than 5 minutes are rejected to prevent replay attacks.
Response on invalid signature: 403 Forbidden with { "error": "Invalid webhook signature" }.
Configuring the webhook in Zitadel
- In your Zitadel instance, go to Actions → Webhooks → + New.
- Set the URL to
https://api.rivano.ai/api/auth/webhook. - Select the event types you want to send (minimum:
user.created,user.deleted). - Set the signing secret to match
ZITADEL_WEBHOOK_SECRETin your control plane environment. - Save and test with the Send test event button.
After enabling SSO, test the webhook by creating a test user in Zitadel and verifying they appear in Settings → Team in the Rivano dashboard within a few seconds.
Retry behavior
Zitadel retries failed webhook deliveries (non-2xx responses or timeouts) with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, Zitadel stops retrying. Check your Zitadel webhook delivery log for failures.
Rivano’s webhook endpoint is idempotent — replaying the same user.created event does not create duplicate users.
Related
- Authentication — OAuth and OIDC sign-in flow
- SSO Setup — Configure Zitadel and connect to Rivano
- RBAC — Role assignment for provisioned users