Webhooks

Webhooks allow you to receive real-time HTTP notifications when events occur in Fence. Integrate with PagerDuty, Opsgenie, custom alerting systems, or your own applications.

Availability

Tier Webhooks Custom Headers Retry Logic
Hobby ❌ No N/A N/A
Startup ✅ Yes (5 webhooks) ✅ Yes ✅ 3 retries
Enterprise ✅ Yes (20 webhooks) ✅ Yes ✅ 5 retries
Custom ✅ Unlimited ✅ Yes ✅ Configurable

Webhook Events

Fence sends webhooks for these events:

Event Trigger Payload Includes
scan.completed Scan finishes successfully Scan ID, domain, vulnerability count
scan.failed Scan fails or times out Scan ID, domain, error message
vulnerability.detected New vulnerability found Severity, CVE, OWASP category, remediation
vulnerability.critical Critical vulnerability found Full vulnerability details
certificate.expiring Certificate expires in 30/14/7/3/1 days Domain, expiration date, days remaining
certificate.expired Certificate has expired Domain, expired date
domain.verified Domain verification succeeds Domain, verification method
domain.verification_failed Domain verification fails Domain, failure reason

Creating a Webhook

  1. Navigate to SettingsNotifications
  2. Click Add ChannelWebhook
  3. Configure webhook:
  4. Name: "PagerDuty Critical Alerts"
  5. URL: https://events.pagerduty.com/v2/enqueue
  6. Events: Select events to receive
  7. Custom Headers: Add authentication headers
  8. Click Test Webhook to verify
  9. Click Save

Webhook Payload Format

All webhooks use JSON format with consistent structure:

Scan Completed

{
  "event": "scan.completed",
  "timestamp": "2025-01-20T14:45:00Z",
  "data": {
    "scan_id": "uuid-abc123",
    "domain": "example.com",
    "scan_type": "FULL",
    "status": "COMPLETED",
    "vulnerabilities": {
      "critical": 2,
      "high": 5,
      "medium": 12,
      "low": 8,
      "info": 3
    },
    "duration_seconds": 287,
    "started_at": "2025-01-20T14:40:13Z",
    "completed_at": "2025-01-20T14:45:00Z",
    "url": "https://fence.dev/scans/uuid-abc123/"
  }
}

Vulnerability Detected (Critical)

{
  "event": "vulnerability.critical",
  "timestamp": "2025-01-20T14:45:05Z",
  "data": {
    "vulnerability_id": "uuid-def456",
    "domain": "example.com",
    "severity": "CRITICAL",
    "cvss_score": 9.8,
    "title": "SQL Injection in login form",
    "description": "User-supplied input is passed directly to SQL query without sanitization",
    "cve_id": "CVE-2024-12345",
    "owasp_category": "A03:2021 - Injection",
    "affected_url": "https://example.com/login",
    "remediation": {
      "summary": "Use parameterized queries or ORM",
      "difficulty": "MEDIUM",
      "references": [
        "https://owasp.org/www-community/attacks/SQL_Injection"
      ]
    },
    "url": "https://fence.dev/issues/uuid-def456/"
  }
}

Certificate Expiring

{
  "event": "certificate.expiring",
  "timestamp": "2025-01-20T09:00:00Z",
  "data": {
    "domain": "example.com",
    "expires_at": "2025-02-03T23:59:59Z",
    "days_remaining": 14,
    "issuer": "Let's Encrypt",
    "san_entries": ["example.com", "www.example.com"],
    "alert_threshold": "14_days"
  }
}

Webhook Signatures

All webhooks include an X-Fence-Signature header for verification:

Verifying Signatures

Python:

import hmac
import hashlib

def verify_webhook(payload, signature, secret):
    """Verify webhook signature."""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, f"sha256={expected_signature}")

# In your Flask/Django view
@app.route('/webhooks/fence', methods=['POST'])
def handle_fence_webhook():
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-Fence-Signature')
    secret = os.environ['FENCE_WEBHOOK_SECRET']

    if not verify_webhook(payload, signature, secret):
        return 'Invalid signature', 401

    data = json.loads(payload)
    # Process webhook...
    return 'OK', 200

Node.js:

const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expectedSignature}`)
  );
}

// Express.js route
app.post('/webhooks/fence', express.raw({type: 'application/json'}), (req, res) => {
  const payload = req.body.toString();
  const signature = req.headers['x-fence-signature'];
  const secret = process.env.FENCE_WEBHOOK_SECRET;

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

  const data = JSON.parse(payload);
  // Process webhook...
  res.send('OK');
});

Retry Logic

If your webhook endpoint fails, Fence automatically retries:

Retry Schedule

Attempt Delay Total Time Elapsed
1 Immediate 0s
2 1 minute 1m
3 5 minutes 6m
4 15 minutes 21m
5 1 hour 1h 21m

Retry conditions:
- HTTP status codes: 408, 429, 500, 502, 503, 504
- Connection timeout (30 seconds)
- DNS resolution failure

No retry:
- HTTP 4xx errors (except 408, 429)
- Invalid SSL certificate
- Webhook URL unreachable after DNS lookup

Integration Examples

PagerDuty

// Fence webhook payload transformation
{
  "routing_key": "YOUR_PAGERDUTY_INTEGRATION_KEY",
  "event_action": "trigger",
  "payload": {
    "summary": "Critical vulnerability detected: SQL Injection",
    "severity": "critical",
    "source": "Fence",
    "custom_details": {
      "domain": "example.com",
      "cve": "CVE-2024-12345",
      "cvss": 9.8,
      "url": "https://fence.dev/issues/uuid-def456/"
    }
  }
}

Setup:
1. Get PagerDuty Events API v2 Integration Key
2. Add webhook in Fence: https://events.pagerduty.com/v2/enqueue
3. Add custom header: Content-Type: application/json
4. Transform payload using middleware (or use Fence custom payload feature)

Opsgenie

{
  "message": "Critical vulnerability: SQL Injection in example.com",
  "alias": "fence-vuln-uuid-def456",
  "description": "CVSS 9.8 - SQL Injection detected in login form",
  "priority": "P1",
  "tags": ["fence", "security", "critical"],
  "details": {
    "domain": "example.com",
    "cve": "CVE-2024-12345",
    "url": "https://fence.dev/issues/uuid-def456/"
  }
}

Setup:
1. Create Opsgenie API integration
2. Add webhook: https://api.opsgenie.com/v2/alerts
3. Add header: Authorization: GenieKey YOUR_API_KEY

Slack (Custom Webhooks)

While we have native Slack integration, you can also use webhooks:

{
  "text": "🚨 Critical vulnerability detected",
  "blocks": [
    {
      "type": "header",
      "text": {
        "type": "plain_text",
        "text": "Critical: SQL Injection"
      }
    },
    {
      "type": "section",
      "fields": [
        {"type": "mrkdwn", "text": "*Domain:*\nexample.com"},
        {"type": "mrkdwn", "text": "*CVSS:*\n9.8"},
        {"type": "mrkdwn", "text": "*CVE:*\nCVE-2024-12345"}
      ]
    },
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "text": {"type": "plain_text", "text": "View Details"},
          "url": "https://fence.dev/issues/uuid-def456/"
        }
      ]
    }
  ]
}

Custom Application

Python Flask example:

from flask import Flask, request, jsonify
import logging

app = Flask(__name__)
logger = logging.getLogger(__name__)

@app.route('/webhooks/fence', methods=['POST'])
def fence_webhook():
    """Handle Fence webhook events."""
    event = request.json.get('event')
    data = request.json.get('data')

    logger.info(f"Received Fence webhook: {event}")

    if event == 'vulnerability.critical':
        # Send urgent alert
        domain = data['domain']
        severity = data['severity']
        title = data['title']
        cvss = data['cvss_score']

        alert_security_team(
            message=f"Critical vulnerability in {domain}: {title} (CVSS: {cvss})",
            priority="high",
            url=data['url']
        )

    elif event == 'certificate.expiring':
        # Create ticket in issue tracker
        domain = data['domain']
        days = data['days_remaining']

        create_jira_ticket(
            summary=f"Renew SSL certificate for {domain}",
            description=f"Certificate expires in {days} days",
            priority="medium"
        )

    elif event == 'scan.completed':
        # Log metrics
        vulnerabilities = data['vulnerabilities']
        if vulnerabilities['critical'] > 0 or vulnerabilities['high'] > 0:
            logger.warning(f"Scan found {vulnerabilities['critical']} critical and {vulnerabilities['high']} high severity issues")

    return jsonify({"status": "ok"}), 200

if __name__ == '__main__':
    app.run(port=5000)

Testing Webhooks

Manual Testing

Use Fence's built-in test feature:
1. Go to SettingsNotifications
2. Select your webhook
3. Click Test Webhook
4. Fence sends sample payload to your endpoint

Using Webhook.site

Test without writing code:
1. Visit https://webhook.site
2. Copy the unique URL
3. Add as webhook in Fence
4. Trigger events (run scan, etc.)
5. View payloads in webhook.site dashboard

Local Testing with ngrok

Test on localhost:

# Start ngrok tunnel
ngrok http 5000

# Copy HTTPS URL (e.g., https://abc123.ngrok.io)
# Add to Fence as webhook URL: https://abc123.ngrok.io/webhooks/fence

# Start local server
python app.py

# Trigger test webhook in Fence
# View requests in ngrok dashboard

Best Practices

Endpoint Requirements

  • ✅ Respond with HTTP 200-299 within 30 seconds
  • ✅ Return quickly (acknowledge receipt, process async)
  • ✅ Verify webhook signatures
  • ✅ Handle idempotent events (same event may be sent twice)
  • ✅ Use HTTPS (required)
  • ❌ Don't perform long operations synchronously
  • ❌ Don't return 5xx errors for validation failures

Security

# ✅ GOOD: Async processing
@app.route('/webhooks/fence', methods=['POST'])
def fence_webhook():
    # Verify signature
    if not verify_signature(request):
        return 'Unauthorized', 401

    # Queue for async processing
    queue.enqueue('process_fence_webhook', request.json)

    return 'OK', 200  # Return immediately

# Process in background worker
def process_fence_webhook(data):
    # Long-running operations here
    send_email(...)
    update_database(...)

Error Handling

@app.route('/webhooks/fence', methods=['POST'])
def fence_webhook():
    try:
        data = request.json
        if not data:
            return 'Invalid JSON', 400  # 4xx = don't retry

        process_webhook(data)
        return 'OK', 200

    except ValidationError as e:
        # Validation error = our fault, don't retry
        logger.error(f"Webhook validation failed: {e}")
        return str(e), 400

    except Exception as e:
        # Unexpected error = retry
        logger.exception(f"Webhook processing failed: {e}")
        return 'Internal error', 500  # 5xx = Fence will retry

Monitoring Webhooks

Delivery Status

View webhook delivery history:
1. Navigate to SettingsNotifications
2. Click on webhook name
3. View Delivery Log:
- ✅ Success (HTTP 2xx)
- 🔄 Pending retry
- ❌ Failed (exhausted retries)

Webhook Metrics

Track in Fence dashboard:
- Total deliveries (last 7 days)
- Success rate (%)
- Average response time
- Failed deliveries
- Retry count

Troubleshooting

Webhooks Not Received

Check:
1. Webhook URL is correct and accessible
2. Firewall allows Fence IP addresses
3. Endpoint returns HTTP 200 within 30 seconds
4. SSL certificate is valid (no self-signed certs)
5. Webhook events are enabled for the trigger

Fence IP addresses to whitelist:

# Production
34.123.45.67/32
35.234.56.78/32

# Staging
34.98.76.54/32

Signature Verification Fails

Common issues:
- Secret key mismatch
- Payload encoding (use raw body, not parsed JSON)
- Timing attacks (use hmac.compare_digest())

# ✅ CORRECT
payload = request.get_data(as_text=True)  # Raw body

# ❌ WRONG
payload = json.dumps(request.json)  # Reparsed JSON won't match

Duplicate Events

Fence may send the same event twice due to:
- Network timeouts (endpoint didn't respond in 30s)
- Retries after 5xx errors
- Database replication lag

Make your endpoint idempotent:

# Store processed event IDs
processed_events = set()

@app.route('/webhooks/fence', methods=['POST'])
def fence_webhook():
    event_id = request.headers.get('X-Fence-Event-ID')

    if event_id in processed_events:
        logger.info(f"Duplicate event {event_id}, skipping")
        return 'OK', 200  # Already processed

    # Process event
    process_webhook(request.json)

    # Mark as processed
    processed_events.add(event_id)
    return 'OK', 200

Next Steps

Was this page helpful?

Let us know if you have any questions or suggestions.