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:
- Respond with a
200 OKstatus within 30 seconds - Verify the HMAC signature in the
X-ComplianceLayer-Signatureheader - Process the event asynchronously (don't block the webhook response)
Configuring Webhooks
Set up webhooks from your dashboard:
- Navigate to Settings → Webhook Endpoints
- Click Add Webhook
- Enter your HTTPS endpoint URL
- Select which events to subscribe to
- Save and copy your signing secret (shown only once!)
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: 12345X-ComplianceLayer-Signature- HMAC-SHA256 signature (hex) for verifying authenticityX-ComplianceLayer-Event- The event type being deliveredX-ComplianceLayer-Delivery- Unique delivery ID for tracking
Available Events
scan.completedTriggered 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
}
}alert.triggeredTriggered 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
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.
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.
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/compliancelayer3. 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.timingSafeEqualorhmac.compare_digestfor 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 OKwithin 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:
- Check the delivery history for error messages
- Fix the underlying issue (server down, invalid code, etc.)
- Test with the Test button to verify it's working
- Re-enable the endpoint in Settings
- Monitor for 24 hours to ensure stability
Support
Need help with webhooks? Contact our support team:
- Email: [email protected]
- Dashboard: View delivery logs in Settings → Webhook Endpoints
- Status: Check system status page for any ongoing issues