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 that triggered the event (e.g., "DirectoryUser", "Directory", "Connection"). "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 ID of the organization associated with the event. "type": "organization.directory.user_created", // The specific event type (e.g., "organization.directory.user_created"). "data": { // Event-specific payload containing details relevant to the event type. "user_id": "usr_123456789", "email": "user@example.com", "name": "John Doe" }}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 Settings > 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 from the dropdown:
- User events -
user.created,user.updated,user.deleted - Organization events -
organization.created,organization.updated - Authentication events -
session.created,session.expired - Membership events -
membership.created,membership.updated,membership.deleted
- 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.created':// Handle new user registrationawait handleUserCreated(event.data.user, event.data.organization);break;case 'user.updated':// Handle user profile updatesawait handleUserUpdated(event.data.user);break;case 'organization.created':// Handle new organization creationawait handleOrganizationCreated(event.data.organization);break;case 'membership.created':// Handle user joining organizationawait handleMembershipCreated(event.data.membership);break;default:console.log(`Unhandled event type: ${event.type}`);}}async function handleUserCreated(user, organization) {// Use case: Sync new user to your database, send welcome email, set up user workspaceconsole.log(`New user created: ${user.email} in org: ${organization.display_name}`);// Sync to your databaseawait syncUserToDatabase(user, organization);// Send welcome emailawait sendWelcomeEmail(user.email, user.first_name);// Set up user 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.created':# Handle new user registrationhandle_user_created(event_data['user'], event_data['organization'])elif event_type == 'user.updated':# Handle user profile updateshandle_user_updated(event_data['user'])elif event_type == 'organization.created':# Handle new organization creationhandle_organization_created(event_data['organization'])elif event_type == 'membership.created':# Handle user joining organizationhandle_membership_created(event_data['membership'])else:print(f'Unhandled event type: {event_type}')def handle_user_created(user, organization):# Use case: Sync new user to your database, send welcome email, set up user workspaceprint(f'New user created: {user["email"]} in org: {organization["display_name"]}')# Sync to your databasesync_user_to_database(user, organization)# Send welcome emailsend_welcome_email(user['email'], user['first_name'])# Set up user 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.created":// Handle new user registrationuser := eventData["user"].(map[string]interface{})organization := eventData["organization"].(map[string]interface{})handleUserCreated(user, organization)case "user.updated":// Handle user profile updatesuser := eventData["user"].(map[string]interface{})handleUserUpdated(user)case "organization.created":// Handle new organization creationorganization := eventData["organization"].(map[string]interface{})handleOrganizationCreated(organization)case "membership.created":// Handle user joining organizationmembership := eventData["membership"].(map[string]interface{})handleMembershipCreated(membership)default:fmt.Printf("Unhandled event type: %s\n", eventType)}}func handleUserCreated(user, organization map[string]interface{}) {// Use case: Sync new user to your database, send welcome email, set up user workspacefmt.Printf("New user created: %s in org: %s\n",user["email"], organization["display_name"])// Sync to your databasesyncUserToDatabase(user, organization)// Send welcome emailsendWelcomeEmail(user["email"].(string), user["first_name"].(string))// Set up user 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.created":// Handle new user registrationMap<String, Object> user = (Map<String, Object>) eventData.get("user");Map<String, Object> organization = (Map<String, Object>) eventData.get("organization");handleUserCreated(user, organization);break;case "user.updated":// Handle user profile updateshandleUserUpdated((Map<String, Object>) eventData.get("user"));break;case "organization.created":// Handle new organization creationhandleOrganizationCreated((Map<String, Object>) eventData.get("organization"));break;case "membership.created":// Handle user joining organizationhandleMembershipCreated((Map<String, Object>) eventData.get("membership"));break;default:System.out.println("Unhandled event type: " + eventType);}}private void handleUserCreated(Map<String, Object> user, Map<String, Object> organization) {// Use case: Sync new user to your database, send welcome email, set up user workspaceSystem.out.println("New user created: " + user.get("email") +" in org: " + organization.get("display_name"));// Sync to your databasesyncUserToDatabase(user, organization);// Send welcome emailsendWelcomeEmail((String) user.get("email"), (String) user.get("first_name"));// Set up user 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-idEvent id in the HMAC 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: