SecurityHeaders.com API Shutdown: How to Migrate Your Scans
SecurityHeaders.com API is discontinued in April 2026. Here's a step-by-step guide for CI/CD security header checks, with examples and a drop-in replacement.
TL;DR — The SecurityHeaders.com API is shut down as of April 2026. If you were using it for CI/CD header checks, compliance scans or scheduled audits, you now need a replacement. The Guardr REST API is a drop-in alternative: same shape of GET request, structured JSON response, and a free tier for automated use. Every request and response in this post is a real example from the live API — not pseudocode. Migrations typically take under five minutes per pipeline.
What’s covered
- What happened
- What you actually need to replace
- The Guardr REST API — what it is
- Side-by-side: the old call vs. the new one
- What a real response looks like
- What a clean-site response looks like
- Response field mapping
- Rate limits and the free tier
- Error handling
- Five-minute CI/CD migration (GitHub Actions)
- What’s actually different, honestly
- What about other alternatives?
- Frequently asked questions
What happened
If you’ve been watching the web security space this spring, you already know: the SecurityHeaders.com API has been discontinued. Visit securityheaders.com/api today and you’ll see the notice — no new subscriptions, no renewals, service retired.
The short version of how we got here:
- January 2023 — SecurityHeaders.com launched its paid API, letting developers automate what had been a manual website check.
- June 2023 — Probely acquired SecurityHeaders.com from its original creator, Scott Helme.
- June 2025 — Snyk acquired Probely.
- April 2025 — Probely announced the API would be discontinued in April 2026, giving users a year of notice.
- April 2026 — The API is now shut down. The free web UI at securityheaders.com remains live, but the programmatic endpoint is gone.
Credit where it’s due: Snyk and Probely gave a full year of warning, and SecurityHeaders.com itself — a tool that’s done more to raise the baseline for HTTP security headers than almost anything else on the web — continues to exist. Scott Helme’s original work remains a huge contribution to the field, and this post isn’t a criticism of any business decision.
But if you’re one of the many developers who built CI/CD pipelines, scheduled audits, compliance reports, or monitoring systems on top of that API, you still need somewhere to go. That’s what this guide is for.
What you actually need to replace
Before picking a replacement, it’s worth being clear about what the SecurityHeaders.com API actually did. From what I’ve seen in issues, forums, and my own inbox, the four most common use cases were:
- CI/CD gate on HTTP security headers — fail the build if CSP, HSTS, or X-Frame-Options regress.
- Scheduled audits across an estate of sites — run a weekly scan on 50+ domains and dump results to a dashboard or Slack.
- Compliance evidence — JSON output filed as evidence for SOC 2, PCI, or internal policy reviews.
- Client reporting for agencies — automated scans feeding into monthly client reports.
All four share a common shape: one HTTP GET per domain, JSON in, structured response out, repeat on a schedule. That’s the shape we need to replicate.
The Guardr REST API — what it is
I’m Anatoli. I’m the solo founder building Guardr — a Cloudflare-native tool that scans websites for security misconfigurations (headers, TLS, DNS, cookies, exposure paths, and JS bundle secrets) and monitors uptime from three global regions.
The Guardr API v1 launched this month specifically to give SecurityHeaders.com API users a place to land. It covers the same core job — programmatic security scans with a structured JSON response — plus a few things the old API didn’t have.
The full endpoint list is short:
GET /v1/scan/:domain— read the most recent cached scan for a domain. Works with no key (20 req/day per IP, grade + score only), or with a key for richer results.POST /v1/scan— trigger a fresh on-demand scan. Requires an API key, available on every plan including Free.GET /v1/account— check your plan, quota config, and active keys.
Base URL: https://api.guardr.io
Full documentation with response samples is at guardr.io/docs/api.
Side-by-side: the old call vs. the new one
Here’s the exact migration for the most common pattern — a scheduled GET against a single domain.
Before (SecurityHeaders.com):
curl -H "x-api-key: YOUR_OLD_KEY" \
"https://api.securityheaders.com/?q=example.com&hide=on&followRedirects=on"
After (Guardr):
curl -H "X-API-Key: YOUR_NEW_KEY" \
"https://api.guardr.io/v1/scan/example.com"
Same style of auth header, same shape of GET request, domain passed as a URL path segment instead of a query parameter. The hide and followRedirects parameters from the old API have no equivalent in Guardr — scan results are private to your account by default, and redirects are always followed as part of the scan.
Want to try it without signing up? The GET endpoint works without a key, rate-limited to 20 req/day per IP, returning grade + score only:
curl https://api.guardr.io/v1/scan/example.com
That’s useful for quick health checks from a shell or a read-only monitoring script, but for CI/CD and anything with quotas or full results, you’ll want a key.
If you want to force a fresh scan (the Guardr GET endpoint returns a cached result, up to an hour old), use the POST variant — available on every plan:
curl -X POST "https://api.guardr.io/v1/scan" \
-H "X-API-Key: YOUR_NEW_KEY" \
-H "Content-Type: application/json" \
-d '{"domain":"example.com"}'
POST scans run inline and typically return in 5–15 seconds depending on the target’s response times and redirect chain.
What a real response looks like
This is the actual response from GET /v1/scan/example.com on a Solo-plan key, trimmed for readability but with every field name preserved exactly as the API returns it:
{
"domain": "example.com",
"scanned_at": "2026-04-18T12:46:48.373Z",
"grade": "C-",
"score": 58,
"categories": {
"tls": 55,
"headers": 0,
"cookies": 100,
"dns": 90,
"exposure": 100
},
"issues": [
{
"title": "HSTS not enabled",
"severity": "high",
"category": "TLS",
"description": "Strict-Transport-Security header is missing. Browsers won't enforce HTTPS for future visits.",
"remediation": {
"summary": "Add the Strict-Transport-Security header to tell browsers to always use HTTPS. This prevents protocol downgrade attacks.",
"effort": "moderate",
"warning": "Before enabling HSTS, make sure your entire site works over HTTPS — including all subdomains if using includeSubDomains.",
"snippets": [
{
"platform": "Cloudflare (_headers)",
"code": "/*\n Strict-Transport-Security: max-age=31536000; includeSubDomains; preload"
},
{
"platform": "Nginx",
"code": "add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;"
},
{
"platform": "Apache",
"code": "Header always set Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\""
}
],
"learnMore": "/blog/security-headers-guide"
}
}
],
"issues_truncated": false,
"total_issues": 8,
"secrets_found": [],
"tls": { "hstsPresent": false, "httpToHttps": false, "score": 55, "...": "..." },
"dns": { "dnssecEnabled": true, "caaPresent": false, "score": 90, "...": "..." },
"cookies": { "present": false, "cookiesCount": 0, "score": 100 },
"exposure_paths": {
"/.env": { "status": 404, "exposed": false },
"/.git/HEAD": { "status": 404, "exposed": false },
"/phpinfo.php": { "status": 404, "exposed": false },
"/wp-login.php": { "status": 404, "exposed": false }
}
}
Three things worth highlighting:
remediation.effort is a field you won’t find on the old API. Every issue comes back tagged quick-fix, moderate, or requires-planning. That’s useful when you’re prioritizing a backlog — you can filter for quick wins first and batch the larger CSP rollouts for a planned sprint.
remediation.warning flags issues where the naive fix will break something. HSTS has one about subdomain rollout. CSP has one about third-party integrations breaking. X-Frame-Options has one about intentional iframe embedding. These are the kinds of notes that usually live in a senior engineer’s head — having them inline in the API response means a junior dev can run the fix without breaking production.
remediation.snippets is an array of { platform, code } objects. Platforms include Cloudflare (_headers), Nginx, Apache, and occasionally multi-step flows like Step 1: Audit with report-only mode / Step 2: Enforce after testing for CSP deployments.
What a clean-site response looks like
For contrast, here’s GET /v1/scan/guardr.io on the same key. This is the shape you’ll see when a site is mostly configured correctly — one low-severity informational note, everything else clean:
{
"domain": "guardr.io",
"scanned_at": "2026-04-18T14:15:30.429Z",
"grade": "A+",
"score": 99,
"categories": {
"tls": 100,
"headers": 95,
"cookies": 100,
"dns": 100,
"exposure": 100
},
"issues": [
{
"title": "CSP note: content-security-policy",
"severity": "low",
"category": "Security Headers",
"description": "CSP present but: uses 'unsafe-inline' in script-src without hashes/nonces.",
"remediation": { "effort": "requires-planning", "...": "..." }
}
],
"issues_truncated": false,
"total_issues": 1,
"tls": {
"httpsServed": true,
"httpToHttps": true,
"hstsPresent": true,
"hstsMaxAge": 31536000,
"hstsIncludesSubs": true,
"hstsPreload": true,
"score": 100
},
"dns": {
"dnssecEnabled": true,
"caaPresent": true,
"score": 100
}
}
For CI/CD gating, the three fields that matter most are at the top level: grade, score, and categories. Everything else is detail you only drill into when something regresses.
Response field mapping
If you’re porting existing code, here’s the practical mapping between the two APIs:
| What you want | SecurityHeaders.com | Guardr |
|---|---|---|
| Overall grade (A–F) | summary.grade | grade |
| Numeric score | not available | score (0–100) |
| Scanned site | summary.site | domain |
| Timestamp | summary.timestamp | scanned_at (ISO 8601) |
| Header-by-header results | summary.headers (traffic-light colors) | issues[] with severity, title, description, remediation |
| Raw response headers | rawHeaders[] | not returned — use a separate HEAD request if needed |
| Missing headers | inferred from rawHeaders | filter issues[] where category == "Security Headers" |
| Grade cap explanation | summary.gradeCap | categories object shows which area lost points |
The headline difference: the old API was header-centric (the whole response was organized around the raw HTTP headers). Guardr is issue-centric (the response is organized around “what’s wrong and how to fix it”). That’s a better fit for CI/CD gating, worse if what you wanted was a dump of every HTTP header on the response.
Rate limits and the free tier
Two independent limits apply to every request: a per-minute burst limit, and a per-domain scan quota for POST scans.
| Plan | Burst limit | Scan quota | Quota window |
|---|---|---|---|
| Public (no key) | 20 req/day per IP | Read only | — |
| Free | 5 req/min | 1/domain | 7 days |
| Solo | 15 req/min | 1/domain | 24 hours |
| Starter | 30 req/min | 1/domain | 24 hours |
| Pro | 60 req/min | 1/domain | 6 hours |
| Agency | 120 req/min | 1/domain | 1 hour |
Scan quota is per-domain, so scanning 10 different domains uses 10 independent slots — not 10× a single slot. The GET /v1/scan/:domain endpoint reads from cache and doesn’t count against your scan quota at all, only against the burst limit. This matters for large sweeps: if you’re auditing 50 client sites nightly, trigger one POST per domain, then use GET for all subsequent reads during the day.
Every response includes standards-compliant rate-limit headers. Here’s a real 429 from a burst overflow on the free tier:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 18
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1776550081
{
"error": "rate_limit_exceeded",
"message": "Burst limit reached (5 req/min).",
"retry_after": 18
}
There’s also a distinct quota_exceeded error (also 429) when you’ve used your per-domain scan slot for the current window. Handle both in CI by respecting the Retry-After header — don’t hammer.
One note on free-tier responses: on the Free plan, scan responses include the grade, score, category breakdown, and the top three critical/high severity issues with full remediation text. The response carries issues_truncated: true, total_issues, and upgrade_url so your code can detect when it’s hitting the ceiling. The secrets_found, tls, dns, cookies, and exposure_paths objects are omitted until you’re on a paid plan. Plan your integration accordingly — if you need full TLS or DNS data in CI, start with Solo at $7/month.
Error handling
Four error shapes you should handle:
401 Unauthorized (missing or invalid key):
{
"error": "invalid_key",
"message": "API key not recognized. Check your key at guardr.io/dashboard/settings."
}
429 Rate Limited — shown above. Respect Retry-After; don’t hammer.
404 Not Found — domain was never scanned and the GET path can’t find a cached result. Either fall back to POST /v1/scan or try again after the first scan completes.
5xx — transient. Retry with exponential backoff.
Five-minute CI/CD migration (GitHub Actions)
The most common setup I saw with SecurityHeaders.com users: a nightly GitHub Actions workflow that hits the API, parses the grade, and fails the job if it regresses. Here’s that workflow, migrated:
# .github/workflows/security-scan.yml
name: Security Header Scan
on:
schedule:
- cron: '0 4 * * *' # daily at 04:00 UTC
workflow_dispatch:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Scan site with Guardr
env:
GUARDR_API_KEY: ${{ secrets.GUARDR_API_KEY }}
run: |
response=$(curl -sf \
-H "X-API-Key: $GUARDR_API_KEY" \
"https://api.guardr.io/v1/scan/example.com")
grade=$(echo "$response" | jq -r '.grade')
score=$(echo "$response" | jq -r '.score')
headers_score=$(echo "$response" | jq -r '.categories.headers')
echo "Grade: $grade | Score: $score | Headers: $headers_score"
# Fail if overall grade drops below B, OR headers category below 70
if [[ "$grade" == "D" || "$grade" == "E" || "$grade" == "F" \
|| "$grade" == "C" || "$grade" == "C-" ]]; then
echo "::error::Security grade regressed to $grade"
echo "$response" | jq '.issues[] | select(.severity == "critical" or .severity == "high")'
exit 1
fi
if (( headers_score < 70 )); then
echo "::error::Headers score dropped to $headers_score"
exit 1
fi
Add GUARDR_API_KEY to your repo secrets, generate the key in the Guardr dashboard under Settings → API Access, and you’re done.
If you’d rather trigger a fresh scan per run instead of reading from cache, swap the curl to a POST against /v1/scan — just be mindful of your per-domain scan quota.
What’s actually different, honestly
I’d rather you migrate with clear eyes than find out post-launch that something surprised you.
Things Guardr adds that SecurityHeaders.com didn’t have:
- Platform-specific remediation snippets (Cloudflare, Nginx, Apache) inline in the response
remediation.effortclassification (quick-fix/moderate/requires-planning) for backlog prioritizationremediation.warningfield flagging common pitfalls in the naive fix- TLS/SSL scanning with HSTS parsing (
hstsMaxAge,hstsIncludesSubs,hstsPreloadas structured fields) - DNS security checks (DNSSEC + CAA records)
- Exposure path checks (
/.git/HEAD,/.env,/phpinfo.php,/wp-login.php) - JavaScript bundle secret scanning (finds leaked OpenAI, Anthropic, Stripe, AWS, Supabase service-role keys in shipped JS)
- Cookie attribute audits (Secure, HttpOnly, SameSite)
- Standards-compliant rate-limit headers on every response
- Numeric scoring per category, not just a letter grade
Things the old API did that Guardr’s v1 API doesn’t:
- Raw HTTP header dump (
rawHeaders[]with color-coded explanatory text). Guardr returns structured findings instead of raw headers. If you specifically need a raw dump, a separate HEAD request is a better tool for the job. - The
hide=onparameter. Scan results are private to your account by default — nothing to opt out of.
Things I’m deliberately leaving out of v1 until users ask:
- History endpoint (
GET /v1/scan/:domain/history) - Before/after comparison endpoint (
GET /v1/scan/:domain/compare) - PDF-via-API (
GET /v1/scan/:domain/pdf)
All three exist in the Guardr dashboard today; exposing them as endpoints is a matter of when enough users ask. If any of these is blocking your migration, email me directly.
What about other alternatives?
Being honest: Guardr isn’t the only option, and the best tool depends on what you’re trying to accomplish.
- Mozilla Observatory — free scanner, excellent for one-off manual checks. Current version doesn’t offer a public API.
- Qualys SSL Labs — gold standard for deep TLS analysis. Doesn’t cover HTTP security headers.
- A handful of new replacement endpoints launched or expanded around the shutdown announcement. Worth shopping around if Guardr’s scope isn’t a fit.
- Roll your own — if all you need is “does this site send HSTS”, a 30-line Python script and a cron job will do it. You lose the grading, remediation text, historical tracking, and alerting, but it’s free.
My honest take: if you want a grade-based API with remediation and scheduled monitoring, Guardr is the closest drop-in. If you want deep TLS analysis specifically, use SSL Labs. If you want manual one-off scans, Mozilla Observatory is free and excellent. Pick the tool that matches the job.
If HTTP security headers are still a little fuzzy for you beyond CSP and HSTS, we’ve published a complete guide to adding security headers across platforms and a primer on what website security scores actually mean that cover the fundamentals.
Frequently asked questions
Is the SecurityHeaders.com website also shutting down?
No. As of April 2026, only the paid API is discontinued. The free web-based scanner at securityheaders.com is still operational. Snyk hasn’t announced any plan to retire it. This guide is specifically about replacing the programmatic API.
Do I need to pay to use the Guardr API?
No. The Guardr free tier includes one API key that returns the grade, score, category breakdown, and top three critical/high severity findings per scan — and it also includes POST scan access, so you can trigger fresh scans in CI (one scan per domain per 7-day window on Free). You need a paid plan if you want full TLS/DNS/cookie/exposure data in the response, tighter scan windows, or higher burst limits. There’s also a no-key public endpoint (GET /v1/scan/:domain, 20 req/day per IP) that returns grade + score only — useful for read-only health checks from anywhere.
How fast is a Guardr scan?
A cached GET returns in under 200ms. A fresh POST scan typically completes in 5–15 seconds depending on the target site’s response times and redirect chain. If you’re running a large sweep, prefer the GET /v1/scan/:domain endpoint — it reads from cache and doesn’t count against your per-domain scan quota.
Will my grade be the same as it was on SecurityHeaders.com?
Often similar, sometimes different. Guardr scores across five categories — TLS, headers, cookies, DNS, exposure paths — with weights documented on the methodology page. SecurityHeaders.com graded primarily on HTTP response headers. A site with weak TLS will score lower on Guardr than it did on SecurityHeaders.com; a site with missing headers but perfect TLS might score similarly. The categories object in every response lets you see exactly where points are coming from.
How do I check my current usage?
Hit GET /v1/account with your key:
curl -H "X-API-Key: YOUR_KEY" \
"https://api.guardr.io/v1/account"
It returns your plan, burst limit, scan window, and a redacted list of your active keys:
{
"plan": "starter",
"quota": {
"scan_window": 86400,
"burst_per_minute": 30
},
"keys": [
{
"key_id": "...",
"display": "••••••••••••••••f630",
"label": "CI pipeline",
"created_at": "18.04.2026",
"last_used_at": "18.04.2026",
"revoked": false
}
]
}
Can I use Guardr for compliance evidence?
Yes. PDF reports (available from the Starter plan and above) include the full scan result, timestamp, and per-issue findings — which is what most SOC 2 and PCI auditors are looking for. Agency-plan branding lets you white-label reports with your own logo for client deliverables.
Do you support webhook notifications?
Yes, on Pro and above. Slack, Microsoft Teams, and Discord webhooks fire on grade drops, SSL expiry warnings, and downtime events. Configure them in dashboard settings after signing in.
What if I need an endpoint the API doesn’t have?
Email me directly. API v1 is deliberately minimal. History, compare, and PDF-via-API all exist in the dashboard today; exposing them as API endpoints is a matter of when enough users ask, not whether.
Ready to migrate?
The shortest path:
- Sign up at guardr.io (free, no credit card).
- Go to Settings → API Access and generate a key.
- Swap
api.securityheaders.comforapi.guardr.io/v1/scanand update your auth header fromx-api-keytoX-API-Key. - Re-run your CI job.
Full documentation, response samples, and curl examples are at guardr.io/docs/api.
If anything’s unclear, or if there’s an endpoint from the old API you relied on that doesn’t have a Guardr equivalent yet, I want to hear about it. This API exists because the old one is gone — it should actually serve the use cases it was built to replace.
— Anatoli, building Guardr solo from Tel Aviv, Israel.