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:
- Serializes the event payload as JSON.
- Signs the payload with HMAC-SHA256 using your webhook's secret key.
- Sends an HTTP POST to your endpoint with a 30-second timeout.
- 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
- Go to Settings → Webhooks → Add Webhook.
- Enter a Name and the Endpoint URL (must be
https://in production). - Select the events to subscribe to.
- 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"
}
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
| Event | Triggered when |
|---|---|
user.created | A new user account is created (direct, SCIM, JIT, or invitation) |
user.updated | A user's profile is updated |
user.deleted | A user account is permanently deleted |
user.suspended | A user account is suspended |
user.reactivated | A suspended user account is reactivated |
Authentication events
| Event | Triggered when |
|---|---|
auth.login | A user successfully authenticates |
auth.logout | A user logs out |
auth.failed | An authentication attempt fails |
auth.mfa_verified | A user completes MFA verification |
Security events
| Event | Triggered when |
|---|---|
security.password_changed | A user changes their password |
security.mfa_enabled | A user enables MFA on their account |
security.lockout | A user account is locked after failed attempts |
Role events
| Event | Triggered when |
|---|---|
role.assigned | A role is assigned to a user |
role.revoked | A role is removed from a user |
role.created | A new role is created in the tenant |
role.deleted | A role is deleted |
Group events
| Event | Triggered when |
|---|---|
group.member_added | A user is added to a group |
group.member_removed | A user is removed from a group |
SCIM events
| Event | Triggered when |
|---|---|
scim.user_synced | A user is created or updated via SCIM |
scim.group_synced | A group is created or updated via SCIM |
Invitation events
| Event | Triggered when |
|---|---|
invitation.sent | An invitation email is sent to a user |
invitation.accepted | A 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);
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
2xxHTTP 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.
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