Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- Running a security scan manually once a month is better than never, but it is not monitoring — it is archaeology. This guide walks through using ZeriFlow's REST API to automate security scanning: authenticate with X-API-Key, parse the JSON response, schedule scans with cron, integrate with alerting systems, and handle errors and rate limits properly.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
How to Automate Website Security Scans with a REST API
Manual security scans generate reports. Automated security scans generate continuous awareness. The difference is whether you know about a configuration regression within hours of it happening, or whether you find out when a customer mentions it.
ZeriFlow provides a REST API that makes it straightforward to integrate security scanning into any automation pipeline — a cron job on a VPS, a serverless function on a schedule, a post-deploy hook in your CI/CD pipeline, or a custom monitoring dashboard. This guide covers the complete implementation from authentication to alerting.
The ZeriFlow API: Core Concepts
Base URL: https://api.zeriflow.com
Authentication: every request requires an X-API-Key header with your API key from the ZeriFlow dashboard (Settings > API Keys).
Plan limits: - Pro: 30 API calls/month - Business: 100 API calls/month - Unlimited: 1,000 API calls/month
Rate limiting: the API enforces per-minute rate limits in addition to monthly quotas. Respect Retry-After headers on 429 responses.
The /scan-quick Endpoint
POST /scan-quick is the primary scanning endpoint. It runs 80+ checks against a target URL and returns a complete JSON report within 60 seconds.
Request
curl -X POST https://api.zeriflow.com/scan-quick \\
-H "Content-Type: application/json" \\
-H "X-API-Key: zf_live_your_api_key_here" \\
-d ''{
"url": "https://your-app.com"
}''Response Structure
{
"scan_id": "scan_01hwxyz789abc",
"url": "https://your-app.com",
"score": 74,
"grade": "C+",
"scanned_at": "2026-05-02T09:14:32Z",
"scan_duration_ms": 8420,
"summary": {
"total_checks": 83,
"passed": 61,
"failed": 22,
"critical": 1,
"high": 3,
"medium": 9,
"low": 6,
"informational": 3
},
"findings": [
{
"id": "finding_csp_missing",
"check_id": "content-security-policy",
"title": "Content Security Policy header is missing",
"severity": "critical",
"category": "http-headers",
"description": "The Content-Security-Policy header is not present. This header helps prevent XSS attacks by specifying which sources of content the browser should allow.",
"remediation": "Add a Content-Security-Policy header to your server responses. Start with a strict policy: Content-Security-Policy: default-src ''self''",
"references": [
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy",
"https://owasp.org/www-project-secure-headers/"
],
"cvss_score": null,
"passed": false
},
{
"id": "finding_hsts_present",
"check_id": "strict-transport-security",
"title": "Strict-Transport-Security header is present",
"severity": "high",
"category": "http-headers",
"description": null,
"remediation": null,
"passed": true
}
],
"tls": {
"valid": true,
"issuer": "Let''s Encrypt",
"expires_at": "2026-08-01T00:00:00Z",
"days_until_expiry": 91,
"protocol_versions": ["TLSv1.2", "TLSv1.3"],
"grade": "A"
},
"headers_found": ["Strict-Transport-Security", "X-Frame-Options", "Referrer-Policy"],
"headers_missing": ["Content-Security-Policy", "Permissions-Policy"],
"report_url": "https://zeriflow.com/reports/scan_01hwxyz789abc"
}Parsing the Response in Shell
#!/bin/bash
# scan.sh
URL="${1:-https://your-app.com}"
THRESHOLD="${2:-75}"
RESPONSE=$(curl -s -w "\\n%{http_code}" \\
-X POST https://api.zeriflow.com/scan-quick \\
-H "Content-Type: application/json" \\
-H "X-API-Key: $ZERIFLOW_API_KEY" \\
-d "{\\"url\\": \\"$URL\\"}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed ''$d'')
# Handle HTTP errors
case "$HTTP_CODE" in
200) ;;
401) echo "ERROR: Invalid API key"; exit 2 ;;
422) echo "ERROR: Invalid URL format"; exit 3 ;;
429)
RETRY=$(echo "$RESPONSE" | grep -i ''retry-after'' | awk ''{print $2}'' | tr -d ''\\r'')
echo "ERROR: Rate limited. Retry after ${RETRY:-60} seconds"
exit 4 ;;
5*) echo "ERROR: ZeriFlow API error (HTTP $HTTP_CODE)"; exit 5 ;;
*) echo "ERROR: Unexpected response (HTTP $HTTP_CODE)"; exit 6 ;;
esac
# Extract values
SCORE=$(echo "$BODY" | jq -r ''.score'')
CRITICAL=$(echo "$BODY" | jq -r ''.summary.critical'')
HIGH=$(echo "$BODY" | jq -r ''.summary.high'')
REPORT_URL=$(echo "$BODY" | jq -r ''.report_url'')
CERT_EXPIRY=$(echo "$BODY" | jq -r ''.tls.days_until_expiry'')
echo "Score: $SCORE/100"
echo "Critical findings: $CRITICAL"
echo "High findings: $HIGH"
echo "Certificate expires in: $CERT_EXPIRY days"
echo "Full report: $REPORT_URL"
# List critical and high findings
echo ""
echo "Issues requiring attention:"
echo "$BODY" | jq -r ''
.findings[] |
select(.passed == false and (.severity == "critical" or .severity == "high")) |
" [\\(.severity | ascii_upcase)] \\(.title)"
''
# Exit code based on threshold
if (( $(echo "$SCORE < $THRESHOLD" | bc -l) )); then
echo ""
echo "FAIL: Score $SCORE is below threshold $THRESHOLD"
exit 1
fi
echo ""
echo "PASS: Score $SCORE meets threshold $THRESHOLD"
exit 0Parsing the Response in Python
#!/usr/bin/env python3
# scan.py
import os
import sys
import json
import time
import requests
from datetime import datetime
def scan_url(url: str, threshold: int = 75) -> dict:
api_key = os.environ.get(''ZERIFLOW_API_KEY'')
if not api_key:
raise ValueError("ZERIFLOW_API_KEY environment variable is not set")
response = requests.post(
''https://api.zeriflow.com/scan-quick'',
headers={
''Content-Type'': ''application/json'',
''X-API-Key'': api_key
},
json={''url'': url},
timeout=90 # scans can take up to 60 seconds
)
if response.status_code == 429:
retry_after = int(response.headers.get(''Retry-After'', 60))
raise Exception(f"Rate limited. Retry after {retry_after} seconds.")
response.raise_for_status()
return response.json()
def format_findings(data: dict) -> str:
critical_high = [
f for f in data[''findings'']
if not f[''passed''] and f[''severity''] in (''critical'', ''high'')
]
if not critical_high:
return "No critical or high findings."
lines = []
for finding in critical_high:
lines.append(f"[{finding[''severity''].upper()}] {finding[''title'']}")
if finding.get(''remediation''):
lines.append(f" Fix: {finding[''remediation''][:120]}...")
return ''\\n''.join(lines)
def main():
url = sys.argv[1] if len(sys.argv) > 1 else ''https://your-app.com''
threshold = int(sys.argv[2]) if len(sys.argv) > 2 else 75
try:
print(f"Scanning: {url}")
data = scan_url(url, threshold)
score = data[''score'']
cert_days = data[''tls''][''days_until_expiry'']
report_url = data[''report_url'']
print(f"Score: {score}/100")
print(f"Certificate expires in: {cert_days} days")
print(f"Full report: {report_url}")
print()
print("Findings requiring attention:")
print(format_findings(data))
# Cert expiry warning
if cert_days < 30:
print(f"\\nWARNING: Certificate expires in {cert_days} days!")
if score < threshold:
print(f"\\nFAIL: Score {score} is below threshold {threshold}")
sys.exit(1)
print(f"\\nPASS: Score {score} meets threshold {threshold}")
except Exception as e:
print(f"Scan error: {e}", file=sys.stderr)
sys.exit(2)
if __name__ == ''__main__'':
main()Scheduling Scans with Cron (Linux/VPS)
For a simple production setup on any Linux server, cron provides reliable scheduled execution. Store your scripts in a consistent location and log output for debugging.
# Create the scanner script
mkdir -p /opt/zeriflow
cat > /opt/zeriflow/scan.sh << ''EOF''
#!/bin/bash
# Load API key from a secure location
export ZERIFLOW_API_KEY=$(cat /etc/zeriflow/api_key)
LOG_FILE="/var/log/zeriflow/scan-$(date +%Y%m%d).log"
mkdir -p /var/log/zeriflow
echo "=== Scan started: $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" >> "$LOG_FILE"
# Run the scan
/opt/zeriflow/scan-and-alert.sh "https://your-app.com" "75" >> "$LOG_FILE" 2>&1
EXIT_CODE=$?
echo "=== Scan completed: exit $EXIT_CODE ===" >> "$LOG_FILE"
# Rotate logs older than 30 days
find /var/log/zeriflow -name "*.log" -mtime +30 -delete
EOF
chmod +x /opt/zeriflow/scan.sh
# Store API key securely
mkdir -p /etc/zeriflow
echo "zf_live_your_api_key_here" > /etc/zeriflow/api_key
chmod 600 /etc/zeriflow/api_key
# Add to crontab: run daily at 2 AM
echo "0 2 * * * /opt/zeriflow/scan.sh" | crontab -For multi-URL monitoring:
# /opt/zeriflow/multi-scan.sh
#!/bin/bash
export ZERIFLOW_API_KEY=$(cat /etc/zeriflow/api_key)
URLS=(
"https://your-app.com:80"
"https://api.your-app.com:75"
"https://admin.your-app.com:85"
)
FAILED=0
for ENTRY in "${URLS[@]}"; do
URL="${ENTRY%%:*}"
THRESHOLD="${ENTRY##*:}"
echo "Scanning $URL (threshold: $THRESHOLD)..."
/opt/zeriflow/scan-and-alert.sh "$URL" "$THRESHOLD" || FAILED=$((FAILED + 1))
sleep 5 # avoid hitting rate limits
done
if [ "$FAILED" -gt 0 ]; then
echo "$FAILED scan(s) failed threshold check"
exit 1
fiIntegrating with Slack and PagerDuty
Build a complete scan-and-alert script that routes to the right channel based on severity:
#!/bin/bash
# /opt/zeriflow/scan-and-alert.sh
URL="$1"
THRESHOLD="${2:-75}"
SLACK_WEBHOOK="${SLACK_SECURITY_WEBHOOK}"
PAGERDUTY_KEY="${PAGERDUTY_INTEGRATION_KEY}"
RESPONSE=$(curl -s \\
-X POST https://api.zeriflow.com/scan-quick \\
-H "Content-Type: application/json" \\
-H "X-API-Key: $ZERIFLOW_API_KEY" \\
-d "{\\"url\\": \\"$URL\\"}")
SCORE=$(echo "$RESPONSE" | jq -r ''.score'')
CRITICAL=$(echo "$RESPONSE" | jq -r ''.summary.critical'')
REPORT_URL=$(echo "$RESPONSE" | jq -r ''.report_url'')
notify_slack() {
local message="$1"
local color="$2"
curl -s -X POST "$SLACK_WEBHOOK" \\
-H "Content-Type: application/json" \\
-d "{
\\"attachments\\": [{
\\"color\\": \\"$color\\",
\\"text\\": \\"$message\\",
\\"footer\\": \\"ZeriFlow | $(date -u +%Y-%m-%dT%H:%M:%SZ)\\"
}]
}"
}
trigger_pagerduty() {
local summary="$1"
curl -s -X POST "https://events.pagerduty.com/v2/enqueue" \\
-H "Content-Type: application/json" \\
-d "{
\\"routing_key\\": \\"$PAGERDUTY_KEY\\",
\\"event_action\\": \\"trigger\\",
\\"dedup_key\\": \\"zeriflow-$(echo "$URL" | md5sum | head -c8)-$(date +%Y%m%d)\\",
\\"payload\\": {
\\"summary\\": \\"$summary\\",
\\"severity\\": \\"critical\\",
\\"source\\": \\"zeriflow\\",
\\"custom_details\\": {
\\"url\\": \\"$URL\\",
\\"score\\": $SCORE,
\\"critical_findings\\": $CRITICAL,
\\"report\\": \\"$REPORT_URL\\"
}
}
}"
}
# Route alerts based on severity
if [ "$CRITICAL" -gt 0 ]; then
trigger_pagerduty "$CRITICAL critical security finding(s) on $URL — score: $SCORE/100"
notify_slack "CRITICAL: $CRITICAL critical finding(s) on $URL — score $SCORE/100. Report: $REPORT_URL" "danger"
elif (( $(echo "$SCORE < $THRESHOLD" | bc -l) )); then
notify_slack "Security score dropped to $SCORE/100 on $URL (threshold: $THRESHOLD). Report: $REPORT_URL" "warning"
else
echo "OK: $URL scored $SCORE/100 (threshold: $THRESHOLD)"
fiError Handling and Retry Logic
Network errors, API timeouts, and rate limits need to be handled gracefully in any production script. Implement exponential backoff:
#!/bin/bash
# Wrapper with retry logic
scan_with_retry() {
local url="$1"
local max_attempts=3
local attempt=1
local delay=10
while [ $attempt -le $max_attempts ]; do
RESPONSE=$(curl -s -w "\\n%{http_code}" \\
--connect-timeout 10 \\
--max-time 90 \\
-X POST https://api.zeriflow.com/scan-quick \\
-H "Content-Type: application/json" \\
-H "X-API-Key: $ZERIFLOW_API_KEY" \\
-d "{\\"url\\": \\"$url\\"}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed ''$d'')
if [ "$HTTP_CODE" = "200" ]; then
echo "$BODY"
return 0
fi
if [ "$HTTP_CODE" = "429" ]; then
# Extract Retry-After header from body (ZeriFlow includes it in JSON too)
RETRY_AFTER=$(echo "$BODY" | jq -r ''.retry_after // 60'')
echo "Rate limited. Waiting ${RETRY_AFTER}s before retry..." >&2
sleep "$RETRY_AFTER"
attempt=$((attempt + 1))
continue
fi
echo "Attempt $attempt/$max_attempts failed (HTTP $HTTP_CODE). Retrying in ${delay}s..." >&2
sleep $delay
delay=$((delay * 2)) # exponential backoff
attempt=$((attempt + 1))
done
echo "All $max_attempts attempts failed" >&2
return 1
}
RESULT=$(scan_with_retry "https://your-app.com")
if [ $? -eq 0 ]; then
echo "Score: $(echo "$RESULT" | jq -r ''.score'')"
else
echo "Scan failed after retries"
exit 1
fiManaging Monthly API Quota
On the Pro plan (30 calls/month), budget your calls carefully:
| Use case | Calls needed |
|---|---|
| Daily scan of 1 URL | 31 calls/month |
| Post-deploy scan (20 deploys/month) | 20 calls/month |
| CI/CD check on every PR (estimate) | 15-40 calls/month |
For quota-conscious setups, check the scan result against a cache before spending an API call:
#!/bin/bash
# Only scan if config files have changed since last scan
CACHE_DIR="/var/cache/zeriflow"
mkdir -p "$CACHE_DIR"
# Hash of your security-relevant config files
CONFIG_HASH=$(find /your/project -name "nginx.conf" -o -name "next.config.js" -o -name "Caddyfile" | \\
sort | xargs sha256sum 2>/dev/null | sha256sum | head -c16)
CACHE_FILE="$CACHE_DIR/last-scan-$CONFIG_HASH.json"
if [ -f "$CACHE_FILE" ]; then
AGE=$(($(date +%s) - $(stat -c%Y "$CACHE_FILE")))
if [ "$AGE" -lt 86400 ]; then
echo "Using cached scan result (${AGE}s old)"
cat "$CACHE_FILE"
exit 0
fi
fi
# Run fresh scan
RESULT=$(curl -s \\
-X POST https://api.zeriflow.com/scan-quick \\
-H "X-API-Key: $ZERIFLOW_API_KEY" \\
-H "Content-Type: application/json" \\
-d ''{"url": "https://your-app.com"}'')
echo "$RESULT" > "$CACHE_FILE"
echo "$RESULT"Conclusion
The ZeriFlow REST API makes it straightforward to integrate security scanning into any part of your infrastructure stack — cron jobs, post-deploy hooks, CI pipelines, or custom dashboards. The JSON response is structured for easy parsing, and the endpoints are designed for automation rather than interactive use.
Start with a cron job that runs daily and sends Slack alerts on score drops. Once you have baseline data and understand your typical score range, add threshold-based PagerDuty escalation for critical findings.
ZeriFlow Pro includes 30 API calls/month — enough for daily scanning, with headroom for CI/CD integration on actively developed projects.