> **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/)

---

# Following webhook best practices

Webhooks are HTTP endpoints that you register with a system, allowing that system to inform your application about events by sending HTTP POST requests with event information in the body.

Developers register their applications' webhook endpoints with Scalekit to listen to events from the directory providers of their enterprise customers. Here are some common best practices developers follow to ensure their apps are secure and performant:

## Subscribe only to relevant events

While you can listen to all events from Scalekit, it's best to subscribe only to the events your app needs. This approach has several benefits:

- Your app doesn't have to process every event
- You can avoid overloading a single execution context by handling every event type

## Verify webhook signatures

Scalekit sends POST requests to your registered webhook endpoint. To ensure the request is coming from Scalekit and not a malicious actor, you should verify the request using the signing secret found in the Scalekit dashboard > Webhook > _Any Endpoint_.

Here's an example of how to verify webhooks using the Svix library:

```javascript
app.post('/webhook', async (req, res) => {
  // Parse the JSON body of the request
  const event = await req.json();

  // Get headers from the request
  const headers = req.headers;

  // Secret from Scalekit dashboard > Webhooks
  const secret = process.env.SCALEKIT_WEBHOOK_SECRET;

  try {
    // Verify the webhook payload
    await scalekit.verifyWebhookPayload(secret, headers, event);
  } catch (error) {
    return res.status(400).json({
      error: 'Invalid signature',
    });
  }
});
```
```python
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/webhook")
async def api_webhook(request: Request):
    # Get request data
    body = await request.body()

    # Extract webhook headers
    headers = {
        'webhook-id': request.headers.get('webhook-id'),
        'webhook-signature': request.headers.get('webhook-signature'),
        'webhook-timestamp': request.headers.get('webhook-timestamp')
    }

    # Verify webhook signature
    is_valid = scalekit.verify_webhook_payload(
        secret='<secret>',
        headers=headers,
        payload=body
    )
    print(is_valid)

    return JSONResponse(
        status_code=201,
        content=''
    )
```
```go
mux.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) {
    webhookSecret := os.Getenv("SCALEKIT_WEBHOOK_SECRET")

    // Read request body
    bodyBytes, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Prepare 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
    _, err = sc.VerifyWebhookPayload(
        webhookSecret,
        headers,
        bodyBytes
    )
    if err != nil {
        http.Error(w, err.Error(), http.StatusUnauthorized)
        return
    }
})
```
```java
@PostMapping("/webhook")
public String webhook(@RequestBody String body, @RequestHeader Map<String, String> headers) {
  String secret = "<WEBHOOK SECRET>";

  // Verify webhook signature
  boolean valid = scalekit.webhook().verifyWebhookPayload(secret, headers, body.getBytes());

  if (!valid) {
    return "error";
  }

  ObjectMapper mapper = new ObjectMapper();

  try {
    // Parse event data
    JsonNode node = mapper.readTree(body);
    String eventType = node.get("type").asText();
    JsonNode data = node.get("data");

    // Handle different event types
    switch (eventType) {
      case "organization.directory.user_created":
        handleUserCreate(data);
        break;
      case "organization.directory.user_updated":
        handleUserUpdate(data);
        break;
      default:
        System.out.println("Unhandled event type: " + eventType);
    }
  } catch (IOException e) {
    return "error";
  }

  return "ok";
}
```
## Check the event type before processing

Make sure to check the event.type before consuming the data received by the webhook endpoint. This ensures that your application relies on accurate information, even if more events are added in the future.

```javascript
app.post('/webhook', async (req, res) => {
  const event = req.body;

  // Handle different event types
  switch (event.type) {
    case 'organization.directory.user_created':
      const { email, name } = event.data;
      await createUserAccount(email, name);
      break;

    case 'organization.directory.user_updated':
      await updateUserAccount(event.data);
      break;

    default:
      console.log('Unhandled event type:', event.type);
  }

  return res.status(201).json({
    status: 'success',
  });
});

async function createUserAccount(email, name) {
  // Implement your user creation logic
}
```
```python
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/webhook")
async def api_webhook(request: Request):
    # Parse request body
    body = await request.body()
    payload = json.loads(body.decode())
    event_type = payload['type']

    # Handle different event types
    match event_type:
        case 'organization.directory.user_created':
            await handle_user_create(payload['data'])
        case 'organization.directory.user_updated':
            await handle_user_update(payload['data'])
        case _:
            print('Unhandled event type:', event_type)

    return JSONResponse(
        status_code=201,
        content={'status': 'success'}
    )
```
```go
mux.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) {
    // Read and verify webhook payload
    bodyBytes, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Parse event data
    var event map[string]interface{}
    err = json.Unmarshal(bodyBytes, &event)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Handle different event types
    eventType := event["type"]
    switch eventType {
    case "organization.directory.user_created":
        handleUserCreate(event["data"])
    case "organization.directory.user_updated":
        handleUserUpdate(event["data"])
    default:
        fmt.Println("Unhandled event type:", eventType)
    }

    w.WriteHeader(http.StatusOK)
})
```
```java
@PostMapping("/webhook")
public String webhook(@RequestBody String body, @RequestHeader Map<String, String> headers) {
  // Verify webhook signature first
  String secret = "<WEBHOOK_SECRET>";
  if (!verifyWebhookSignature(secret, headers, body)) {
    return "error";
  }

  try {
    // Parse event data
    ObjectMapper mapper = new ObjectMapper();
    JsonNode node = mapper.readTree(body);
    String eventType = node.get("type").asText();
    JsonNode data = node.get("data");

    // Handle different event types
    switch (eventType) {
      case "organization.directory.user_created":
        handleUserCreate(data);
        break;
      case "organization.directory.user_updated":
        handleUserUpdate(data);
        break;
      default:
        System.out.println("Unhandled event type: " + eventType);
    }
  } catch (IOException e) {
    return "error";
  }

  return "ok";
}
```
## Avoid webhook timeouts

To avoid unnecessary timeouts, respond to the webhook trigger with a response code of 201 and process the event asynchronously.

By following these best practices, you can ensure that your application effectively handles events from Scalekit, maintaining optimal performance and security.

## Do not ignore errors

Do not overlook repeated 4xx and 5xx error codes. Instead, verify that your API interactions are correct. For instance, if an endpoint expects a string but receives a numeric value, a validation error should occur. Likewise, trying to access an unauthorized or nonexistent endpoint will trigger a 4xx error.

## Advanced signature verification

While using the Scalekit SDK is recommended for webhook signature verification, you can also verify signatures manually using HMAC-SHA256 libraries when the SDK isn't available for your language.

### Manual signature verification

```javascript title="Manual signature verification"
function verifySignatureManually(rawBody, signature, secret) {
  const crypto = require('crypto');

  // Extract timestamp and signature from header
  // Header format: "t=<timestamp>,v1=<signature>"
  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
  // Payload format: <timestamp>.<raw_body>
  const payload = `${timestamp}.${rawBody}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  // Compare signatures securely using timing-safe comparison
  // This prevents timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(receivedSignature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}
```

### Timestamp validation

Always validate the webhook timestamp to prevent replay attacks:

```javascript title="Timestamp validation"
function validateWebhookTimestamp(timestamp, toleranceSeconds = 300) {
  // Convert timestamp to milliseconds
  const webhookTime = parseInt(timestamp) * 1000;
  const currentTime = Date.now();
  const timeDifference = Math.abs(currentTime - webhookTime);

  // Reject webhooks older than tolerance period (default 5 minutes)
  if (timeDifference > toleranceSeconds * 1000) {
    throw new Error('Webhook timestamp too old or too far in future');
  }

  return true;
}
```

## Advanced error handling and reliability

Implement comprehensive error handling to ensure reliable webhook processing across various failure scenarios.

### Retry logic with exponential backoff

```javascript title="Retry with exponential backoff"
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,
          attempts: attempt,
          timestamp: new Date()
        });
        throw error;
      }

      // Wait before retry with exponential backoff
      // Attempt 1: 1s, Attempt 2: 2s, Attempt 3: 4s
      const waitTime = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }
  }
}
```

### Circuit breaker pattern

Prevent cascading failures by implementing a circuit breaker:

```javascript title="Circuit breaker for webhook processing"
class WebhookCircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.recoveryTimeout = options.recoveryTimeout || 60000; // 60 seconds
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failures = 0;
    this.nextAttempt = Date.now();
  }

  async execute(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      // Try to recover
      this.state = 'HALF_OPEN';
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failures++;
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.recoveryTimeout;
    }
  }
}

// Usage
const circuitBreaker = new WebhookCircuitBreaker({
  failureThreshold: 5,
  recoveryTimeout: 60000
});

async function handleWebhook(event) {
  try {
    await circuitBreaker.execute(async () => {
      return await processWebhookEvent(event);
    });
  } catch (error) {
    if (error.message === 'Circuit breaker is OPEN') {
      // Service is unhealthy, queue for later
      await queueForLater(event);
    }
    throw error;
  }
}
```

## Advanced testing strategies

### Webhook testing utilities

Create comprehensive testing utilities for your webhook handlers:

```javascript title="Webhook testing utilities"
// Test webhook handler with sample events
async function testWebhookHandler() {
  const sampleUserCreatedEvent = {
    spec_version: '1',
    id: 'evt_test_123',
    type: 'organization.directory.user_created',
    occurred_at: new Date().toISOString(),
    environment_id: 'env_test_123',
    organization_id: 'org_test_123',
    object: 'DirectoryUser',
    data: {
      id: 'diruser_test_123',
      organization_id: 'org_test_123',
      email: 'test@example.com',
      given_name: 'Test',
      family_name: 'User',
      active: true,
      groups: [],
      roles: []
    }
  };

  // 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 payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload);
  const signature = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payloadString}`)
    .digest('hex');

  return {
    'webhook-id': 'evt_test_' + Date.now(),
    'webhook-timestamp': timestamp.toString(),
    'webhook-signature': `t=${timestamp},v1=${signature}`
  };
}

// Integration test
async function testWebhookIntegration() {
  const testSecret = 'test_secret_key';
  const testEvent = {
    type: 'organization.directory.user_created',
    data: { /* test data */ }
  };

  const headers = createTestSignature(testEvent, testSecret);

  // Make request to your webhook endpoint
  const response = await fetch('http://localhost:3000/webhooks/manage-users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...headers
    },
    body: JSON.stringify(testEvent)
  });

  assert(response.status === 201, 'Expected 201 status');
  console.log('Integration test passed');
}
```

## Monitoring and debugging

### Webhook delivery monitoring

Track webhook processing metrics to identify issues and optimize performance:

```javascript title="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.organization_id,
    environment_id: event.environment_id,
    timestamp: new Date()
  });

  // Alert on processing time anomalies
  if (processingTime > 5000) { // 5 seconds
    await alertService.warn({
      message: 'Slow webhook processing detected',
      eventType: event.type,
      processingTime: processingTime,
      eventId: event.id
    });
  }

  // Alert on failures
  if (!success) {
    await alertService.error({
      message: 'Webhook processing failed',
      eventType: event.type,
      eventId: event.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,
      MAX(processing_time_ms) as max_processing_time,
      MIN(processing_time_ms) as min_processing_time
    FROM processed_webhooks
    WHERE processed_at > NOW() - INTERVAL 24 HOUR
    GROUP BY event_type
    ORDER BY total_events DESC
  `);

  res.json(stats);
});

// Real-time webhook monitoring
async function monitorWebhookHealth() {
  const recentFailures = await db.processed_webhooks.count({
    where: {
      status: 'failed',
      processed_at: {
        $gte: new Date(Date.now() - 5 * 60 * 1000) // Last 5 minutes
      }
    }
  });

  if (recentFailures > 10) {
    await alertService.critical({
      message: 'High webhook failure rate detected',
      failureCount: recentFailures,
      timeWindow: '5 minutes'
    });
  }
}

// Run health check every minute
setInterval(monitorWebhookHealth, 60000);
```

### Debugging webhook issues

```javascript title="Webhook debugging utilities"
// Detailed webhook logging
async function logWebhookDetails(event, context) {
  await db.webhook_logs.create({
    event_id: event.id,
    event_type: event.type,
    organization_id: event.organization_id,
    environment_id: event.environment_id,
    received_at: new Date(),
    headers: context.headers,
    payload: event,
    ip_address: context.ip,
    user_agent: context.userAgent
  });
}

// Webhook replay for debugging
async function replayWebhook(eventId) {
  // Retrieve original webhook from logs
  const webhookLog = await db.webhook_logs.findOne({
    event_id: eventId
  });

  if (!webhookLog) {
    throw new Error(`Webhook ${eventId} not found`);
  }

  // Replay the webhook
  console.log(`Replaying webhook ${eventId}`);
  await processWebhookEvent(webhookLog.payload);
  console.log(`Webhook ${eventId} replayed successfully`);
}

// Dead letter queue processor for failed webhooks
async function processDeadLetterQueue() {
  const failedWebhooks = await deadLetterQueue.getAll('failed_webhook');

  for (const item of failedWebhooks) {
    try {
      console.log(`Reprocessing failed webhook: ${item.event.id}`);
      await processWebhookEvent(item.event);

      // Remove from dead letter queue on success
      await deadLetterQueue.remove('failed_webhook', item.id);

    } catch (error) {
      console.error(`Failed to reprocess webhook ${item.event.id}:`, error);

      // Increment retry count
      item.retries = (item.retries || 0) + 1;

      if (item.retries >= 5) {
        // Move to permanent failure queue
        await permanentFailureQueue.add(item);
        await deadLetterQueue.remove('failed_webhook', item.id);
      }
    }
  }
}

// Run dead letter queue processor periodically
setInterval(processDeadLetterQueue, 5 * 60 * 1000); // Every 5 minutes
```

### Performance optimization

```javascript title="Webhook performance optimization"
// Batch processing for high-volume webhooks
class WebhookBatchProcessor {
  constructor(options = {}) {
    this.batchSize = options.batchSize || 100;
    this.flushInterval = options.flushInterval || 5000; // 5 seconds
    this.queue = [];
    this.timer = null;
  }

  add(event) {
    this.queue.push(event);

    if (this.queue.length >= this.batchSize) {
      this.flush();
    } else if (!this.timer) {
      this.timer = setTimeout(() => this.flush(), this.flushInterval);
    }
  }

  async flush() {
    if (this.queue.length === 0) return;

    const batch = this.queue.splice(0, this.batchSize);
    clearTimeout(this.timer);
    this.timer = null;

    try {
      await this.processBatch(batch);
    } catch (error) {
      console.error('Batch processing error:', error);
      // Re-queue failed items
      this.queue.unshift(...batch);
    }
  }

  async processBatch(events) {
    // Process multiple events efficiently
    await db.transaction(async (trx) => {
      // Bulk insert processed events
      await trx('processed_webhooks').insert(
        events.map(e => ({
          event_id: e.id,
          event_type: e.type,
          organization_id: e.organization_id,
          status: 'processing',
          received_at: new Date()
        }))
      );

      // Process events in parallel
      await Promise.all(events.map(e => this.processEvent(e, trx)));
    });
  }

  async processEvent(event, trx) {
    // Event-specific processing logic
    // Use transaction for atomicity
  }
}

// Usage
const batchProcessor = new WebhookBatchProcessor({
  batchSize: 100,
  flushInterval: 5000
});

app.post('/webhooks/manage-users', async (req, res) => {
  // Verify signature...
  const event = req.body;

  // Add to batch processor
  batchProcessor.add(event);

  // Respond immediately
  return res.status(201).json({ received: true });
});
```

By following these advanced best practices, you can build a robust, reliable, and performant webhook integration that handles high volumes of events while maintaining data consistency and security.

---

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