> **Building with AI coding agents?** If you're using an AI coding agent, install the official Scalekit plugin. It gives your agent full awareness of the Scalekit API — reducing hallucinations and enabling faster, more accurate code generation.
>
> - **Claude Code**: `/plugin marketplace add scalekit-inc/claude-code-authstack` then `/plugin install <auth-type>@scalekit-auth-stack`
> - **GitHub Copilot CLI**: `copilot plugin marketplace add scalekit-inc/github-copilot-authstack` then `copilot plugin install <auth-type>@scalekit-auth-stack`
> - **Codex**: run the bash installer, restart, then open Plugin Directory and enable `<auth-type>`
> - **Skills CLI** (Windsurf, Cline, 40+ agents): `npx skills add scalekit-inc/skills --list` then `--skill <skill-name>`
>
> `<auth-type>` / `<skill-name>`: `agent-auth`, `full-stack-auth`, `mcp-auth`, `modular-sso`, `modular-scim` — [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Handle webhook events in your application

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.

<br />

```d2
shape: sequence_diagram

User -> Scalekit: Trigger event (login, signup, directory sync)
Scalekit -> Scalekit: Queue webhook event
Scalekit -> Your App: POST webhook payload
Your App <> Scalekit SDK: Verify webhook signature
Your App -> Your App: Process event data
Your App -> Scalekit: Return success response (200/201)
```

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

All webhook payloads follow a standardized structure with metadata and event-specific data in the `data` field.

```json title="User created event payload" showLineNumbers=false
{
  "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

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

1. In your Scalekit dashboard, navigate to **Settings** > **Webhooks**

2. 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. 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`
   <br />
4. Copy the **Signing Secret** - you'll use this to verify webhook authenticity in your application

5. Use the **Send Test Event** button to verify your endpoint is working correctly
**Webhook response requirements:** Your webhook endpoint should respond with a `201` status code within 10 seconds to be considered successful. Failed deliveries are retried up to 3 times with exponential backoff.

## Implement webhook handlers

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

1. ### Set up webhook endpoint

   Create an HTTP POST endpoint in your application to receive webhook payloads from Scalekit.

   ```javascript title="Express.js webhook handler" wrap collapse={1-3} {7-8,12-15,19-22,26-29}
   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' });
     }
   });
   ```

   ```python title="Flask webhook handler" wrap collapse={1-4} {8-9,13-16,20-23,27-30}
   from flask import Flask, request, jsonify
   import json
   from scalekit import ScalekitClient

   app = Flask(__name__)
   scalekit_client = ScalekitClient(/* your credentials */)

   @app.route('/webhooks/scalekit', methods=['POST'])
   def handle_webhook():
       try:
           # Get webhook signature from headers
           signature = request.headers.get('scalekit-signature')
           raw_body = request.get_data()

           # Verify webhook signature using Scalekit SDK
           is_valid = scalekit_client.webhooks.verify_signature(
               raw_body,
               signature,
               os.environ.get('SCALEKIT_WEBHOOK_SECRET')
           )

           if not is_valid:
               print('Invalid webhook signature')
               return jsonify({'error': 'Invalid signature'}), 401

           # Parse and process the webhook payload
           event = json.loads(raw_body.decode('utf-8'))
           process_webhook_event(event)

           # Always respond with 200 to acknowledge receipt
           return jsonify({'received': True}), 200

       except Exception as error:
           print(f'Webhook processing error: {error}')
           return jsonify({'error': 'Webhook processing failed'}), 500
   ```

   ```go title="Gin webhook handler" wrap collapse={1-8} {12-13,17-20,24-27,31-34}
   package main

   import (
       "encoding/json"
       "io"
       "net/http"
       "github.com/gin-gonic/gin"
       "github.com/scalekit-inc/scalekit-sdk-go"
   )

   scalekitClient := scalekit.NewScalekitClient(/* your credentials */)

   func handleWebhook(c *gin.Context) {
       // Get webhook signature from headers
       signature := c.GetHeader("scalekit-signature")

       // Read raw body
       rawBody, err := io.ReadAll(c.Request.Body)
       if err != nil {
           c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"})
           return
       }

       // Verify webhook signature using Scalekit SDK
       isValid, err := scalekitClient.Webhooks.VerifySignature(
           rawBody,
           signature,
           os.Getenv("SCALEKIT_WEBHOOK_SECRET"),
       )

       if err != nil || !isValid {
           c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"})
           return
       }

       // Parse and process the webhook payload
       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)

       // Always respond with 200 to acknowledge receipt
       c.JSON(http.StatusOK, gin.H{"received": true})
   }
   ```

   ```java title="Spring webhook handler" wrap collapse={1-8} {12-15,19-22,26-29,33-36}
   import org.springframework.web.bind.annotation.*;
   import org.springframework.http.ResponseEntity;
   import org.springframework.http.HttpStatus;
   import com.scalekit.ScalekitClient;
   import com.fasterxml.jackson.databind.ObjectMapper;
   import javax.servlet.http.HttpServletRequest;
   import java.io.IOException;

   @RestController
   public class WebhookController {

       private final ScalekitClient scalekitClient;
       private final ObjectMapper objectMapper = new ObjectMapper();

       @PostMapping("/webhooks/scalekit")
       public ResponseEntity<Map<String, Object>> handleWebhook(
           HttpServletRequest request,
           @RequestBody String rawBody
       ) {
           try {
               // Get webhook signature from headers
               String signature = request.getHeader("scalekit-signature");

               // Verify webhook signature using Scalekit SDK
               boolean isValid = scalekitClient.webhooks().verifySignature(
                   rawBody.getBytes(),
                   signature,
                   System.getenv("SCALEKIT_WEBHOOK_SECRET")
               );

               if (!isValid) {
                   return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                       .body(Map.of("error", "Invalid signature"));
               }

               // Parse and process the webhook payload
               Map<String, Object> event = objectMapper.readValue(rawBody, Map.class);
               processWebhookEvent(event);

               // Always respond with 200 to acknowledge receipt
               return ResponseEntity.ok(Map.of("received", true));

           } catch (Exception error) {
               return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                   .body(Map.of("error", "Webhook processing failed"));
           }
       }
   }
   ```

2. ### Process webhook events

   Handle different event types based on your application's needs.

   ```javascript title="Process webhook events"
   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);
   }
   ```

   ```python title="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 registration
           handle_user_created(event_data['user'], event_data['organization'])
       elif event_type == 'user.updated':
           # Handle user profile updates
           handle_user_updated(event_data['user'])
       elif event_type == 'organization.created':
           # Handle new organization creation
           handle_organization_created(event_data['organization'])
       elif event_type == 'membership.created':
           # Handle user joining organization
           handle_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 workspace
       print(f'New user created: {user["email"]} in org: {organization["display_name"]}')

       # Sync to your database
       sync_user_to_database(user, organization)

       # Send welcome email
       send_welcome_email(user['email'], user['first_name'])

       # Set up user workspace or default settings
       setup_user_defaults(user['id'], organization['id'])
   ```

   ```go title="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 registration
           user := eventData["user"].(map[string]interface{})
           organization := eventData["organization"].(map[string]interface{})
           handleUserCreated(user, organization)

       case "user.updated":
           // Handle user profile updates
           user := eventData["user"].(map[string]interface{})
           handleUserUpdated(user)

       case "organization.created":
           // Handle new organization creation
           organization := eventData["organization"].(map[string]interface{})
           handleOrganizationCreated(organization)

       case "membership.created":
           // Handle user joining organization
           membership := 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 workspace
       fmt.Printf("New user created: %s in org: %s\n",
           user["email"], organization["display_name"])

       // Sync to your database
       syncUserToDatabase(user, organization)

       // Send welcome email
       sendWelcomeEmail(user["email"].(string), user["first_name"].(string))

       // Set up user workspace or default settings
       setupUserDefaults(user["id"].(string), organization["id"].(string))
   }
   ```

   ```java title="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 registration
               Map<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 updates
               handleUserUpdated((Map<String, Object>) eventData.get("user"));
               break;

           case "organization.created":
               // Handle new organization creation
               handleOrganizationCreated((Map<String, Object>) eventData.get("organization"));
               break;

           case "membership.created":
               // Handle user joining organization
               handleMembershipCreated((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 workspace
       System.out.println("New user created: " + user.get("email") +
           " in org: " + organization.get("display_name"));

       // Sync to your database
       syncUserToDatabase(user, organization);

       // Send welcome email
       sendWelcomeEmail((String) user.get("email"), (String) user.get("first_name"));

       // Set up user workspace or default settings
       setupUserDefaults((String) user.get("id"), (String) organization.get("id"));
   }
   ```

3. ### Verify webhook signature

   Use the Scalekit SDK to verify webhook signatures before processing events.

   ```javascript title="Signature verification"
   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;
     }
   }
   ```

   ```python title="Signature verification"
   def verify_webhook_signature(raw_body, signature, secret):
       try:
           # Use Scalekit SDK for verification (recommended)
           is_valid = scalekit_client.webhooks.verify_signature(raw_body, signature, secret)
           return is_valid

       except Exception as error:
           print(f'Signature verification failed: {error}')
           return False
   ```

   ```go title="Signature verification"
   func verifyWebhookSignature(rawBody []byte, signature string, secret string) (bool, error) {
       // Use Scalekit SDK for verification (recommended)
       isValid, err := scalekitClient.Webhooks.VerifySignature(rawBody, signature, secret)
       if err != nil {
           fmt.Printf("Signature verification failed: %v\n", err)
           return false, err
       }
       return isValid, nil
   }
   ```

   ```java title="Signature verification"
   private boolean verifyWebhookSignature(byte[] rawBody, String signature, String secret) {
       try {
           // Use Scalekit SDK for verification (recommended)
           boolean isValid = scalekitClient.webhooks().verifySignature(rawBody, signature, secret);
           return isValid;

       } catch (Exception error) {
           System.err.println("Signature verification failed: " + error.getMessage());
           return false;
       }
   }
   ```
**Caution:** Security: Always verify webhook signatures before processing events. This prevents unauthorized parties from triggering your webhook handlers with malicious payloads.

## Respond to webhook event

Scalekit expects specific HTTP status codes in response to webhook deliveries. Return appropriate status codes to control retry behavior.

1. ### Return success responses

   Return success status codes when webhooks are processed successfully.

   | Status Code | Description |
   |-------------|-------------|
   | `200 OK` | Webhook processed successfully |
   | `201 Created` Recommended | Webhook processed and resource created |
   | `202 Accepted` | Webhook accepted for asynchronous processing |

2. ### Handle error responses

   Return error status codes to indicate processing failures.

   | Status Code | Description |
   |-------------|-------------|
   | `400 Bad Request` | Invalid payload or malformed request |
   | `401 Unauthorized` | Invalid webhook signature |
   | `403 Forbidden` | Webhook not authorized |
   | `422 Unprocessable Entity` | Valid request but cannot process |
   | `500 Internal Server Error` | Server error during processing |
**Retry schedule:** Scalekit retries failed webhooks automatically using exponential backoff. Return appropriate error codes to avoid unnecessary retries.
   - **Initial retry**: Immediately after failure
   - **Subsequent retries**: 5 seconds, 30 seconds, 2 minutes, 5 minutes, 15 minutes
   - **Maximum attempts**: 6 total attempts over approximately 22 minutes
   - **Final failure**: Webhook marked as failed after all retries exhausted

   Webhook failures are logged in your Scalekit dashboard for monitoring and debugging.

## Testing webhooks

Test your webhook implementation locally before deploying to production.

Use **ngrok** to expose your local development server for webhook testing.

```bash title="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
```

## 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

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

[Organization events](/apis/#webhook/organizationcreated)
  [User events](/apis/#webhook/usersignup)
  [Directory events](/apis/#webhook/organizationdirectoryenabled)
  [SSO connection events](/apis/#webhook/organizationssocreated)
  [Role events](/apis/#webhook/rolecreated)
  [Permission events](/apis/#webhook/permissiondeleted)

---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
