This guide shows you how to automate user provisioning with SCIM using Scalekit’s Directory API and webhooks. You’ll learn to sync user data in real-time, create webhook endpoints for instant updates, and build automated provisioning workflows that keep your application’s user data synchronized with your customers’ directory providers.
See the SCIM provisioning in action Play
With SCIM Provisioning from Scalekit, you can:
Use webhooks to listen for events from your customers’ directory providers (e.g., user updates, group changes)
Use REST APIs to list users, groups, and directories on demand
Scalekit abstracts the complexities of various directory providers, giving you a single interface to automate user lifecycle management. This enables you to create accounts for new hires during onboarding, deactivate accounts when employees depart, and adjust access levels as employees change roles.
Review the SCIM provisioning sequence
SCIM Provisioning Sequence Directory Provider Scalekit Your App User/Group changes occur Webhook events sent (user_created, user_updated, etc.) Process webhook and verify signature Update local user database (create, update, deactivate) API calls for sync (list users, groups) Fetch latest data from directory provider
/plugin marketplace add scalekit-inc/claude-code-authstack
/plugin install modular-scim@scalekit-auth-stack
copilot plugin marketplace add scalekit-inc/github-copilot-authstack
copilot plugin install modular-scim@scalekit-auth-stack
npx skills add scalekit-inc/skills --skill implementing-scim-provisioning
Continue building with AI →
Scalekit’s directory API allows you to fetch information about users, groups, and directories associated with an organization on-demand. This approach is ideal for scheduled synchronization tasks, bulk data imports, or when you need to ensure your application’s user data matches the latest directory provider state.
Let’s explore how to use the Directory API to retrieve user and group data programmatically.
Before you begin, ensure that your organization has a directory set up in Scalekit .
Scalekit offers language-specific SDKs for fast SSO integration. Use the installation instructions below for your technology stack:
npm install @scalekit-sdk/node
pip install scalekit-sdk-python
go get -u github.com/scalekit-inc/scalekit-sdk-go
/* Gradle users - add the following to your dependencies in build file */
implementation " com.scalekit:scalekit-sdk-java:2.0.11 "
<!-- Maven users - add the following to your `pom.xml` -->
< groupId > com.scalekit </ groupId >
< artifactId > scalekit-sdk-java </ artifactId >
< version > 2.0.11 </ version >
Navigate to Dashboard > Developers > Settings > API Credentials to obtain your credentials. Store your credentials securely in environment variables:
# Get these values from Dashboard > Developers > Settings > API Credentials
SCALEKIT_ENVIRONMENT_URL = ' https://b2b-app-dev.scalekit.com '
SCALEKIT_CLIENT_ID = ' <CLIENT_ID_FROM_SCALEKIT_DASHBOARD> '
SCALEKIT_CLIENT_SECRET = ' <SECRET_FROM_SCALEKIT_DASHBOARD> '
Initialize the Scalekit client with your environment variables and make your first API call to list organizations.
# Security: Replace <ACCESS_TOKEN> with a valid access token from Scalekit
# This token authorizes your API requests to access organization data
# Use case: Verify API connectivity and test authentication
# Examples: Initial setup testing, debugging integration issues
curl -L " https:// $SCALEKIT_ENVIRONMENT_URL /api/v1/organizations?page_size=5 " \
-H " Authorization: Bearer <ACCESS_TOKEN> "
import { ScalekitClient } from ' @scalekit-sdk/node ' ;
// Initialize Scalekit client with environment variables
// Security: Always use environment variables for sensitive credentials
const scalekit = new ScalekitClient (
process . env . SCALEKIT_ENVIRONMENT_URL ,
process . env . SCALEKIT_CLIENT_ID ,
process . env . SCALEKIT_CLIENT_SECRET ,
// Use case: Retrieve organizations for bulk user provisioning workflows
// Examples: Multi-tenant applications, enterprise customer onboarding
const { organizations } = await scalekit . organization . listOrganization ({
console . log ( ` Organization name: ${ organizations [ 0 ] . display_name } ` ) ;
console . log ( ` Organization ID: ${ organizations [ 0 ] . id } ` ) ;
console . error ( ' Failed to list organizations: ' , error ) ;
// Handle error appropriately for your application
from scalekit import ScalekitClient
# Initialize the SDK client with environment variables
# Security: Use os.getenv() to securely access credentials
scalekit_client = ScalekitClient (
env_url = os . getenv ( " SCALEKIT_ENVIRONMENT_URL " ) ,
client_id = os . getenv ( " SCALEKIT_CLIENT_ID " ) ,
client_secret = os . getenv ( " SCALEKIT_CLIENT_SECRET " )
# Use case: Sync user data across multiple organizations
# Examples: Scheduled provisioning tasks, HR system integration
org_list = scalekit_client . organization . list_organizations ( page_size = 100 )
print ( f 'Organization details: { org_list [ 0 ] } ' )
print ( f 'Organization ID: { org_list [ 0 ] . id } ' )
except Exception as error :
print ( f 'Error listing organizations: { error } ' )
# Implement appropriate error handling for your use case
" github.com/scalekit/scalekit-go "
// Initialize Scalekit client with environment variables
// Security: Always load credentials from environment, not hardcoded
scalekitClient := scalekit . NewScalekitClient (
os . Getenv ( " SCALEKIT_ENVIRONMENT_URL " ) ,
os . Getenv ( " SCALEKIT_CLIENT_ID " ) ,
os . Getenv ( " SCALEKIT_CLIENT_SECRET " ) ,
// Use case: Get specific organization for directory sync operations
// Examples: Targeted user provisioning, organization-specific workflows
organization , err := scalekitClient . Organization . GetOrganization (
// Handle error appropriately for your application
return fmt . Errorf ( " failed to get organization: %w " , err )
import com . scalekit . ScalekitClient ;
// Initialize Scalekit client with environment variables
// Security: Use System.getenv() to securely access credentials
ScalekitClient scalekitClient = new ScalekitClient (
System . getenv ( " SCALEKIT_ENVIRONMENT_URL " ) ,
System . getenv ( " SCALEKIT_CLIENT_ID " ) ,
System . getenv ( " SCALEKIT_CLIENT_SECRET " )
// Use case: List organizations for automated provisioning workflows
// Examples: Enterprise customer setup, multi-tenant management
ListOrganizationsResponse organizations = scalekitClient . organizations ()
. listOrganizations ( 5 , "" ) ;
if ( ! organizations . getOrganizations () . isEmpty () ) {
Organization firstOrg = organizations . getOrganizations () . get ( 0 ) ;
System . out . println ( " Organization name: " + firstOrg . getDisplayName ()) ;
System . out . println ( " Organization ID: " + firstOrg . getId ()) ;
} catch ( ScalekitException error ) {
System . err . println ( " Failed to list organizations: " + error . getMessage ()) ;
// Implement appropriate error handling
After successfully listing organizations, you’ll need to retrieve the specific directory to begin syncing user and group data. You can retrieve directories using either the organization and directory IDs, or fetch the primary directory for an organization.
// Use case: Get specific directory when organization has multiple directories
// Examples: Department-specific provisioning, multi-division companies
const { directory } = await scalekit . directory . getDirectory ( ' <organization_id> ' , ' <directory_id> ' ) ;
console . log ( ` Directory name: ${ directory . name } ` ) ;
// Use case: Get primary directory for simple provisioning workflows
// Examples: Small organizations, single-directory setups
const { directory } = await scalekit . directory . getPrimaryDirectoryByOrganizationId ( ' <organization_id> ' ) ;
console . log ( ` Primary directory ID: ${ directory . id } ` ) ;
console . error ( ' Failed to retrieve directory: ' , error ) ;
// Handle error appropriately for your application
# Use case: Access specific directory for targeted user sync operations
# Examples: Regional offices, business unit-specific provisioning
directory = scalekit_client . directory . get_directory (
organization_id = ' <organization_id> ' , directory_id = ' <directory_id> '
print ( f 'Directory name: { directory . name } ' )
# Use case: Get primary directory for streamlined user management
# Examples: Standard employee provisioning, main company directory
primary_directory = scalekit_client . directory . get_primary_directory_by_organization_id (
organization_id = ' <organization_id> '
print ( f 'Primary directory ID: { primary_directory . id } ' )
except Exception as error :
print ( f 'Error retrieving directory: { error } ' )
# Implement appropriate error handling
// Use case: Retrieve specific directory for granular access control
// Examples: Multi-tenant environments, department-level provisioning
directory , err := scalekitClient . Directory () . GetDirectory ( ctx , organizationId , directoryId )
return fmt . Errorf ( " failed to get directory: %w " , err )
fmt . Printf ( " Directory name: %s \n " , directory . Name )
// Use case: Get primary directory for simplified user management
// Examples: Automated provisioning workflows, bulk user imports
directory , err := scalekitClient . Directory () . GetPrimaryDirectoryByOrganizationId ( ctx , organizationId )
return fmt . Errorf ( " failed to get primary directory: %w " , err )
fmt . Printf ( " Primary directory ID: %s \n " , directory . ID )
// Use case: Access specific directory for detailed user management
// Examples: Custom provisioning logic, directory-specific rules
Directory directory = scalekitClient . directories ()
. getDirectory ( " <directoryId> " , " <organizationId> " ) ;
System . out . println ( " Directory name: " + directory . getName ()) ;
// Use case: Get primary directory for standard provisioning workflows
// Examples: Employee onboarding, automated user sync
Directory primaryDirectory = scalekitClient . directories ()
. getPrimaryDirectoryByOrganizationId ( " <organizationId> " ) ;
System . out . println ( " Primary directory ID: " + primaryDirectory . getId ()) ;
} catch ( ScalekitException error ) {
System . err . println ( " Failed to retrieve directory: " + error . getMessage ()) ;
// Implement appropriate error handling
Once you have the directory information, you can fetch users within that directory. This is commonly used for bulk user synchronization and maintaining an up-to-date user database.
// Use case: Bulk user synchronization and provisioning
// Examples: New customer onboarding, scheduled user data sync
const { users } = await scalekit . directory . listDirectoryUsers ( ' <organization_id> ' , ' <directory_id> ' ) ;
// Process each user for provisioning or updates
console . log ( ` User email: ${ user . email } , Name: ${ user . name } ` ) ;
// TODO: Implement your user provisioning logic here
console . error ( ' Failed to list directory users: ' , error ) ;
// Handle error appropriately for your application
# Use case: Automated user provisioning workflows
# Examples: HR system integration, bulk user imports
directory_users = scalekit_client . directory . list_directory_users (
organization_id = ' <organization_id> ' , directory_id = ' <directory_id> '
# Process each user for local database updates
for user in directory_users :
print ( f 'User email: { user . email } , Name: { user . name } ' )
# TODO : Implement your user synchronization logic here
except Exception as error :
print ( f 'Error listing directory users: { error } ' )
# Implement appropriate error handling
// Configure pagination options for large user directories
options := & ListDirectoryUsersOptions {
PageSize : 50 , // Adjust based on your needs
// Use case: Paginated user retrieval for large directories
// Examples: Enterprise customer provisioning, regular sync jobs
directoryUsers , err := scalekitClient . Directory () . ListDirectoryUsers ( ctx , organizationId , directoryId , options )
return fmt . Errorf ( " failed to list directory users: %w " , err )
for _ , user := range directoryUsers . Users {
fmt . Printf ( " User email: %s , Name: %s \n " , user . Email , user . Name )
// TODO: Implement your user provisioning logic
// Configure options for user listing with pagination
var options = ListDirectoryResourceOptions . builder ()
. pageSize ( 50 ) // Adjust based on your requirements
. includeDetail ( true ) // Include detailed user information
// Use case: Enterprise user management and synchronization
// Examples: Scheduled sync tasks, user provisioning automation
ListDirectoryUsersResponse usersResponse = scalekitClient . directories ()
. listDirectoryUsers ( directory . getId () , organizationId , options ) ;
// Process each user for provisioning
for ( User user : usersResponse . getUsers () ) {
System . out . println ( " User email: " + user . getEmail () + " , Name: " + user . getName ()) ;
// TODO: Implement your user provisioning logic here
} catch ( ScalekitException error ) {
System . err . println ( " Failed to list directory users: " + error . getMessage ()) ;
// Implement appropriate error handling
Customer onboarding use case
When setting up a new customer account, use the listDirectoryUsers function to automatically connect to their directory and start syncing user data. This enables immediate user provisioning without manual user creation.
Groups are essential for implementing role-based access control (RBAC) in your application. After retrieving users, you can fetch groups to manage permissions and access levels based on organizational structure.
// Use case: Role-based access control implementation
// Examples: Department-level permissions, project-based access
const { groups } = await scalekit . directory . listDirectoryGroups (
// Process each group for RBAC setup
groups . forEach ( group => {
console . log ( ` Group name: ${ group . name } , ID: ${ group . id } ` ) ;
// TODO: Implement your group-based permission logic here
console . error ( ' Failed to list directory groups: ' , error ) ;
// Handle error appropriately for your application
# Use case: Department-based access control
# Examples: Engineering vs Sales permissions, project team access
directory_groups = scalekit_client . directory . list_directory_groups (
directory_id = ' <directory_id> ' , organization_id = ' <organization_id> '
# Process each group for permission mapping
for group in directory_groups :
print ( f 'Group name: { group . name } , ID: { group . id } ' )
# TODO : Implement your group-based permission logic here
except Exception as error :
print ( f 'Error listing directory groups: { error } ' )
# Implement appropriate error handling
// Configure pagination for group listing
options := & ListDirectoryGroupsOptions {
PageSize : 25 , // Adjust based on expected group count
// Use case: Organizational role management
// Examples: Enterprise role hierarchy, department-based access
directoryGroups , err := scalekitClient . Directory () . ListDirectoryGroups ( ctx , organizationId , directoryId , options )
return fmt . Errorf ( " failed to list directory groups: %w " , err )
// Process each group for RBAC implementation
for _ , group := range directoryGroups . Groups {
fmt . Printf ( " Group name: %s , ID: %s \n " , group . Name , group . ID )
// TODO: Implement your group-based permission logic
// Configure options for detailed group information
var options = ListDirectoryResourceOptions . builder ()
. pageSize ( 25 ) // Adjust based on your requirements
. includeDetail ( true ) // Include group membership details
// Use case: Enterprise permission management
// Examples: Role assignments, access level configurations
ListDirectoryGroupsResponse groupsResponse = scalekitClient . directories ()
. listDirectoryGroups ( directory . getId () , organizationId , options ) ;
// Process each group for permission mapping
for ( Group group : groupsResponse . getGroups () ) {
System . out . println ( " Group name: " + group . getName () + " , ID: " + group . getId ()) ;
// TODO: Implement your group-based permission logic here
} catch ( ScalekitException error ) {
System . err . println ( " Failed to list directory groups: " + error . getMessage ()) ;
// Implement appropriate error handling
Role-based access control
Use group information to implement role-based access control in your application. Map directory groups to application roles and permissions to automatically assign access levels based on a user’s organizational memberships.
Scalekit’s Directory API provides a simple way to fetch user and group information on-demand. Refer to our API reference to explore more capabilities.
While the Directory API is perfect for scheduled synchronization, webhooks enable immediate, real-time user provisioning. When directory providers send events to Scalekit, we forward them instantly to your application, allowing you to respond to user changes as they happen.
This approach is ideal for scenarios requiring immediate action, such as new employee onboarding or emergency access revocation.
Create a webhook endpoint to receive real-time events from directory providers. After implementing your endpoint, register it in Dashboard > Webhooks where you’ll receive a secret for payload verification.
Critical security requirement
Always verify webhook signatures before processing events. This prevents unauthorized parties from triggering your provisioning logic and protects against replay attacks.
app . post ( ' /webhook ' , async ( req , res ) => {
// Security: ALWAYS verify requests are from Scalekit before processing
// This prevents unauthorized parties from triggering your provisioning logic
const headers = req . headers ;
const secret = process . env . SCALEKIT_WEBHOOK_SECRET ;
// Verify webhook signature to prevent replay attacks and forged requests
await scalekit . verifyWebhookPayload ( secret , headers , event ) ;
console . error ( ' Webhook signature verification failed: ' , error ) ;
// Return 400 for invalid signatures - this prevents processing malicious requests
return res . status ( 400 ) . json ({ error : ' Invalid signature ' }) ;
// Use case: Real-time user provisioning based on directory events
// Examples: New hire onboarding, emergency access revocation, role changes
const { email , name } = event . data ;
// Process the webhook event based on its type
case ' organization.directory.user_created ' :
await createUserAccount ( email , name ) ;
case ' organization.directory.user_updated ' :
await updateUserAccount ( email , name ) ;
case ' organization.directory.user_deleted ' :
await deactivateUserAccount ( email ) ;
console . log ( ` Unhandled event type: ${ event . type } ` ) ;
res . status ( 201 ) . json ({ message : ' Webhook processed successfully ' }) ;
} catch ( processingError ) {
console . error ( ' Failed to process webhook event: ' , processingError ) ;
res . status ( 500 ) . json ({ error : ' Processing failed ' }) ;
from fastapi import FastAPI , Request , HTTPException
async def api_webhook ( request : Request ):
# Security: ALWAYS verify webhook signatures before processing events
# This prevents unauthorized webhook calls and replay attacks
headers = request . headers
body = await request . json ()
# Verify webhook payload using the secret from Scalekit dashboard
# Get this from Dashboard > Webhooks after registering your endpoint
is_valid = scalekit_client . verify_webhook_payload (
secret = os . getenv ( " SCALEKIT_WEBHOOK_SECRET " ) ,
payload = json . dumps ( body ) . encode ( ' utf-8 ' )
raise HTTPException ( status_code = 400 , detail = " Invalid webhook signature " )
except Exception as verification_error :
print ( f "Webhook verification failed: { verification_error } " )
raise HTTPException ( status_code = 400 , detail = " Webhook verification failed " )
# Use case: Instant user provisioning based on directory events
# Examples: Automated onboarding, immediate access revocation, role updates
event_type = body . get ( " type " )
event_data = body . get ( " data " , {} )
email = event_data . get ( " email " )
name = event_data . get ( " name " )
if event_type == " organization.directory.user_created " :
await create_user_account ( email , name )
elif event_type == " organization.directory.user_updated " :
await update_user_account ( email , name )
elif event_type == " organization.directory.user_deleted " :
await deactivate_user_account ( email )
return JSONResponse ( status_code = 201 , content ={ " status " : " processed " } )
except Exception as processing_error :
print ( f "Failed to process webhook: { processing_error } " )
raise HTTPException ( status_code = 500 , detail = " Event processing failed " )
public ResponseEntity < String > webhook (
@ RequestBody String body ,
@ RequestHeader Map < String , String > headers ) {
// Security: ALWAYS verify webhook signatures before processing
// This prevents malicious webhook calls and protects against replay attacks
String secret = System . getenv ( " SCALEKIT_WEBHOOK_SECRET " ) ;
// Verify webhook signature using Scalekit SDK
boolean isValid = scalekitClient . webhook ()
. verifyWebhookPayload ( secret , headers , body . getBytes ()) ;
return ResponseEntity . badRequest () . body ( " Invalid webhook signature " ) ;
} catch ( Exception verificationError ) {
System . err . println ( " Webhook verification failed: " + verificationError . getMessage ()) ;
return ResponseEntity . badRequest () . body ( " Webhook verification failed " ) ;
// Use case: Real-time user lifecycle management
// Examples: Employee onboarding, access termination, role modifications
ObjectMapper mapper = new ObjectMapper () ;
JsonNode rootNode = mapper . readTree ( body ) ;
String eventType = rootNode . get ( " type " ) . asText () ;
JsonNode data = rootNode . get ( " data " ) ;
case " organization.directory.user_created " :
String email = data . get ( " email " ) . asText () ;
String name = data . get ( " name " ) . asText () ;
createUserAccount ( email , name ) ;
case " organization.directory.user_updated " :
case " organization.directory.user_deleted " :
deactivateUserAccount ( data . get ( " email " ) . asText ()) ;
System . out . println ( " Unhandled event type: " + eventType ) ;
return ResponseEntity . status ( HttpStatus . CREATED ) . body ( " Webhook processed " ) ;
} catch ( Exception processingError ) {
System . err . println ( " Failed to process webhook event: " + processingError . getMessage ()) ;
return ResponseEntity . status ( HttpStatus . INTERNAL_SERVER_ERROR )
. body ( " Event processing failed " ) ;
// Security: Store webhook secret securely in environment variables
// Get this from Dashboard > Webhooks after registering your endpoint
webhookSecret := os . Getenv ( " SCALEKIT_WEBHOOK_SECRET " )
http . HandleFunc ( " /webhook " , func ( w http . ResponseWriter , r * http . Request ) {
// Security: ALWAYS verify webhook signatures before processing events
// This prevents unauthorized webhook calls and replay attacks
if r . Method != http . MethodPost {
http . Error ( w , " Method not allowed " , http . StatusMethodNotAllowed )
body , err := io . ReadAll ( r . Body )
http . Error ( w , err . Error () , http . StatusBadRequest )
// Extract webhook headers for verification
headers := map [ string ] string {
" webhook-id " : r . Header . Get ( " webhook-id " ) ,
" webhook-signature " : r . Header . Get ( " webhook-signature " ) ,
" webhook-timestamp " : r . Header . Get ( " webhook-timestamp " ) ,
// Verify webhook signature to prevent malicious requests
_ , err = scalekitClient . VerifyWebhookPayload ( webhookSecret , headers , body )
http . Error ( w , " Invalid webhook signature " , http . StatusBadRequest )
// Use case: Instant user provisioning and lifecycle management
// Examples: Real-time onboarding, emergency access revocation, role synchronization
var webhookEvent WebhookEvent
if err := json . Unmarshal ( body , & webhookEvent ) ; err != nil {
http . Error ( w , " Invalid webhook payload " , http . StatusBadRequest )
switch webhookEvent . Type {
case " organization.directory.user_created " :
err = createUserAccount ( webhookEvent . Data . Email , webhookEvent . Data . Name )
case " organization.directory.user_updated " :
err = updateUserAccount ( webhookEvent . Data )
case " organization.directory.user_deleted " :
err = deactivateUserAccount ( webhookEvent . Data . Email )
fmt . Printf ( " Unhandled event type: %s \n " , webhookEvent . Type )
http . Error ( w , " Failed to process webhook " , http . StatusInternalServerError )
w . WriteHeader ( http . StatusCreated )
w . Write ([] byte ( ` {"status": "processed"} ` ))
After implementing your secure webhook endpoint, register it in the Scalekit dashboard to start receiving events:
Navigate to Dashboard > Webhooks
Click +Add Endpoint
Enter your webhook endpoint URL (e.g., https://your-app.com/api/webhooks/scalekit)
Add a meaningful description for your reference
Select the event types you want to receive. Common choices include:
organization.directory.user_created - New user provisioning
organization.directory.user_updated - User profile changes
organization.directory.user_deleted - User deactivation
organization.directory.group_created - New group creation
organization.directory.group_updated - Group modifications
Once registered, your webhook endpoint will start receiving event payloads from directory providers in real-time.
Scalekit standardizes event payloads across different directory providers, ensuring consistent data structure regardless of whether your customers use Azure AD, Okta, Google Workspace, or other providers.
When directory changes occur, Scalekit sends events with the following structure:
" type " : " organization.directory.user_created " ,
" email " : " john.doe@company.com " ,
" organization_id " : " org_12345 " ,
" directory_id " : " dir_67890 "
" timestamp " : " 2024-01-15T10:30:00Z "
Webhook delivery and retry policy
Scalekit attempts webhook delivery using an exponential backoff retry policy until we receive a successful 200/201 response code from your servers:
Attempt Timing 1 Immediately 2 5 seconds 3 5 minutes 4 30 minutes 5 2 hours 6 5 hours 7 10 hours 8 10 hours
You have now successfully implemented and registered a webhook endpoint, enabling your application to receive real-time events for automated user provisioning. Your system can now respond instantly to directory changes, providing seamless user lifecycle management.
Refer to our webhook implementation guide for the complete list of available event types and payload structures.