API Documentation

Complete guide to integrating ComplianceLayer into your security workflow.

Webhooks

Receive real-time HTTP notifications when scans complete, alerts trigger, or security scores change. Webhooks enable automated workflows and integrations with your existing tools.

How Webhooks Work

When a monitored event occurs, ComplianceLayer sends an HTTP POST request to your configured endpoint with a JSON payload containing event details. Your endpoint should:

  1. Respond with a 200 OK status within 30 seconds
  2. Verify the HMAC signature in the X-ComplianceLayer-Signature header
  3. Process the event asynchronously (don't block the webhook response)

Configuring Webhooks

Set up webhooks from your dashboard:

  1. Navigate to Settings → Webhook Endpoints
  2. Click Add Webhook
  3. Enter your HTTPS endpoint URL
  4. Select which events to subscribe to
  5. Save and copy your signing secret (shown only once!)
HTTPS Required: Webhook endpoints must use HTTPS in production. HTTP is only allowed for local development (http://localhost).

Webhook Headers

All webhook requests include these headers for verification and debugging:

Content-Type: application/json
User-Agent: ComplianceLayer-Webhooks/1.0
X-ComplianceLayer-Signature: a1b2c3d4e5f6...
X-ComplianceLayer-Event: scan.completed
X-ComplianceLayer-Delivery: 12345
  • X-ComplianceLayer-Signature - HMAC-SHA256 signature (hex) for verifying authenticity
  • X-ComplianceLayer-Event - The event type being delivered
  • X-ComplianceLayer-Delivery - Unique delivery ID for tracking

Available Events

EVENTscan.completed

Triggered when a domain scan completes successfully. Includes score, grade, and issue counts.

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "scan.completed",
  "created_at": "2026-03-08T12:34:56.789Z",
  "data": {
    "scan_id": 12345,
    "job_id": 67890,
    "domain": "example.com",
    "score": 85,
    "grade": "B",
    "issues_count": 8,
    "critical_issues": 0,
    "high_issues": 2,
    "medium_issues": 3,
    "low_issues": 3,
    "scanned_at": "2026-03-08T12:34:56.789Z",
    "scan_duration_ms": 2847
  }
}
EVENTalert.triggered

Triggered when an alert condition is met, such as score drops, critical issues detected, or certificate expiration warnings.

{
  "id": "550e8400-e29b-41d4-a716-446655440001",
  "type": "alert.triggered",
  "created_at": "2026-03-08T12:34:56.789Z",
  "data": {
    "alert_id": 456,
    "alert_type": "score_drop",
    "severity": "high",
    "domain": "example.com",
    "title": "Security Score Dropped",
    "message": "Score decreased from 92 to 78 (-14 points)",
    "old_value": "92",
    "new_value": "78",
    "score": 78,
    "grade": "C+",
    "created_at": "2026-03-08T12:34:56.789Z"
  }
}

Alert Types: score_drop, critical_issue, cert_expired, cert_expiring_soon, config_change

Coming Soon: Additional events — score.changed, domain.added, and domain.removed — are planned for a future release.

Verifying Signatures

All webhook requests include an X-ComplianceLayer-Signature header containing an HMAC-SHA256 signature. Always verify this signature to ensure requests are authentic and from ComplianceLayer.

Security: The signature is computed using your webhook secret and the raw JSON payload. Store your secret securely and never commit it to version control.
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  // Compute HMAC-SHA256 of the JSON payload
  const computed = crypto
    .createHmac('sha256', secret)
    .update(payload)  // Raw JSON string, not parsed object
    .digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(computed)
  );
}

// Express.js example
const express = require('express');
const app = express();

// IMPORTANT: Use express.raw() to get raw body for signature verification
app.post('/webhooks/compliancelayer',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-compliancelayer-signature'];
    const secret = process.env.WEBHOOK_SECRET;
    const payload = req.body.toString('utf8');

    // Verify signature
    if (!verifyWebhook(payload, signature, secret)) {
      return res.status(401).send('Invalid signature');
    }

    // Parse payload after verification
    const event = JSON.parse(payload);

    // Respond immediately
    res.status(200).send('OK');

    // Process asynchronously
    processWebhook(event);
  }
);

async function processWebhook(event) {
  console.log(`Processing ${event.type}`, event.data);

  switch (event.type) {
    case 'scan.completed':
      await handleScanCompleted(event.data);
      break;
    case 'alert.triggered':
      await handleAlert(event.data);
      break;
  }
}

Handling Events

Process webhook events asynchronously to avoid timeouts. Return a 200 OK immediately, then handle the event in a background job:

const Queue = require('bull');
const webhookQueue = new Queue('webhooks');

// Add job to queue immediately
app.post('/webhooks/compliancelayer', async (req, res) => {
  // ... verify signature ...

  const event = JSON.parse(payload);

  // Add to queue for background processing
  await webhookQueue.add('process', event, {
    attempts: 3,
    backoff: { type: 'exponential', delay: 2000 }
  });

  // Respond immediately
  res.status(200).send('OK');
});

// Process jobs in background
webhookQueue.process('process', async (job) => {
  const event = job.data;

  switch (event.type) {
    case 'scan.completed':
      await handleScanCompleted(event.data);
      break;

    case 'alert.triggered':
      await handleAlert(event.data);
      break;
  }
});

async function handleScanCompleted(data) {
  const { domain, score, grade, critical_issues, high_issues } = data;

  // Create Jira ticket for low scores
  if (score < 70) {
    await jira.createIssue({
      project: 'SEC',
      summary: `Low security score for ${domain}: ${grade}`,
      description: `
        Domain: ${domain}
        Score: ${score}
        Grade: ${grade}
        Critical Issues: ${critical_issues}
        High Issues: ${high_issues}
      `,
      issuetype: 'Bug',
      priority: 'High'
    });
  }

  // Post to Slack
  await slack.postMessage({
    channel: '#security',
    text: `✅ Scan completed for *${domain}*: ${grade} (${score}/100)`,
    attachments: [{
      color: score >= 80 ? 'good' : score >= 60 ? 'warning' : 'danger',
      fields: [
        { title: 'Critical', value: critical_issues, short: true },
        { title: 'High', value: high_issues, short: true }
      ]
    }]
  });
}

async function handleAlert(data) {
  const { domain, alert_type, severity, title, message } = data;

  // Send PagerDuty alert for critical issues
  if (severity === 'critical') {
    await pagerduty.trigger({
      routing_key: process.env.PD_KEY,
      event_action: 'trigger',
      payload: {
        summary: `${title} - ${domain}`,
        severity: 'critical',
        source: 'ComplianceLayer',
        custom_details: { message, alert_type }
      }
    });
  }

  // Log to datadog
  await datadog.logEvent({
    title: `Alert: ${title}`,
    text: message,
    tags: [`domain:${domain}`, `severity:${severity}`, `type:${alert_type}`],
    alert_type: severity === 'critical' ? 'error' : 'warning'
  });
}

Retry Behavior

If your endpoint fails to respond or returns a non-2xx status code, ComplianceLayer will automatically retry delivery with exponential backoff:

  • Attempt 1: Immediately
  • Attempt 2: After 1 minute
  • Attempt 3: After 5 minutes
  • Attempt 4: After 15 minutes
  • Attempt 5: After 1 hour
  • Attempt 6: After 2 hours (final)

After 5 failed delivery attempts, the webhook event is marked as failed and moved to the dead letter queue. You can view failed deliveries and error details in your Settings → Webhook Endpoints dashboard.

Automatic Disabling: If an endpoint accumulates 10 consecutive failures, it will be automatically disabled to prevent further delivery attempts. Re-enable it after fixing the issue.

Testing Webhooks

Test your webhook implementation before going live:

1. Use the Dashboard Test Button

Click the Test button next to your webhook endpoint to send a test event:

{
  "id": "550e8400-e29b-41d4-a716-446655440099",
  "type": "scan.completed",
  "created_at": "2026-03-08T12:34:56.789Z",
  "data": {
    "test": true,
    "domain": "example.com",
    "score": 85,
    "grade": "B",
    "message": "This is a test webhook from ComplianceLayer"
  }
}

2. Use Webhook Testing Tools

During development, use services like webhook.site or ngrok to expose local endpoints:

# Install ngrok
brew install ngrok

# Expose local server
ngrok http 3000

# Use the HTTPS URL in your webhook settings
# https://abc123.ngrok.io/webhooks/compliancelayer

3. Check Delivery Logs

View detailed delivery logs in the dashboard, including:

  • HTTP status codes
  • Response times
  • Error messages
  • Retry attempts
  • Full request/response details

Best Practices

1. Always Verify Signatures

Never process webhook events without verifying the HMAC signature. This prevents malicious actors from triggering your webhook endpoint with fake events.

// ❌ NEVER DO THIS
app.post('/webhook', express.json(), (req, res) => {
  // Directly processing without signature verification
  processEvent(req.body);  // INSECURE!
  res.send('OK');
});

// ✅ ALWAYS DO THIS
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-compliancelayer-signature'];
  const payload = req.body.toString('utf8');

  if (!verifyWebhook(payload, signature, secret)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);
  processEvent(event);  // SECURE!
  res.send('OK');
});

2. Respond Within 30 Seconds

ComplianceLayer expects a response within 30 seconds. Process all heavy work asynchronously after responding:

app.post('/webhook', async (req, res) => {
  // Verify signature
  if (!verifyWebhook(...)) {
    return res.status(401).send('Invalid signature');
  }

  // ✅ Respond immediately
  res.status(200).send('OK');

  // ✅ Process in background
  const event = JSON.parse(payload);
  await queue.add(event);  // Add to job queue
});

// ❌ DON'T do this (may timeout)
app.post('/webhook', async (req, res) => {
  if (!verifyWebhook(...)) return res.status(401).send('Invalid');

  const event = JSON.parse(payload);

  // Heavy synchronous processing before responding
  await fetchFullReport(event.data.domain);
  await updateDatabase(event.data);
  await sendEmails(event.data);
  await createTickets(event.data);

  res.status(200).send('OK');  // May timeout!
});

3. Handle Idempotency

Your webhook handler must be idempotent. The same event may be delivered multiple times due to retries or network issues. Use the id field to detect and skip duplicate events:

// Using Redis for deduplication
const redis = require('redis').createClient();

async function processEvent(event) {
  const key = `webhook_processed:${event.id}`;

  // Check if already processed
  const exists = await redis.exists(key);
  if (exists) {
    console.log('Skipping duplicate event:', event.id);
    return;
  }

  // Process event
  await handleEvent(event);

  // Mark as processed (expires after 7 days)
  await redis.setex(key, 604800, '1');
}

4. Monitor Delivery Health

Regularly check your webhook delivery logs in Settings → Webhook Endpoints. Set up monitoring for:

  • High failure rates: Alert when >10% of deliveries fail
  • Slow response times: Monitor for responses >10 seconds
  • Repeated retries: Investigate if many events require multiple attempts
  • Disabled endpoints: Get notified if auto-disabled after 10 failures

5. Secure Your Webhook Secret

  • Store webhook secrets in environment variables or secret management systems (Vault, AWS Secrets Manager, etc.)
  • Never commit secrets to version control
  • Rotate secrets periodically or after suspected compromise
  • Use different secrets for different environments (dev/staging/prod)

6. Use Dedicated Endpoints

Don't reuse webhook endpoints across multiple services. Use unique URLs for better security and debugging:

  • https://api.acme.com/webhooks/compliancelayer
  • https://api.acme.com/webhooks/stripe
  • https://api.acme.com/webhooks (shared)

Integration Examples

Slack Notifications

const { WebClient } = require('@slack/web-api');
const slack = new WebClient(process.env.SLACK_TOKEN);

async function handleScanCompleted(data) {
  const { domain, score, grade, critical_issues } = data;

  await slack.chat.postMessage({
    channel: '#security-alerts',
    blocks: [
      {
        type: 'header',
        text: {
          type: 'plain_text',
          text: `🔒 Scan completed: ${domain}`
        }
      },
      {
        type: 'section',
        fields: [
          { type: 'mrkdwn', text: `*Score:*\n${score}/100` },
          { type: 'mrkdwn', text: `*Grade:*\n${grade}` },
          { type: 'mrkdwn', text: `*Critical:*\n${critical_issues}` }
        ]
      }
    ]
  });
}

PagerDuty Alerts

const axios = require('axios');

async function handleAlert(data) {
  const { domain, severity, title, message } = data;

  if (severity === 'critical') {
    await axios.post('https://events.pagerduty.com/v2/enqueue', {
      routing_key: process.env.PAGERDUTY_KEY,
      event_action: 'trigger',
      dedup_key: `compliancelayer-${domain}`,
      payload: {
        summary: `${title} - ${domain}`,
        severity: 'critical',
        source: 'ComplianceLayer',
        custom_details: { message, domain }
      }
    });
  }
}

Troubleshooting

Webhooks Not Being Received

  • Check that your endpoint is publicly accessible (not behind VPN/firewall)
  • Verify HTTPS certificate is valid and not self-signed
  • Check server logs for incoming requests
  • Review delivery history in Settings → Webhook Endpoints
  • Test with webhook.site or ngrok first

Signature Verification Failing

  • Ensure you're using the correct signing secret (check Settings)
  • Verify you're hashing the raw request body, not parsed JSON
  • Check for encoding issues (must be UTF-8)
  • Don't modify the payload before verification
  • Use crypto.timingSafeEqual or hmac.compare_digest for comparison
// ❌ WRONG - Parsing before verification
app.post('/webhook', express.json(), (req, res) => {
  const signature = req.headers['x-compliancelayer-signature'];
  const payload = JSON.stringify(req.body);  // Will fail!
  if (!verifyWebhook(payload, signature, secret)) {
    return res.status(401).send('Invalid');
  }
});

// ✅ CORRECT - Raw body for verification
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-compliancelayer-signature'];
  const payload = req.body.toString('utf8');  // Raw body!
  if (!verifyWebhook(payload, signature, secret)) {
    return res.status(401).send('Invalid');
  }
  const event = JSON.parse(payload);  // Parse after verification
});

Timeouts

  • Respond with 200 OK within 30 seconds
  • Move all processing to background jobs (use queues like Bull, Celery, RQ)
  • Avoid making external API calls in the webhook handler
  • Don't perform database-heavy operations synchronously
  • Test with realistic payloads under load

High Failure Rates

  • Check server health and resource usage (CPU, memory, disk)
  • Monitor error logs for exceptions
  • Verify database connection pool isn't exhausted
  • Ensure dependencies (Redis, database, APIs) are available
  • Implement proper error handling and retries

Endpoint Auto-Disabled

If your endpoint is automatically disabled after 10 consecutive failures:

  1. Check the delivery history for error messages
  2. Fix the underlying issue (server down, invalid code, etc.)
  3. Test with the Test button to verify it's working
  4. Re-enable the endpoint in Settings
  5. Monitor for 24 hours to ensure stability

Support

Need help with webhooks? Contact our support team: