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"
  }
}
FieldTypeDescription
typestringEvent type (see below)
createdAtstringISO 8601 timestamp of the event
dataobjectEvent-specific payload
data.userIdstringZitadel user ID
data.emailstringUser’s email address
data.displayNamestringUser’s display name
data.orgIdstringZitadel organization ID

Event types

EventWhen it fires
user.createdA new user signs up via SSO for the first time
user.updatedA user updates their profile (email, display name)
user.deletedA user is deleted from the identity provider
user.deactivatedA user account is deactivated (suspended)
user.reactivatedA deactivated user is reactivated
session.terminatedA 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

  1. In your Zitadel instance, go to Actions → Webhooks → + New.
  2. Set the URL to https://api.rivano.ai/api/auth/webhook.
  3. Select the event types you want to send (minimum: user.created, user.deleted).
  4. Set the signing secret to match ZITADEL_WEBHOOK_SECRET in your control plane environment.
  5. 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:

AttemptDelay
1Immediate
230 seconds
35 minutes
430 minutes
52 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.

  • Authentication — OAuth and OIDC sign-in flow
  • SSO Setup — Configure Zitadel and connect to Rivano
  • RBAC — Role assignment for provisioned users