Skip to main content

Webhooks

Ithbat IAM sends HTTP POST requests to your configured endpoints when events occur within your tenant. Use webhooks to keep downstream systems synchronized — sync users to your CRM when they sign up, alert your security team when a lockout occurs, or update your own audit database in real time.


How it works

When a subscribed event fires, Ithbat:

  1. Serializes the event payload as JSON.
  2. Signs the payload with HMAC-SHA256 using your webhook's secret key.
  3. Sends an HTTP POST to your endpoint with a 30-second timeout.
  4. Records the delivery attempt (status code and response body) in the delivery log.

Delivery is asynchronous — the action that triggered the event completes independently of webhook delivery. Your endpoint must respond with an HTTP 2xx status code to be considered successful.


Creating a webhook

Via admin console

  1. Go to Settings → Webhooks → Add Webhook.
  2. Enter a Name and the Endpoint URL (must be https:// in production).
  3. Select the events to subscribe to.
  4. Click Save. Ithbat generates a signing secret (whsec_...) — copy it now.

Via API

POST /api/v1/webhooks
Authorization: Bearer {token}
X-Tenant-ID: {tenant_id}
Content-Type: application/json

{
"name": "Production Webhook",
"url": "https://your-app.example.com/webhooks/ithbat",
"events": [
"user.created",
"user.deleted",
"auth.login",
"auth.failed",
"security.lockout",
"role.assigned"
]
}

Response:

{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"tenantId": "c2e3f4a5-...",
"name": "Production Webhook",
"url": "https://your-app.example.com/webhooks/ithbat",
"secret": "whsec_1a2b3c4d5e6f...",
"events": ["user.created", "user.deleted", "auth.login", "auth.failed", "security.lockout", "role.assigned"],
"isActive": true,
"createdAt": "2026-02-24T10:00:00Z",
"updatedAt": "2026-02-24T10:00:00Z"
}
warning

The secret field is returned only on creation. Store it securely in your secrets manager. To retrieve a new secret, use the regenerate endpoint.


Event catalog

Subscribe to any combination of the following events.

User events

EventTriggered when
user.createdA new user account is created (direct, SCIM, JIT, or invitation)
user.updatedA user's profile is updated
user.deletedA user account is permanently deleted
user.suspendedA user account is suspended
user.reactivatedA suspended user account is reactivated

Authentication events

EventTriggered when
auth.loginA user successfully authenticates
auth.logoutA user logs out
auth.failedAn authentication attempt fails
auth.mfa_verifiedA user completes MFA verification

Security events

EventTriggered when
security.password_changedA user changes their password
security.mfa_enabledA user enables MFA on their account
security.lockoutA user account is locked after failed attempts

Role events

EventTriggered when
role.assignedA role is assigned to a user
role.revokedA role is removed from a user
role.createdA new role is created in the tenant
role.deletedA role is deleted

Group events

EventTriggered when
group.member_addedA user is added to a group
group.member_removedA user is removed from a group

SCIM events

EventTriggered when
scim.user_syncedA user is created or updated via SCIM
scim.group_syncedA group is created or updated via SCIM

Invitation events

EventTriggered when
invitation.sentAn invitation email is sent to a user
invitation.acceptedA user accepts an invitation and completes account setup

Payload format

All webhook deliveries share the same envelope structure. The event field tells you what happened and the payload object contains event-specific data.

{
"id": "e7f8a9b0-1234-4abc-def0-1234567890ab",
"event": "user.created",
"tenantId": "c2e3f4a5-6b7c-8d9e-0f1a-2b3c4d5e6f70",
"timestamp": "2026-02-24T10:00:00Z",
"test": false,
"payload": {
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "[email protected]",
"firstName": "Jane",
"lastName": "Doe",
"status": "active",
"createdAt": "2026-02-24T10:00:00Z"
}
}

For test deliveries (sent via the Send Test button or API), "test": true is set and the payload is a simplified object.


Signature verification

Every delivery includes an X-Webhook-Signature header containing the HMAC-SHA256 hex digest of the raw request body, signed with your webhook's secret key.

Signature format: hex-encoded HMAC-SHA256 (not prefixed — a 64-character lowercase hex string).

Secret format: whsec_ followed by 64 hex characters (the whsec_ prefix is part of the secret value used as the HMAC key).

How signing works

signature = HMAC-SHA256(secret, raw_request_body)
header value = hex.encode(signature)

The raw request body is the exact bytes received, before any JSON parsing.

Verification examples

Node.js

const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}

// Express example
app.post('/webhooks/ithbat', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const secret = process.env.ITHBAT_WEBHOOK_SECRET;

if (!verifyWebhook(req.body, signature, secret)) {
return res.status(401).send('Invalid signature');
}

const event = JSON.parse(req.body);
console.log('Received event:', event.event);
res.status(200).send('OK');
});

Python

import hmac
import hashlib

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

# Flask example
from flask import Flask, request, abort
import os

app = Flask(__name__)

@app.route('/webhooks/ithbat', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature', '')
secret = os.environ['ITHBAT_WEBHOOK_SECRET']

if not verify_webhook(request.get_data(), signature, secret):
abort(401)

event = request.get_json()
print(f"Received event: {event['event']}")
return '', 200

Go

package webhook

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
)

func VerifySignature(body []byte, signature, secret string) bool {
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
expected := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}

func HandleWebhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}

signature := r.Header.Get("X-Webhook-Signature")
secret := os.Getenv("ITHBAT_WEBHOOK_SECRET")

if !VerifySignature(body, signature, secret) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}

// Process event...
w.WriteHeader(http.StatusOK)
}

PHP

function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}

$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret = getenv('ITHBAT_WEBHOOK_SECRET');

if (!verifyWebhook($rawBody, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}

$event = json_decode($rawBody, true);
error_log('Received event: ' . $event['event']);
http_response_code(200);
tip

Always use a timing-safe comparison (crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal, hash_equals) to prevent timing attacks. Never use == or === for signature comparison.


Delivery and retry policy

  • Ithbat makes the initial delivery attempt immediately after the event fires.
  • Delivery is considered successful if your endpoint returns any 2xx HTTP status code.
  • If delivery fails (non-2xx response, timeout, or connection error), Ithbat records the failure in the delivery log.
  • The HTTP client has a 30-second timeout per attempt.
  • You can manually retry any failed delivery from the admin console or via the API.
note

Automatic exponential backoff retry is on the roadmap. Currently, failed deliveries must be retried manually via the admin console or POST /api/v1/webhooks/logs/{logId}/retry.

Your endpoint should respond quickly (within a few seconds) and process the event asynchronously. If your handler takes longer than 30 seconds, the delivery is marked as failed even if processing succeeds.


Delivery logs

Ithbat records every delivery attempt with the request payload, response status code, and response body.

View delivery logs in the console

Go to Settings → Webhooks → {webhook name} → Delivery Logs.

Via API

GET /api/v1/webhooks/logs
Authorization: Bearer {token}
X-Tenant-ID: {tenant_id}

Filter by webhook:

GET /api/v1/webhooks/logs?webhookId={id}

Retry a failed delivery

POST /api/v1/webhooks/logs/{logId}/retry
Authorization: Bearer {token}
X-Tenant-ID: {tenant_id}

Sending a test webhook

Before going live, send a test delivery from the admin console or API to verify your endpoint is reachable and signature verification works.

Via admin console

Go to Settings → Webhooks → {webhook} → Send Test.

Via API

POST /api/v1/webhooks/{id}/test
Authorization: Bearer {token}
X-Tenant-ID: {tenant_id}
Content-Type: application/json

{
"event": "user.created"
}

Response:

{
"success": true,
"statusCode": 200,
"response": "OK"
}

Managing webhooks via API

List webhooks

GET /api/v1/webhooks
Authorization: Bearer {token}
X-Tenant-ID: {tenant_id}

Get a webhook

GET /api/v1/webhooks/{id}
Authorization: Bearer {token}

Update a webhook

PUT /api/v1/webhooks/{id}
Authorization: Bearer {token}
Content-Type: application/json

{
"name": "Production Webhook",
"url": "https://your-app.example.com/webhooks/ithbat",
"events": ["user.created", "user.deleted"],
"isActive": true
}

Delete a webhook

DELETE /api/v1/webhooks/{id}
Authorization: Bearer {token}

Regenerate signing secret

Use this if you believe the secret has been compromised. The new secret is returned in the response and takes effect immediately. All subsequent deliveries will use the new secret.

POST /api/v1/webhooks/{id}/regenerate-secret
Authorization: Bearer {token}

Response:

{
"secret": "whsec_newvalue..."
}

Security best practices

Verify every request: Never process a webhook event without first verifying the X-Webhook-Signature header. An unverified endpoint can be abused to inject malicious payloads.

Use HTTPS: HTTP endpoints are accepted but not recommended. In production, your endpoint must be served over HTTPS with a valid certificate.

Respond quickly: Return a 200 status as soon as you have verified the signature. Move heavy processing to a background queue to avoid timeouts.

Idempotency: It is possible (though rare) for the same event to be delivered more than once due to retries. Design your handler to be idempotent — use the delivery id as a deduplication key.

Store the raw body: Parse JSON only after signature verification. Parsing first and re-serializing can alter byte order and invalidate the signature check.

Rotate secrets periodically: Use the regenerate-secret endpoint every few months as part of routine secret rotation.


Next steps

  • RBAC & Permissions — control which admin users can manage webhooks (webhook:read, webhook:write)
  • Audit Logs — webhook events are also available in the audit log