Skip to content

Process webhook events from Scalekit

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.

This guide shows you how to set up webhook endpoints, configure event subscriptions, and securely process webhook payloads in your application.

Review the webhook flow UserScalekitEvent QueueYour AppScalekit SDK Trigger event (login, signup, etc.) Queue webhook event POST webhook payload Verify webhook signature Process event data Return 201 OK response
Example webhook payload
User signup webhook payload
{
"id": "evt_1234567890abcdef",
"type": "user.created",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"user": {
"id": "usr_9876543210fedcba",
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"email_verified": true,
"created_at": "2024-01-15T10:30:00Z"
},
"organization": {
"id": "org_abcdef1234567890",
"display_name": "Example Corp",
"external_id": "ext_corp_123"
}
},
"environment_id": "env_1a2b3c4d5e6f7g8h"
}

Set up webhook endpoints and select which events you want to receive through the Scalekit dashboard.

  1. Access webhook settings

    In your Scalekit dashboard, navigate to Settings > Webhooks

  2. Add webhook endpoint

    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
  3. Select events

    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
  4. Configure security

    Copy the Signing Secret - you’ll use this to verify webhook authenticity in your application

  5. Test the endpoint

    Use the Send Test Event button to verify your endpoint is working correctly

Your webhook endpoint must respond with a 200 status code within 10 seconds to be considered successful. Failed deliveries are retried up to 3 times with exponential backoff.

Create secure webhook handlers in your application to process incoming events from Scalekit.

Express.js webhook handler
3 collapsed lines
import express from 'express';
import { Scalekit } from '@scalekit-sdk/node';
const app = express();
const scalekit = new Scalekit(/* your credentials */);
// Use raw body parser for webhook signature verification
app.use('/webhooks/scalekit', express.raw({ type: 'application/json' }));
app.post('/webhooks/scalekit', async (req, res) => {
try {
// Get webhook signature from headers
const signature = req.headers['scalekit-signature'];
const rawBody = req.body;
// Verify webhook signature using Scalekit SDK
const isValid = await scalekit.webhooks.verifySignature(
rawBody,
signature,
process.env.SCALEKIT_WEBHOOK_SECRET
);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse and process the webhook payload
const event = JSON.parse(rawBody.toString());
await processWebhookEvent(event);
// Always respond with 200 to acknowledge receipt
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Webhook processing failed' });
}
});
async function processWebhookEvent(event) {
console.log(`Processing event: ${event.type}`);
switch (event.type) {
case 'user.created':
// Handle new user registration
await handleUserCreated(event.data.user, event.data.organization);
break;
case 'user.updated':
// Handle user profile updates
await handleUserUpdated(event.data.user);
break;
case 'organization.created':
// Handle new organization creation
await handleOrganizationCreated(event.data.organization);
break;
case 'membership.created':
// Handle user joining organization
await 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 workspace
console.log(`New user created: ${user.email} in org: ${organization.display_name}`);
// Sync to your database
await syncUserToDatabase(user, organization);
// Send welcome email
await sendWelcomeEmail(user.email, user.first_name);
// Set up user workspace or default settings
await setupUserDefaults(user.id, organization.id);
}

Sync user data across systems

  • Update your user database when users sign up or update their profiles
  • Provision accounts in downstream systems
  • Trigger onboarding workflows for new users
User lifecycle webhook handlers
async function handleUserCreated(user, organization) {
// Sync to your user database
await db.users.create({
scalekit_id: user.id,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
organization_id: organization.id,
created_at: new Date()
});
// Trigger onboarding workflow
await onboardingService.startUserOnboarding(user.id);
// Provision external accounts
await externalServices.createUserAccounts(user, organization);
}
async function handleUserUpdated(user) {
// Update user profile in your database
await db.users.update(
{ scalekit_id: user.id },
{
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
updated_at: new Date()
}
);
// Sync changes to external systems
await externalServices.updateUserProfile(user);
}

Track organization changes and user memberships

  • Set up workspaces when new organizations are created in Scalekit
  • Update user access when they join or leave organizations
  • Provision organization-specific resources
Organization management webhook handlers
async function handleOrganizationCreated(organization) {
// Create organization workspace
await workspaceService.createOrganizationWorkspace({
scalekit_org_id: organization.id,
name: organization.display_name,
external_id: organization.external_id
});
// Set up organization defaults
await setupOrganizationDefaults(organization.id);
// Notify admin team
await notificationService.notifyNewOrganization(organization);
}
async function handleMembershipCreated(membership) {
// Grant user access to organization workspace
await accessService.grantOrganizationAccess(
membership.user_id,
membership.organization_id,
membership.roles
);
// Send welcome-to-organization email
await emailService.sendOrganizationWelcome(membership);
// Update billing if needed
await billingService.updateSeatCount(membership.organization_id);
}
async function handleMembershipDeleted(membership) {
// Revoke user access to organization workspace
await accessService.revokeOrganizationAccess(
membership.user_id,
membership.organization_id
);
// Archive user data if needed
await dataService.archiveUserOrganizationData(membership);
// Update billing
await billingService.updateSeatCount(membership.organization_id);
}

Track authentication events for security and analytics

  • Monitor login patterns and detect anomalies
  • Update last-seen timestamps
  • Trigger security alerts for suspicious activity
Authentication monitoring webhook handlers
async function handleSessionCreated(session) {
// Update user's last login timestamp
await db.users.update(
{ scalekit_id: session.user_id },
{
last_login_at: new Date(),
last_login_ip: session.ip_address
}
);
// Track login analytics
await analyticsService.trackUserLogin({
user_id: session.user_id,
organization_id: session.organization_id,
auth_method: session.auth_method,
location: session.location
});
// Check for suspicious activity
await securityService.checkLoginAnomaly(session);
}
async function handleSessionExpired(session) {
// Log session expiration
await auditService.logSessionExpired({
user_id: session.user_id,
session_id: session.id,
expired_at: new Date()
});
// Clean up session-specific resources
await cleanupSessionResources(session.id);
}

Always verify that webhooks come from Scalekit using the signature verification:

Signature verification best practices
async function verifyWebhookSignature(rawBody, signature, secret) {
try {
// Use Scalekit SDK for verification (recommended)
const isValid = await scalekit.webhooks.verifySignature(rawBody, signature, secret);
return isValid;
} catch (error) {
console.error('Signature verification failed:', error);
return false;
}
}
// Alternative manual verification (if SDK not available)
function verifySignatureManually(rawBody, signature, secret) {
const crypto = require('crypto');
// Extract timestamp and signature from header
const elements = signature.split(',');
const timestamp = elements.find(el => el.startsWith('t=')).substring(2);
const receivedSignature = elements.find(el => el.startsWith('v1=')).substring(3);
// Create expected signature
const payload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Compare signatures securely
return crypto.timingSafeEqual(
Buffer.from(receivedSignature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}

Implement robust error handling to ensure reliable webhook processing:

Robust webhook error handling
app.post('/webhooks/scalekit', async (req, res) => {
try {
// Verify signature first
const isValid = await verifyWebhookSignature(
req.body,
req.headers['scalekit-signature'],
process.env.SCALEKIT_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
// Process webhook with timeout and retry logic
await Promise.race([
processWebhookWithRetry(event),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Processing timeout')), 8000)
)
]);
// Always respond quickly to prevent retries
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
// Log error for debugging but still return 200 if it's a business logic error
// Only return 500 for critical infrastructure errors
if (error.name === 'ValidationError' || error.name === 'BusinessLogicError') {
res.status(200).json({ received: true, warning: error.message });
} else {
res.status(500).json({ error: 'Critical processing error' });
}
}
});
async function processWebhookWithRetry(event, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await processWebhookEvent(event);
return; // Success, exit retry loop
} catch (error) {
console.error(`Webhook processing attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
// Final attempt failed - log to dead letter queue
await deadLetterQueue.add('failed_webhook', { event, error: error.message });
throw error;
}
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
}

Ensure your webhook handlers are idempotent to handle duplicate deliveries:

Idempotent webhook processing
async function processWebhookEvent(event) {
// Check if we've already processed this event
const existingEvent = await db.processed_webhooks.findOne({
event_id: event.id
});
if (existingEvent) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}
// Start a database transaction for atomic processing
const transaction = await db.beginTransaction();
try {
// Record that we're processing this event
await db.processed_webhooks.create({
event_id: event.id,
event_type: event.type,
processed_at: new Date(),
status: 'processing'
}, { transaction });
// Process the actual event
await handleSpecificEvent(event, transaction);
// Mark as completed
await db.processed_webhooks.update(
{ event_id: event.id },
{ status: 'completed', completed_at: new Date() },
{ transaction }
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
// Mark as failed
await db.processed_webhooks.update(
{ event_id: event.id },
{ status: 'failed', error_message: error.message }
);
throw error;
}
}

Test webhooks locally using ngrok to expose your development server:

Set up local webhook testing
# Install ngrok
npm install -g ngrok
# Start your local server
npm run dev
# In another terminal, expose your local server
ngrok http 3000
# Use the ngrok URL in your Scalekit dashboard
# Example: https://abc123.ngrok.io/webhooks/scalekit

Create utilities to test your webhook handlers:

Webhook testing utilities
// Test webhook handler with sample events
async function testWebhookHandler() {
const sampleUserCreatedEvent = {
id: 'evt_test_123',
type: 'user.created',
created_at: new Date().toISOString(),
data: {
user: {
id: 'usr_test_123',
email: 'test@example.com',
first_name: 'Test',
last_name: 'User',
email_verified: true
},
organization: {
id: 'org_test_123',
display_name: 'Test Organization'
}
}
};
// Test your webhook processing
await processWebhookEvent(sampleUserCreatedEvent);
console.log('Test webhook processed successfully');
}
// Mock webhook signature for testing
function createTestSignature(payload, secret) {
const crypto = require('crypto');
const timestamp = Math.floor(Date.now() / 1000);
const signature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return `t=${timestamp},v1=${signature}`;
}

Monitor webhook delivery success and failures:

Webhook monitoring
// Track webhook processing metrics
async function trackWebhookMetrics(event, processingTime, success) {
await metricsService.record('webhook_processed', {
event_type: event.type,
processing_time_ms: processingTime,
success: success,
organization_id: event.data.organization?.id
});
}
// Dashboard endpoint to view webhook statistics
app.get('/admin/webhook-stats', async (req, res) => {
const stats = await db.query(`
SELECT
event_type,
COUNT(*) as total_events,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successful,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
AVG(processing_time_ms) as avg_processing_time
FROM processed_webhooks
WHERE processed_at > NOW() - INTERVAL 24 HOUR
GROUP BY event_type
`);
res.json(stats);
});

You now have a complete webhook implementation that can reliably process authentication events from Scalekit. Consider these additional improvements: