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
- Navigate to Settings → Notifications
- Click Add Channel → Webhook
- Configure webhook:
- Name: "PagerDuty Critical Alerts"
- URL:
https://events.pagerduty.com/v2/enqueue - Events: Select events to receive
- Custom Headers: Add authentication headers
- Click Test Webhook to verify
- 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 Settings → Notifications
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 Settings → Notifications
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