Implement webhooks
Receive real-time notifications about authentication events in your application using Scalekit webhooks
Receive real-time notifications when authentication and user management events occur in your application. For example, when users are provisioned, updated, or deprovisioned through directory sync, Scalekit sends webhook events to your application so you can keep your systems synchronized automatically.
Scalekit sends POST requests to your webhook endpoint with event details, and waits for a 201 response to confirm receipt. For example, one event fires immediately after a user is provisioned via SCIM. We’ll explore more events throughout this guide.
Implementing webhooks
Section titled “Implementing webhooks”You can receive webhooks for directory sync events, user provisioning changes, and other authentication events.
| Event | When it fires |
|---|---|
organization.directory_enabled | Directory synchronization is activated for an organization |
organization.directory_disabled | Directory synchronization is deactivated for an organization |
organization.directory.user_created | New user is provisioned via SCIM |
organization.directory.user_updated | User profile is updated via SCIM |
organization.directory.user_deleted | User is deprovisioned via SCIM |
organization.directory.group_created | New group is created via SCIM |
organization.directory.group_updated | Group is updated via SCIM |
organization.directory.group_deleted | Group is deleted via SCIM |
At each event occurrence, Scalekit sends a POST request to your webhook endpoint with the relevant event details.
-
Verify the webhook request
Section titled “Verify the webhook request”Create an HTTPS endpoint that receives and verifies POST requests from Scalekit. This critical security step ensures requests are authentic and haven’t been tampered with.
Express.js - Verify request signature // Security: ALWAYS verify requests are from Scalekit before processing// This prevents unauthorized parties from triggering your webhook handlersapp.post('/webhooks/manage-users', async (req, res) => {try {// Parse the webhook headers for signature verificationconst headers = {'webhook-id': req.headers['webhook-id'],'webhook-signature': req.headers['webhook-signature'],'webhook-timestamp': req.headers['webhook-timestamp']};const event = req.body;// Get the signing secret from Scalekit dashboard > Settings > Webhooks// Store this securely in environment variablesconst webhookSecret = process.env.SCALEKIT_WEBHOOK_SECRET;// Initialize Scalekit client (reference installation guide for setup)const scalekit = new ScalekitClient(process.env.SCALEKIT_ENVIRONMENT_URL,process.env.SCALEKIT_CLIENT_ID,process.env.SCALEKIT_CLIENT_SECRET);// Verify the webhook payload signature// This confirms the request is from Scalekit and hasn't been tampered withawait scalekit.verifyWebhookPayload(webhookSecret, headers, event);// ✓ Request verified - proceed to business logic (next step)} catch (error) {console.error('Webhook verification failed:', error);// Return 401 on verification failures to fail securelyreturn res.status(401).json({error: 'Invalid signature'});}});Flask - Verify request signature # Security: ALWAYS verify requests are from Scalekit before processing# This prevents unauthorized parties from triggering your webhook handlersfrom flask import Flask, request, jsonifyimport osapp = Flask(__name__)@app.route('/webhooks/manage-users', methods=['POST'])def handle_webhook():try:# Parse the webhook headers for signature verificationheaders = {'webhook-id': request.headers.get('webhook-id'),'webhook-signature': request.headers.get('webhook-signature'),'webhook-timestamp': request.headers.get('webhook-timestamp')}body = request.get_data()# Get the signing secret from Scalekit dashboard > Settings > Webhooks# Store this securely in environment variableswebhook_secret = os.getenv('SCALEKIT_WEBHOOK_SECRET')# Initialize Scalekit client (reference installation guide for setup)scalekit_client = ScalekitClient(env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),client_id=os.getenv("SCALEKIT_CLIENT_ID"),client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"))# Verify the webhook payload signature# This confirms the request is from Scalekit and hasn't been tampered withis_valid = scalekit_client.verify_webhook_payload(secret=webhook_secret,headers=headers,payload=body)if not is_valid:return jsonify({'error': 'Invalid signature'}), 401# ✓ Request verified - proceed to business logic (next step)except Exception as error:print(f'Webhook verification failed: {error}')# Return 401 on verification failures to fail securelyreturn jsonify({'error': 'Invalid signature'}), 401Gin - Verify request signature // Security: ALWAYS verify requests are from Scalekit before processing// This prevents unauthorized parties from triggering your webhook handlerspackage mainimport ("io""log""net/http""os""github.com/gin-gonic/gin")type WebhookResponse struct {Received bool `json:"received,omitempty"`Error *WebhookError `json:"error,omitempty"`}type WebhookError struct {Message string `json:"message"`}func handleWebhook(c *gin.Context) {// Parse the webhook headers for signature verificationheaders := map[string]string{"webhook-id": c.GetHeader("webhook-id"),"webhook-signature": c.GetHeader("webhook-signature"),"webhook-timestamp": c.GetHeader("webhook-timestamp"),}// Read raw body for verificationbodyBytes, err := io.ReadAll(c.Request.Body)if err != nil {c.JSON(http.StatusBadRequest, WebhookResponse{Error: &WebhookError{Message: "Unable to read request"},})return}// Get the signing secret from Scalekit dashboard > Settings > Webhooks// Store this securely in environment variableswebhookSecret := os.Getenv("SCALEKIT_WEBHOOK_SECRET")// Initialize Scalekit client (reference installation guide for setup)scalekitClient := scalekit.NewScalekitClient(os.Getenv("SCALEKIT_ENVIRONMENT_URL"),os.Getenv("SCALEKIT_CLIENT_ID"),os.Getenv("SCALEKIT_CLIENT_SECRET"),)// Verify the webhook payload signature// This confirms the request is from Scalekit and hasn't been tampered with_, err = scalekitClient.VerifyWebhookPayload(webhookSecret,headers,bodyBytes,)if err != nil {log.Printf("Webhook verification failed: %v", err)// Return 401 on verification failures to fail securelyc.JSON(http.StatusUnauthorized, WebhookResponse{Error: &WebhookError{Message: "Invalid signature"},})return}// ✓ Request verified - proceed to business logic (next step)}Spring Boot - Verify request signature // Security: ALWAYS verify requests are from Scalekit before processing// This prevents unauthorized parties from triggering your webhook handlerspackage com.example.webhooks;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;import java.util.Map;@RestController@RequestMapping("/webhooks")public class WebhookController {@PostMapping("/manage-users")public ResponseEntity<Map<String, Object>> handleWebhook(@RequestBody String body,@RequestHeader Map<String, String> headers) {try {// Get the signing secret from Scalekit dashboard > Settings > Webhooks// Store this securely in environment variablesString webhookSecret = System.getenv("SCALEKIT_WEBHOOK_SECRET");// Initialize Scalekit client (reference installation guide for setup)ScalekitClient scalekitClient = new ScalekitClient(System.getenv("SCALEKIT_ENVIRONMENT_URL"),System.getenv("SCALEKIT_CLIENT_ID"),System.getenv("SCALEKIT_CLIENT_SECRET"));// Verify the webhook payload signature// This confirms the request is from Scalekit and hasn't been tampered withboolean valid = scalekitClient.webhook().verifyWebhookPayload(webhookSecret, headers, body.getBytes());if (!valid) {// Return 401 on invalid signaturesreturn ResponseEntity.status(401).body(Map.of("error", "Invalid signature"));}// ✓ Request verified - proceed to business logic (next step)} catch (Exception error) {System.err.println("Webhook verification failed: " + error.getMessage());// Return 401 on verification failures to fail securelyreturn ResponseEntity.status(401).body(Map.of("error", "Invalid signature"));}}} -
Process the event and respond
Section titled “Process the event and respond”After verification, parse the webhook event, apply your custom business logic, and return a 201 response to acknowledge receipt.
Express.js - Business logic and response // Use case: Sync user data from directory to your application// Examples: User provisioning, deprovisioning, group membership updatesapp.post('/webhooks/manage-users', async (req, res) => {try {// ... (verification code from Step 1)// Parse the verified webhook eventconst event = req.body;// Process webhook asynchronously (don't block the response)// This allows us to respond quickly to prevent unnecessary retriesprocessWebhookEvent(event).catch(error => {// Log error but don't throw - we already acknowledged receiptconsole.error('Async processing error:', error);});// IMPORTANT: Always respond with 201 to acknowledge receipt// Scalekit expects this response within 10 secondsreturn res.status(201).json({ received: true });} catch (error) {console.error('Webhook error:', error);return res.status(401).json({ error: 'Invalid signature' });}});async function processWebhookEvent(event) {console.log(`Processing event: ${event.type}`);switch (event.type) {case 'organization.directory.user_created':// Sync new user to your databaseawait handleUserCreated(event.data);break;case 'organization.directory.user_updated':// Update user profile in your databaseawait handleUserUpdated(event.data);break;case 'organization.directory.user_deleted':// Deprovision user from your applicationawait handleUserDeleted(event.data);break;case 'organization.directory.group_created':// Handle new group creationawait handleGroupCreated(event.data);break;default:console.log(`Unhandled event type: ${event.type}`);}}async function handleUserCreated(data) {// Use case: Sync new user to your database when provisioned via SCIMconsole.log(`New user created: ${data.email} in org: ${data.organization_id}`);// Sync to your databaseawait syncUserToDatabase(data);// Grant default permissionsawait setupUserDefaults(data.id, data.organization_id);}Flask - Business logic and response # Use case: Sync user data from directory to your application# Examples: User provisioning, deprovisioning, group membership updatesimport json@app.route('/webhooks/manage-users', methods=['POST'])def handle_webhook():try:# ... (verification code from Step 1)# Parse the verified webhook eventevent = json.loads(body.decode('utf-8'))# Process webhook asynchronously (don't block the response)# This allows us to respond quickly to prevent unnecessary retriesprocess_webhook_event(event)# IMPORTANT: Always respond with 201 to acknowledge receipt# Scalekit expects this response within 10 secondsreturn jsonify({'received': True}), 201except Exception as error:print(f'Webhook error: {error}')return jsonify({'error': 'Invalid signature'}), 401def process_webhook_event(event):print(f'Processing event: {event["type"]}')event_type = event['type']event_data = event['data']if event_type == 'organization.directory.user_created':# Sync new user to your databasehandle_user_created(event_data)elif event_type == 'organization.directory.user_updated':# Update user profile in your databasehandle_user_updated(event_data)elif event_type == 'organization.directory.user_deleted':# Deprovision user from your applicationhandle_user_deleted(event_data)elif event_type == 'organization.directory.group_created':# Handle new group creationhandle_group_created(event_data)else:print(f'Unhandled event type: {event_type}')def handle_user_created(data):# Use case: Sync new user to your database when provisioned via SCIMprint(f'New user created: {data["email"]} in org: {data["organization_id"]}')# Sync to your databasesync_user_to_database(data)# Grant default permissionssetup_user_defaults(data['id'], data['organization_id'])Gin - Business logic and response // Use case: Sync user data from directory to your application// Examples: User provisioning, deprovisioning, group membership updatespackage mainimport ("encoding/json""log")type WebhookEvent struct {Type string `json:"type"`Data map[string]interface{} `json:"data"`}func handleWebhook(c *gin.Context) {// ... (verification code from Step 1)// Parse the verified webhook eventvar event WebhookEventif err := json.Unmarshal(bodyBytes, &event); err != nil {c.JSON(http.StatusBadRequest, WebhookResponse{Error: &WebhookError{Message: "Invalid event format"},})return}// Process webhook asynchronously (don't block the response)// This allows us to respond quickly to prevent unnecessary retriesgo processWebhookEvent(event)// IMPORTANT: Always respond with 201 to acknowledge receipt// Scalekit expects this response within 10 secondsc.JSON(http.StatusCreated, WebhookResponse{Received: true,})}func processWebhookEvent(event WebhookEvent) {log.Printf("Processing event: %s", event.Type)switch event.Type {case "organization.directory.user_created":// Sync new user to your databasehandleUserCreated(event.Data)case "organization.directory.user_updated":// Update user profile in your databasehandleUserUpdated(event.Data)case "organization.directory.user_deleted":// Deprovision user from your applicationhandleUserDeleted(event.Data)case "organization.directory.group_created":// Handle new group creationhandleGroupCreated(event.Data)default:log.Printf("Unhandled event type: %s", event.Type)}}func handleUserCreated(data map[string]interface{}) {// Use case: Sync new user to your database when provisioned via SCIMlog.Printf("New user created: %s in org: %s",data["email"], data["organization_id"])// Sync to your databasesyncUserToDatabase(data)// Grant default permissionssetupUserDefaults(data["id"].(string), data["organization_id"].(string))}Spring Boot - Business logic and response // Use case: Sync user data from directory to your application// Examples: User provisioning, deprovisioning, group membership updatespackage com.example.webhooks;import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;@PostMapping("/manage-users")public ResponseEntity<Map<String, Object>> handleWebhook(@RequestBody String body,@RequestHeader Map<String, String> headers) {try {// ... (verification code from Step 1)// Parse the verified webhook eventObjectMapper mapper = new ObjectMapper();JsonNode event = mapper.readTree(body);// Process webhook asynchronously (don't block the response)// This allows us to respond quickly to prevent unnecessary retriesprocessWebhookEventAsync(event);// IMPORTANT: Always respond with 201 to acknowledge receipt// Scalekit expects this response within 10 secondsreturn ResponseEntity.status(201).body(Map.of("received", true));} catch (Exception error) {System.err.println("Webhook error: " + error.getMessage());return ResponseEntity.status(401).body(Map.of("error", "Invalid signature"));}}private void processWebhookEvent(JsonNode event) {String eventType = event.get("type").asText();JsonNode eventData = event.get("data");System.out.println("Processing event: " + eventType);switch (eventType) {case "organization.directory.user_created":// Sync new user to your databasehandleUserCreated(eventData);break;case "organization.directory.user_updated":// Update user profile in your databasehandleUserUpdated(eventData);break;case "organization.directory.user_deleted":// Deprovision user from your applicationhandleUserDeleted(eventData);break;case "organization.directory.group_created":// Handle new group creationhandleGroupCreated(eventData);break;default:System.out.println("Unhandled event type: " + eventType);}}private void handleUserCreated(JsonNode data) {// Use case: Sync new user to your database when provisioned via SCIMSystem.out.println("New user created: " + data.get("email").asText() +" in org: " + data.get("organization_id").asText());// Sync to your databasesyncUserToDatabase(data);// Grant default permissionssetupUserDefaults(data.get("id").asText(), data.get("organization_id").asText());} -
Register the webhook in Scalekit dashboard
Section titled “Register the webhook in Scalekit dashboard”Configure your webhook by specifying the endpoint URL, selecting events to receive, and setting up security.
In the Scalekit dashboard, navigate to Settings > Webhooks to register your endpoint.
- Click Add Endpoint and provide:
- Endpoint URL - Your application’s HTTPS webhook handler (e.g.,
https://yourapp.com/webhooks/manage-users) - Description - Optional description for this endpoint
- Endpoint URL - Your application’s HTTPS webhook handler (e.g.,
- Select which events you want to receive:
- Directory sync events (
organization.directory_enabled,organization.directory_disabled) - User events (
organization.directory.user_created,user_updated,user_deleted) - Group events (
organization.directory.group_created,group_updated,group_deleted)
- Directory sync events (
- Copy the Signing Secret to verify webhook authenticity in your application
- Click Create
- Toggle Enable to activate the webhook
Complete webhook events reference View detailed schemas and payload examples for all webhook events - Click Add Endpoint and provide:
-
Test the webhook
Section titled “Test the webhook”Use the Test tab in the Scalekit dashboard to verify your implementation before enabling it in production.
- Open the Test tab on the Webhooks page
- The left panel shows the request body sent to your endpoint
- Click Send Test Event to test your webhook handler
- The right panel shows your application’s response
- Verify your endpoint returns a 201 status code
-
View webhook request logs
Section titled “View webhook request logs”Scalekit keeps a log of every webhook request sent to your application and the response it returned. Use these logs to debug and troubleshoot issues.
- Navigate to Settings > Webhooks in your dashboard
- Select your webhook endpoint
- Click on the Logs tab to view request history
- Each log entry shows the request payload, response status, and timestamps
Requests and responses generated by the “Test” button are not logged. This keeps production logs free of test data.