Webhook Integration for Developers
A comprehensive guide for developers building custom webhook endpoints to receive Keycloak authentication events and logs from Skycloak’s SIEM Integration.
Overview
Webhooks provide a flexible way to receive real-time Keycloak events in your application. This guide covers:
- Building a webhook endpoint
- Understanding the payload format
- Implementing authentication
- Testing and debugging
- Production best practices
Quick Start
Minimal Webhook Endpoint
Here’s a minimal Express.js endpoint that receives Skycloak events:
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/skycloak-events', (req, res) => {
const { source, type, timestamp, event_count, events } = req.body;
console.log(`Received ${event_count} ${type} events from ${source}`);
// Process events
events.forEach(event => {
console.log(`Event: ${event.type} at ${event.timestamp}`);
});
// Return 200 to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});Payload Format
Request Structure
Skycloak sends POST requests with the following envelope structure:
{
"source": "skycloak",
"type": "<event_type>",
"timestamp": "2025-01-15T14:30:00Z",
"event_count": 1,
"events": [...]
}The type field indicates the kind of data: keycloak_events, application_logs, or security_logs. Each type has a different event structure inside the events array.
Envelope Fields
| Field | Type | Description |
|---|---|---|
source |
string | Always "skycloak"
|
type |
string | Data type: "keycloak_events", "application_logs", or "security_logs"
|
timestamp |
string | ISO 8601 timestamp when the webhook was sent |
event_count |
number | Number of events in the batch |
events |
array | Array of event objects (structure depends on type) |
Keycloak Events (keycloak_events)
User authentication events (LOGIN, LOGOUT, REGISTER, etc.) and admin events (CREATE, UPDATE, DELETE).
User Event Example
{
"source": "skycloak",
"type": "keycloak_events",
"timestamp": "2025-01-15T14:30:00Z",
"event_count": 1,
"events": [
{
"type": "LOGIN",
"timestamp": "2025-01-15T14:29:45Z",
"realm": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "production"
},
"auth": {
"client_id": "web-app",
"redirect_uri": "https://app.example.com/callback",
"method": "openid-connect",
"identity_provider": "google"
},
"user": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"username": "[email protected]",
"ip_address": "203.0.113.42"
},
"session": {
"id": "7f3e9c8a-1b4d-4e5f-a6c7-8d9e0f1a2b3c"
},
"metadata": {
"event_type": "keycloak_events",
"workspace_id": "f1e2d3c4-b5a6-4789-a012-345678901234",
"cluster_id": "a1b2c3d4-e5f6-4789-a012-345678901234",
"source": "skycloak"
},
"details": {
"id": "evt-abc123",
"grant_type": "authorization_code",
"event_category": "user",
"is_m2_m": false,
"geo": {
"country": "United States",
"country_code": "US",
"city": "San Francisco",
"latitude": 37.7749,
"longitude": -122.4194
},
"severity": "info",
"priority": 1,
"source": "skycloak",
"product": "keycloak",
"vendor": "keycloak"
}
}
]
}Admin Event Example
Admin events (realm configuration changes, user management, etc.) include resource information:
{
"source": "skycloak",
"type": "keycloak_events",
"timestamp": "2025-01-15T14:30:00Z",
"event_count": 1,
"events": [
{
"type": "CREATE",
"timestamp": "2025-01-15T14:29:45Z",
"realm": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "production"
},
"user": {
"ip_address": "203.0.113.42"
},
"resource": {
"type": "USER",
"path": "users/123e4567-e89b-12d3-a456-426614174000"
},
"metadata": {
"event_type": "keycloak_events",
"workspace_id": "f1e2d3c4-b5a6-4789-a012-345678901234",
"cluster_id": "a1b2c3d4-e5f6-4789-a012-345678901234",
"source": "skycloak"
},
"details": {
"id": "evt-def456",
"event_category": "admin",
"geo": {
"country": "United States",
"country_code": "US",
"city": "San Francisco",
"latitude": 37.7749,
"longitude": -122.4194
},
"severity": "info",
"priority": 1,
"source": "skycloak",
"product": "keycloak",
"vendor": "keycloak"
}
}
]
}Keycloak Event Fields
| Field | Type | Description |
|---|---|---|
type |
string | Event type (LOGIN, LOGOUT, REGISTER, LOGIN_ERROR, CREATE, UPDATE, etc.) |
timestamp |
string | When the event occurred (ISO 8601) |
error |
string | Error code, present for error events (e.g., LOGIN_ERROR → "user_not_found") |
realm |
object | Realm info: id (UUID), name (realm name) |
auth |
object | Auth info: client_id, redirect_uri, method, type, identity_provider
|
user |
object | User info: id, username, email, ip_address
|
session |
object | Session info: id
|
resource |
object | Admin events only: type (USER, REALM, CLIENT, etc.), path
|
metadata |
object | Event metadata (see below) |
details |
object | Additional fields including geo, severity, priority, and event-specific data |
Metadata Fields
| Field | Type | Description |
|---|---|---|
event_type |
string | Type of data: "keycloak_events"
|
workspace_id |
string | Skycloak workspace UUID |
cluster_id |
string | Skycloak cluster UUID |
source |
string | Always "skycloak"
|
Details Fields
The details object contains SIEM-enriched fields and any additional event-specific data:
| Field | Type | Description |
|---|---|---|
id |
string | Unique event ID |
event_category |
string |
"user" or "admin"
|
grant_type |
string | OAuth grant type (e.g., "authorization_code") |
is_m2_m |
boolean | Whether this is a machine-to-machine event |
geo |
object | GeoIP data: country, country_code, city, latitude, longitude
|
severity |
string |
"info" for normal events, "high" for error events |
priority |
number |
1 for normal events, 3 for error events |
product |
string | Always "keycloak"
|
vendor |
string | Always "keycloak"
|
Note: The geo object is automatically enriched from the user’s IP address. It is absent when geolocation data is unavailable.
Application Logs (application_logs)
Raw Keycloak application log lines from Loki.
{
"source": "skycloak",
"type": "application_logs",
"timestamp": "2025-01-15T14:30:00Z",
"event_count": 1,
"events": [
{
"type": "",
"timestamp": "2025-01-15T14:29:45Z",
"application": {
"level": "INFO",
"logger": "org.keycloak.events",
"thread": "executor-thread-19",
"message": "type=LOGIN, realmId=production, clientId=web-app, userId=123e4567..."
},
"metadata": {
"event_type": "application_logs",
"workspace_id": "f1e2d3c4-b5a6-4789-a012-345678901234",
"cluster_id": "a1b2c3d4-e5f6-4789-a012-345678901234",
"source": "skycloak"
},
"details": {}
}
]
}Application Log Fields
| Field | Type | Description |
|---|---|---|
timestamp |
string | When the log was emitted (ISO 8601) |
application |
object | Log data: level, logger, thread, message
|
metadata |
object | Event metadata: event_type, workspace_id, cluster_id, source
|
details |
object | Any additional fields from the log entry |
The application.message field contains the raw Keycloak log line, which may include structured data like type=LOGIN, realmId=..., clientId=....
Security Logs (security_logs)
WAF and geo-blocking security events.
{
"source": "skycloak",
"type": "security_logs",
"timestamp": "2025-01-15T14:30:00Z",
"event_count": 1,
"events": [
{
"type": "",
"timestamp": "2025-01-15T14:29:45Z",
"security": {
"action": "blocked",
"rule_id": "942100",
"attack_type": "sql_injection",
"severity": "critical",
"anomaly_score": "15",
"uri": "/auth/realms/production/protocol/openid-connect/token",
"method": "POST",
"source_ip": "203.0.113.42"
},
"metadata": {
"event_type": "security_logs",
"workspace_id": "f1e2d3c4-b5a6-4789-a012-345678901234",
"cluster_id": "a1b2c3d4-e5f6-4789-a012-345678901234",
"source": "skycloak"
},
"details": {}
}
]
}Security Log Fields
| Field | Type | Description |
|---|---|---|
timestamp |
string | When the security event occurred (ISO 8601) |
security |
object | Security data: action, rule_id, attack_type, severity, anomaly_score, uri, method, source_ip
|
metadata |
object | Event metadata: event_type, workspace_id, cluster_id, source
|
details |
object | Any additional fields from the security event |
Implementation Examples
Node.js with Express
Complete example with authentication and error handling:
const express = require('express');
const crypto = require('crypto');
const app = express();
// Environment variables
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-secret-token';
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json({ limit: '10mb' }));
// Bearer token authentication
const authenticateWebhook = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const token = authHeader.substring(7);
if (token !== WEBHOOK_SECRET) {
return res.status(403).json({ error: 'Invalid token' });
}
next();
};
// Webhook endpoint
app.post('/webhooks/skycloak-events', authenticateWebhook, async (req, res) => {
try {
const { source, type, timestamp, event_count, events } = req.body;
// Validate payload
if (source !== 'skycloak' || type !== 'keycloak_events') {
return res.status(400).json({ error: 'Invalid payload source or type' });
}
if (!Array.isArray(events) || events.length === 0) {
return res.status(400).json({ error: 'Events array is required' });
}
console.log(`[${timestamp}] Received ${event_count} events`);
// Process events asynchronously
const results = await Promise.allSettled(
events.map(event => processEvent(event))
);
// Log any failures
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0) {
console.error(`Failed to process ${failures.length} events:`, failures);
}
// Always return 200 to acknowledge receipt
// Even if some events failed processing, we received them
res.status(200).json({
received: true,
processed: results.filter(r => r.status === 'fulfilled').length,
failed: failures.length
});
} catch (error) {
console.error('Webhook error:', error);
// Return 200 even on error to avoid retries
res.status(200).json({ received: true, error: error.message });
}
});
async function processEvent(event) {
const eventType = event.type;
const username = event.user?.username;
const ipAddress = event.user?.ip_address;
const realmName = event.realm?.name;
console.log(`Processing ${eventType}: user=${username}, ip=${ipAddress}, realm=${realmName}`);
// Example: Store in database
// await db.events.create({
// type: eventType,
// timestamp: new Date(event.timestamp),
// user: username,
// data: event
// });
// Example: Send to analytics
// if (eventType === 'LOGIN' || eventType === 'LOGIN_ERROR') {
// await analytics.track({
// event: eventType,
// userId: event.user?.id,
// properties: { realm: realmName, ip_address: ipAddress }
// });
// }
// Example: Trigger alerts
// if (eventType === 'LOGIN_ERROR' && event.error === 'invalid_user_credentials') {
// await alerting.checkBruteForce(username, ipAddress);
// }
}
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});Python with Flask
from flask import Flask, request, jsonify
import os
from datetime import datetime
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
WEBHOOK_SECRET = os.getenv('WEBHOOK_SECRET', 'your-secret-token')
def authenticate_webhook():
"""Verify Bearer token"""
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return False
token = auth_header[7:]
return token == WEBHOOK_SECRET
@app.route('/webhooks/skycloak-events', methods=['POST'])
def handle_webhook():
# Authenticate
if not authenticate_webhook():
return jsonify({'error': 'Unauthorized'}), 401
try:
data = request.get_json()
# Validate payload
if data.get('source') != 'skycloak' or data.get('type') != 'keycloak_events':
return jsonify({'error': 'Invalid payload'}), 400
events = data.get('events', [])
logging.info(f"Received {len(events)} events at {data.get('timestamp')}")
# Process events
for event in events:
process_event(event)
return jsonify({'received': True, 'processed': len(events)}), 200
except Exception as e:
logging.error(f"Webhook error: {e}")
# Return 200 to avoid retries
return jsonify({'received': True, 'error': str(e)}), 200
def process_event(event):
"""Process individual event"""
event_type = event.get('type')
user = event.get('user', {})
username = user.get('username')
ip_address = user.get('ip_address')
realm_name = event.get('realm', {}).get('name')
logging.info(f"Processing {event_type}: user={username}, ip={ip_address}, realm={realm_name}")
# Example: Store in database
# db.session.add(Event(
# type=event_type,
# timestamp=datetime.fromisoformat(event['timestamp']),
# user=username,
# data=event
# ))
# db.session.commit()
# Example: Check for suspicious activity
if event_type == 'LOGIN_ERROR':
check_failed_login_attempts(username, ip_address)
def check_failed_login_attempts(username, ip_address):
"""Example: Alert on multiple failed logins"""
# Implement your logic here
pass
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3000)Go with net/http
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
type WebhookPayload struct {
Source string `json:"source"`
Type string `json:"type"`
Timestamp string `json:"timestamp"`
EventCount int `json:"event_count"`
Events []map[string]interface{} `json:"events"`
}
type WebhookResponse struct {
Received bool `json:"received"`
Processed int `json:"processed,omitempty"`
Error string `json:"error,omitempty"`
}
var webhookSecret = os.Getenv("WEBHOOK_SECRET")
func main() {
if webhookSecret == "" {
webhookSecret = "your-secret-token"
}
http.HandleFunc("/webhooks/skycloak-events", handleWebhook)
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
log.Printf("Webhook server listening on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
// Check method
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Authenticate
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
if token != webhookSecret {
http.Error(w, "Invalid token", http.StatusForbidden)
return
}
// Parse payload
var payload WebhookPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
log.Printf("Failed to parse payload: %v", err)
respondJSON(w, WebhookResponse{Received: true, Error: err.Error()}, http.StatusOK)
return
}
// Validate
if payload.Source != "skycloak" || payload.Type != "keycloak_events" {
http.Error(w, "Invalid payload", http.StatusBadRequest)
return
}
log.Printf("[%s] Received %d events", payload.Timestamp, payload.EventCount)
// Process events
processed := 0
for _, event := range payload.Events {
if err := processEvent(event); err != nil {
log.Printf("Failed to process event: %v", err)
} else {
processed++
}
}
respondJSON(w, WebhookResponse{Received: true, Processed: processed}, http.StatusOK)
}
func processEvent(event map[string]interface{}) error {
eventType, _ := event["type"].(string)
var username, ipAddress string
if user, ok := event["user"].(map[string]interface{}); ok {
username, _ = user["username"].(string)
ipAddress, _ = user["ip_address"].(string)
}
log.Printf("Processing %s: user=%s, ip=%s", eventType, username, ipAddress)
// Example: Store in database
// if err := db.InsertEvent(event); err != nil {
// return fmt.Errorf("database error: %w", err)
// }
// Example: Send alert
// if eventType == "LOGIN_ERROR" {
// checkBruteForce(username, ipAddress)
// }
return nil
}
func respondJSON(w http.ResponseWriter, data interface{}, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}Authentication
Skycloak supports two authentication methods for webhooks:
Bearer Token (Recommended)
Simple and secure for most use cases:
// Verify Bearer token
const authHeader = req.headers.authorization;
const token = authHeader.substring(7); // Remove "Bearer "
if (token !== WEBHOOK_SECRET) {
return res.status(403).json({ error: 'Invalid token' });
}Configuration in Skycloak:
- Authentication Type: Bearer Token
- Auth Token: Your secure random token (e.g.,
sk_live_abc123...)
Basic Authentication
Username and password encoded in Base64:
// Verify Basic auth
const authHeader = req.headers.authorization;
const base64Credentials = authHeader.substring(6); // Remove "Basic "
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
const [username, password] = credentials.split(':');
if (username !== EXPECTED_USERNAME || password !== EXPECTED_PASSWORD) {
return res.status(403).json({ error: 'Invalid credentials' });
}Configuration in Skycloak:
- Authentication Type: Basic Auth
- Auth Token:
username:password(Skycloak will Base64 encode it)
Testing Your Webhook
Local Development with ngrok
- Start your webhook server locally:
node webhook-server.js
# Server running on http://localhost:3000- Expose it with ngrok:
ngrok http 3000
# Forwarding https://abc123.ngrok.io -> http://localhost:3000- Use the ngrok HTTPS URL in Skycloak:
https://abc123.ngrok.io/webhooks/skycloak-events
Testing with curl
Send a test payload to your endpoint:
curl -X POST https://your-domain.com/webhooks/skycloak-events \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{
"source": "skycloak",
"type": "keycloak_events",
"timestamp": "2025-01-15T14:30:00Z",
"event_count": 1,
"events": [
{
"type": "LOGIN",
"timestamp": "2025-01-15T14:29:45Z",
"realm": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "production" },
"auth": { "client_id": "web-app" },
"user": { "id": "user-uuid", "username": "[email protected]", "ip_address": "203.0.113.42" },
"metadata": { "event_type": "keycloak_events", "workspace_id": "ws-uuid", "cluster_id": "cl-uuid", "source": "skycloak" },
"details": { "event_category": "user", "severity": "info", "priority": 1 }
}
]
}'Testing with Postman
- Create new POST request
- URL:
https://your-domain.com/webhooks/skycloak-events - Headers:
-
Content-Type:application/json -
Authorization:Bearer your-secret-token
-
- Body (raw JSON): Copy the test payload from above
- Send and verify 200 response
Response Codes
Your webhook endpoint should return appropriate HTTP status codes:
| Code | Meaning | Skycloak Behavior |
|---|---|---|
200-299 |
Success | Event marked as delivered |
400-499 |
Client error | Event marked as failed (no retry) |
500-599 |
Server error | Will retry with exponential backoff |
Timeout |
No response after 30s | Will retry |
Important: Always return 200 to acknowledge receipt, even if processing fails. Handle errors internally and retry later if needed.
Error Handling
Idempotency
Skycloak may send the same event multiple times due to retries. Implement idempotency:
const processedEvents = new Set();
async function processEvent(event) {
// Create unique ID from event
const eventId = event.details?.id || `${event.type}-${event.timestamp}-${event.user?.id}`;
// Skip if already processed
if (processedEvents.has(eventId)) {
console.log(`Skipping duplicate event: ${eventId}`);
return;
}
// Process event
await saveToDatabase(event);
// Mark as processed
processedEvents.add(eventId);
}For production, use a database or cache (Redis) instead of in-memory Set.
Retry Logic
If processing fails, queue for retry:
const queue = require('bull'); // Or your preferred queue
const eventQueue = queue('events');
app.post('/webhooks/skycloak-events', async (req, res) => {
const { events } = req.body;
// Acknowledge immediately
res.status(200).json({ received: true });
// Queue for processing
for (const event of events) {
await eventQueue.add(event, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
}
});
// Process events asynchronously
eventQueue.process(async (job) => {
await processEvent(job.data);
});Production Best Practices
1. Use HTTPS
Always use HTTPS in production:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem')
};
https.createServer(options, app).listen(443);Or use a reverse proxy (nginx, Cloudflare) to handle SSL.
2. Rate Limiting
Protect your endpoint from abuse:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Too many requests'
});
app.use('/webhooks', limiter);3. Request Size Limits
Limit payload size to prevent DoS:
app.use(express.json({ limit: '10mb' }));4. Logging
Log all webhook requests for debugging:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'webhooks.log' })
]
});
app.post('/webhooks/skycloak-events', (req, res) => {
logger.info('Webhook received', {
timestamp: new Date().toISOString(),
event_count: req.body.event_count,
source_ip: req.ip
});
// Process...
});5. Monitoring
Monitor webhook health:
const prometheus = require('prom-client');
const webhookCounter = new prometheus.Counter({
name: 'webhook_requests_total',
help: 'Total webhook requests'
});
const webhookDuration = new prometheus.Histogram({
name: 'webhook_duration_seconds',
help: 'Webhook processing duration'
});
app.post('/webhooks/skycloak-events', async (req, res) => {
const start = Date.now();
webhookCounter.inc();
// Process...
webhookDuration.observe((Date.now() - start) / 1000);
res.status(200).json({ received: true });
});6. Secrets Management
Never hardcode secrets:
// Bad
const WEBHOOK_SECRET = 'my-secret-token';
// Good - Use environment variables
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// Better - Use secrets manager
const AWS = require('aws-sdk');
const secrets = new AWS.SecretsManager();
async function getSecret() {
const data = await secrets.getSecretValue({
SecretId: 'skycloak/webhook-secret'
}).promise();
return data.SecretString;
}Use Cases
1. Security Monitoring
Alert on suspicious login patterns:
async function processEvent(event) {
if (event.type === 'LOGIN_ERROR') {
const username = event.user?.username;
const ipAddress = event.user?.ip_address;
const recentFailures = await countRecentFailedLogins(username, ipAddress);
if (recentFailures >= 5) {
await sendAlert({
type: 'brute_force_detected',
username,
ip: ipAddress,
count: recentFailures
});
}
}
}2. User Analytics
Track authentication metrics:
async function processEvent(event) {
if (event.type === 'LOGIN') {
await analytics.track({
userId: event.user?.id,
event: 'User Login',
properties: {
realm: event.realm?.name,
client: event.auth?.client_id,
ip: event.user?.ip_address,
timestamp: event.timestamp
}
});
}
}3. Compliance Logging
Store events for audit trail:
async function processEvent(event) {
await db.auditLog.create({
timestamp: new Date(event.timestamp),
event_type: event.type,
user_id: event.user?.id,
username: event.user?.username,
ip_address: event.user?.ip_address,
realm: event.realm?.name,
details: event.details,
retention_until: new Date(Date.now() + 7 * 365 * 24 * 60 * 60 * 1000) // 7 years
});
}4. Real-time Notifications
Notify users of account activity:
async function processEvent(event) {
if (event.type === 'LOGIN' && isUnusualLocation(event.user?.ip_address)) {
await sendEmail({
to: event.user?.username,
subject: 'New login from unusual location',
body: `Someone logged into your account from ${event.user?.ip_address}`
});
}
}Troubleshooting
Webhook Not Receiving Events
- Check URL is publicly accessible:
curl -I https://your-domain.com/webhooks/skycloak-events- Verify SSL certificate (must be valid):
curl -v https://your-domain.com/webhooks/skycloak-eventsCheck firewall rules - Allow inbound HTTPS (port 443)
Review Skycloak destination status - Check error message in UI
Authentication Failures
- Verify token matches - Check environment variables
-
Check header format - Should be
Authorization: Bearer <token> - Test with curl - Verify manually with correct headers
Events Not Processing
- Check logs - Look for errors in application logs
-
Verify payload structure - Log
req.bodyto debug - Test idempotency - Ensure duplicate events aren’t causing issues
Performance Issues
- Process events asynchronously - Don’t block webhook response
- Use queue for heavy processing - Bull, RabbitMQ, SQS
- Scale horizontally - Run multiple webhook servers behind load balancer
- Optimize database queries - Use indexes, batch inserts
Security Checklist
- ✅ HTTPS only (no HTTP in production)
- ✅ Bearer token or Basic auth enabled
- ✅ Secrets stored in environment variables or secrets manager
- ✅ Request size limits enforced
- ✅ Rate limiting configured
- ✅ Input validation on all fields
- ✅ Logging enabled (but not logging secrets)
- ✅ Monitoring and alerting configured
- ✅ Error handling prevents sensitive data exposure
- ✅ Webhook endpoint not exposed in public documentation
Next Steps
- Set up your webhook endpoint
- Configure in Skycloak SIEM Integration
- Test with sample events
- Monitor webhook health
- Implement your use case (security alerts, analytics, compliance)
Support
For webhook integration questions: