Handle webhook events in your application
Receive real-time notifications about authentication events in your application using Scalekit webhooks
Webhooks provide real-time notifications about authentication and user management events in your Scalekit environment. Instead of polling for changes, your application receives instant notifications when users sign up, log in, join organizations, or when other important events occur.
Webhooks enable your app to react immediately to changes in your auth stack through:
- Real-time updates: Get notified immediately when events occur
- Reduced API calls: No need to poll for changes
- Event-driven architecture: Build responsive workflows that react to user actions
- Reliable delivery: Scalekit ensures webhook delivery with automatic retries
Webhook event object
Section titled “Webhook event object”All webhook payloads follow a standardized structure with metadata and event-specific data in the data field.
{ "spec_version": "1", // The version of the event specification format. Currently "1". "id": "evt_123456789", // A unique identifier for the event (e.g., evt_123456789). "object": "DirectoryUser", // The type of object, such as "DirectoryUser", "Directory" or "Connection". Domain events omit it. "environment_id": "env_123456789", // The ID of the environment where the event occurred. "occurred_at": "2024-08-21T10:20:17.072Z", // ISO 8601 timestamp indicating when the event occurred. "organization_id": "org_123456789", // The organization the event is about. Not sent on user.* events; read data.organization.id or data.user_session.organization_id instead. "type": "organization.directory.user_created", // The specific event type (e.g., "organization.directory.user_created"). "data": { // Event-specific payload. Its fields depend on the event type. "id": "diruser_123456789", "organization_id": "org_123456789", "email": "user@example.com", "name": "John Doe" // ...more user fields }}Configure webhooks in the dashboard
Section titled “Configure webhooks in the dashboard”Set up webhook endpoints and select which events you want to receive through the Scalekit dashboard.
-
In your Scalekit dashboard, navigate to Developers > Webhooks
-
Click Add Endpoint and provide:
- Endpoint URL - Your application’s webhook handler URL (e.g.,
https://yourapp.com/webhooks/scalekit) - Description - Optional description for this endpoint
- Endpoint URL - Your application’s webhook handler URL (e.g.,
-
Choose which events you want to receive. For example:
- User events:
user.signup,user.login,user.logout,user.organization_invitation, anduser.organization_membership_created,_updatedand_deleted - Organization events:
organization.created,organization.updated,organization.deleted - SSO events:
organization.sso_created,organization.sso_enabled,organization.sso_disabled,organization.sso_deleted - Directory events:
organization.directory_created,organization.directory_enabledand siblings, plusorganization.directory.user_*andorganization.directory.group_*
See the webhooks reference for every event and its payload.
- User events:
-
Copy the Signing Secret - you’ll use this to verify webhook authenticity in your application
-
Use the Send Test Event button to verify your endpoint is working correctly
Implement webhook handlers
Section titled “Implement webhook handlers”Create secure webhook handlers in your application to process incoming events from Scalekit.
-
Set up webhook endpoint
Section titled “Set up webhook endpoint”Create an HTTP POST endpoint in your application to receive webhook payloads from Scalekit.
Express.js webhook handler 3 collapsed linesimport express from 'express'import { Scalekit } from '@scalekit-sdk/node'const app = express()const scalekit = new Scalekit(process.env.SCALEKIT_ENVIRONMENT_URL,process.env.SCALEKIT_CLIENT_ID,process.env.SCALEKIT_CLIENT_SECRET)// HMAC is over the raw bytes. Parsing JSON first changes whitespace and fails verify.app.use('/webhooks/scalekit', express.raw({ type: 'application/json' }))app.post('/webhooks/scalekit', async (req, res) => {const secret = process.env.SCALEKIT_WEBHOOK_SECRETconst payload = req.body.toString()const headers = {'webhook-id': req.headers['webhook-id'],'webhook-timestamp': req.headers['webhook-timestamp'],'webhook-signature': req.headers['webhook-signature'],}try {scalekit.verifyWebhookPayload(secret, headers, payload)} catch (error) {console.error('Invalid webhook signature')return res.status(401).json({ error: 'Invalid signature' })}const event = JSON.parse(payload)await processWebhookEvent(event)res.status(201).json({ received: true })})Flask webhook handler 5 collapsed linesimport jsonimport osfrom flask import Flask, jsonify, requestfrom scalekit import ScalekitClientapp = Flask(__name__)scalekit_client = ScalekitClient(env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],client_id=os.environ["SCALEKIT_CLIENT_ID"],client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],)@app.route("/webhooks/scalekit", methods=["POST"])def handle_webhook():secret = os.environ.get("SCALEKIT_WEBHOOK_SECRET")raw_body = request.get_data()headers = {"webhook-id": request.headers.get("webhook-id"),"webhook-timestamp": request.headers.get("webhook-timestamp"),"webhook-signature": request.headers.get("webhook-signature"),}try:scalekit_client.verify_webhook_payload(secret, headers, raw_body)except Exception:print("Invalid webhook signature")return jsonify({"error": "Invalid signature"}), 401event = json.loads(raw_body.decode("utf-8"))process_webhook_event(event)return jsonify({"received": True}), 201Gin webhook handler 11 collapsed linespackage mainimport ("encoding/json""io""net/http""os""github.com/gin-gonic/gin"scalekit "github.com/scalekit-inc/scalekit-sdk-go")var scalekitClient = scalekit.NewScalekitClient(os.Getenv("SCALEKIT_ENVIRONMENT_URL"),os.Getenv("SCALEKIT_CLIENT_ID"),os.Getenv("SCALEKIT_CLIENT_SECRET"),)func handleWebhook(c *gin.Context) {rawBody, err := io.ReadAll(c.Request.Body)if err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"})return}headers := map[string]string{"webhook-id": c.GetHeader("webhook-id"),"webhook-timestamp": c.GetHeader("webhook-timestamp"),"webhook-signature": c.GetHeader("webhook-signature"),}valid, err := scalekitClient.VerifyWebhookPayload(os.Getenv("SCALEKIT_WEBHOOK_SECRET"),headers,rawBody,)if err != nil || !valid {c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"})return}var event map[string]interface{}if err := json.Unmarshal(rawBody, &event); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})return}processWebhookEvent(event)c.JSON(http.StatusCreated, gin.H{"received": true})}Spring webhook handler 8 collapsed linesimport com.fasterxml.jackson.databind.ObjectMapper;import com.scalekit.ScalekitClient;import java.nio.charset.StandardCharsets;import java.util.HashMap;import java.util.Map;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestHeader;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class WebhookController {private final ScalekitClient scalekitClient;private final ObjectMapper objectMapper = new ObjectMapper();@PostMapping("/webhooks/scalekit")public ResponseEntity<Map<String, Object>> handleWebhook(@RequestBody String rawBody,@RequestHeader Map<String, String> requestHeaders) {Map<String, String> headers = new HashMap<>();headers.put("webhook-id", requestHeaders.get("webhook-id"));headers.put("webhook-timestamp", requestHeaders.get("webhook-timestamp"));headers.put("webhook-signature", requestHeaders.get("webhook-signature"));try {scalekitClient.webhook().verifyWebhookPayload(System.getenv("SCALEKIT_WEBHOOK_SECRET"),headers,rawBody.getBytes(StandardCharsets.UTF_8));} catch (Exception error) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", "Invalid signature"));}try {Map<String, Object> event = objectMapper.readValue(rawBody, Map.class);processWebhookEvent(event);return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("received", true));} catch (Exception error) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", "Webhook processing failed"));}}} -
Process webhook events
Section titled “Process webhook events”Handle different event types based on your application’s needs.
Process webhook events async function processWebhookEvent(event) {console.log(`Processing event: ${event.type}`);switch (event.type) {case 'user.organization_membership_created':// A user joined an organization. data has the user and the organization.await handleMemberAdded(event.data.user, event.data.organization);break;case 'user.organization_membership_deleted':// A user left an organizationawait revokeAccess(event.data.user.id, event.data.organization.id);break;case 'organization.created':// data is the organizationawait handleOrganizationCreated(event.data);break;default:console.log(`Unhandled event type: ${event.type}`);}}async function handleMemberAdded(user, organization) {// Use case: Sync the user to your database, send a welcome email, set up defaultsconsole.log(`New member: ${user.email} in org: ${organization.display_name}`);// Sync to your databaseawait syncUserToDatabase(user, organization);// Send a welcome email. Profile fields are in user.user_profile.await sendWelcomeEmail(user.email, user.user_profile.given_name);// Set up the user's workspace or default settingsawait setupUserDefaults(user.id, organization.id);}Process webhook events def process_webhook_event(event):print(f'Processing event: {event["type"]}')event_type = event['type']event_data = event['data']if event_type == 'user.organization_membership_created':# A user joined an organization. data has the user and the organization.handle_member_added(event_data['user'], event_data['organization'])elif event_type == 'user.organization_membership_deleted':# A user left an organizationrevoke_access(event_data['user']['id'], event_data['organization']['id'])elif event_type == 'organization.created':# data is the organizationhandle_organization_created(event_data)else:print(f'Unhandled event type: {event_type}')def handle_member_added(user, organization):# Use case: Sync the user to your database, send a welcome email, set up defaultsprint(f'New member: {user["email"]} in org: {organization["display_name"]}')# Sync to your databasesync_user_to_database(user, organization)# Send a welcome email. Profile fields are in user['user_profile'].send_welcome_email(user['email'], user['user_profile']['given_name'])# Set up the user's workspace or default settingssetup_user_defaults(user['id'], organization['id'])Process webhook events func processWebhookEvent(event map[string]interface{}) {eventType := event["type"].(string)eventData := event["data"].(map[string]interface{})fmt.Printf("Processing event: %s\n", eventType)switch eventType {case "user.organization_membership_created":// A user joined an organization. data has the user and the organization.user := eventData["user"].(map[string]interface{})organization := eventData["organization"].(map[string]interface{})handleMemberAdded(user, organization)case "user.organization_membership_deleted":// A user left an organizationuser := eventData["user"].(map[string]interface{})organization := eventData["organization"].(map[string]interface{})revokeAccess(user["id"].(string), organization["id"].(string))case "organization.created":// data is the organizationhandleOrganizationCreated(eventData)default:fmt.Printf("Unhandled event type: %s\n", eventType)}}func handleMemberAdded(user, organization map[string]interface{}) {// Use case: Sync the user to your database, send a welcome email, set up defaultsfmt.Printf("New member: %s in org: %s\n",user["email"], organization["display_name"])// Sync to your databasesyncUserToDatabase(user, organization)// Send a welcome email. Profile fields are in user["user_profile"].profile := user["user_profile"].(map[string]interface{})sendWelcomeEmail(user["email"].(string), profile["given_name"].(string))// Set up the user's workspace or default settingssetupUserDefaults(user["id"].(string), organization["id"].(string))}Process webhook events private void processWebhookEvent(Map<String, Object> event) {String eventType = (String) event.get("type");Map<String, Object> eventData = (Map<String, Object>) event.get("data");System.out.println("Processing event: " + eventType);switch (eventType) {case "user.organization_membership_created": {// A user joined an organization. data has the user and the organization.Map<String, Object> user = (Map<String, Object>) eventData.get("user");Map<String, Object> organization = (Map<String, Object>) eventData.get("organization");handleMemberAdded(user, organization);break;}case "user.organization_membership_deleted": {// A user left an organizationMap<String, Object> user = (Map<String, Object>) eventData.get("user");Map<String, Object> organization = (Map<String, Object>) eventData.get("organization");revokeAccess((String) user.get("id"), (String) organization.get("id"));break;}case "organization.created":// data is the organizationhandleOrganizationCreated(eventData);break;default:System.out.println("Unhandled event type: " + eventType);}}private void handleMemberAdded(Map<String, Object> user, Map<String, Object> organization) {// Use case: Sync the user to your database, send a welcome email, set up defaultsSystem.out.println("New member: " + user.get("email") +" in org: " + organization.get("display_name"));// Sync to your databasesyncUserToDatabase(user, organization);// Send a welcome email. Profile fields are in user_profile.Map<String, Object> profile = (Map<String, Object>) user.get("user_profile");sendWelcomeEmail((String) user.get("email"), (String) profile.get("given_name"));// Set up the user's workspace or default settingssetupUserDefaults((String) user.get("id"), (String) organization.get("id"));} -
Verify with the three webhook headers
Section titled “Verify with the three webhook headers”Call the SDK verify method before you parse JSON. Pass the dashboard signing secret (
whsec_…), the raw body, and these headers:Header Role webhook-idMessage ID in the HMAC. Same value on every retry webhook-timestampUnix seconds. The SDK rejects timestamps more than 5 minutes in the past or future webhook-signatureSpace-separated v1,<base64>valuesLanguage Method Node.js scalekit.verifyWebhookPayload(secret, headers, payload)Python scalekit_client.verify_webhook_payload(secret, headers, payload)Go scalekitClient.VerifyWebhookPayload(secret, headers, payload)Java scalekitClient.webhook().verifyWebhookPayload(secret, headers, payload)Node, Python, and Java throw when the signature is invalid. Go returns
(false, error).There is no
webhooks.verifySignaturemethod and noscalekit-signatureheader.
Respond to webhook event
Section titled “Respond to webhook event”Scalekit expects specific HTTP status codes in response to webhook deliveries. Return appropriate status codes to control retry behavior.
-
Return success responses
Section titled “Return success responses”Return success status codes when webhooks are processed successfully.
Status Code Description 200 OKWebhook processed successfully 201 CreatedRecommendedWebhook processed and resource created 202 AcceptedWebhook accepted for asynchronous processing -
Handle error responses
Section titled “Handle error responses”Return error status codes to indicate processing failures.
Status Code Description 400 Bad RequestInvalid payload or malformed request 401 UnauthorizedInvalid webhook signature 403 ForbiddenWebhook not authorized 422 Unprocessable EntityValid request but cannot process 500 Internal Server ErrorServer error during processing
Testing webhooks
Section titled “Testing webhooks”Test your webhook implementation locally before deploying to production.
Use ngrok to expose your local development server for webhook testing.
# Install ngroknpm install -g ngrok
# Start your local servernpm run dev
# In another terminal, expose your local serverngrok http 3000
# Use the ngrok URL in your Scalekit dashboard# Example: https://abc123.ngrok.io/webhooks/scalekitCommon webhook use cases
Section titled “Common webhook use cases”Webhooks enable common integration patterns:
- User lifecycle management: Sync user data across systems, provision accounts in downstream services, and trigger onboarding workflows when users sign up or update their profiles
- Organization and membership management: Set up workspaces when organizations are created, update user access when they join or leave organizations, and provision organization-specific resources
- Authentication monitoring: Track login patterns, update last-seen timestamps, and trigger security alerts for suspicious activity
Webhook event reference
Section titled “Webhook event reference”You now have a complete webhook implementation that can reliably process authentication events from Scalekit. Consider these additional improvements: