Docs: API and scoring
HeaderGuard fetches one URL from our server, follows up to 10 redirects, and grades the security headers of the final response.
API
Free, no key, CORS enabled for GET. About 30 scans per minute per IP (approximate; IPv6 addresses are counted per /64). Results are cached in memory for 2 minutes (x-cache: HIT); cached results don't count against the limit.
GET /api/scan?url=<url-or-domain>
curl "https://headerguard.mike-tusa.workers.dev/api/scan?url=example.com"
curl "https://headerguard.mike-tusa.workers.dev/api/scan?url=https://example.com/login"
A bare domain is scanned as https://<domain>/; if HTTPS does not answer at all, plain HTTP is scanned instead (and noted). domain= is accepted as an alias for url=.
Response (abridged)
{
"url": "https://github.com/",
"finalUrl": "https://github.com/",
"finalStatus": 200,
"redirects": [ { "url": "https://github.com/", "status": 200, "ms": 120 } ],
"httpCheck": { "url": "http://github.com/", "status": 301, "redirectsToHttps": true,
"hops": [ { "url": "http://github.com/", "status": 301, "location": "https://github.com/" } ] },
"score": 85, "grade": "A", "gradeWithheld": false, "gradeWithheldReason": null,
"scoring": { "base": 85, "leakPenalty": 0, "cappedNoHttps": false, "max": 100 },
"findings": [
{ "id": "hsts", "name": "Strict-Transport-Security", "present": true,
"value": "max-age=31536000; includeSubdomains; preload",
"status": "pass", "points": 15, "max": 15, "summary": "Strong",
"notes": [ { "level": "pass", "text": "max-age is 31536000 seconds…" } ] },
…
],
"reliability": "ok",
"notes": [],
"fixes": {
"items": [ { "id": "coop", "title": "Add Cross-Origin-Opener-Policy", "header": "Cross-Origin-Opener-Policy",
"value": "same-origin", "note": "…",
"snippets": { "nginx": "…", "apache": "…", "cloudflare": "…", "netlify": "…", "vercel": "…", "express": "…" } } ],
"combined": { "nginx": "…", … },
"platforms": { "nginx": "nginx", "apache": "Apache (.htaccess)", … }
},
"headers": [ ["content-type", "text/html; charset=utf-8"], ["set-cookie", "_gh_sess=<redacted>; path=/; secure; HttpOnly; SameSite=Lax"] ],
"meta": { "version": "0.4.0", "scannedAt": "2026-10-03T12:00:00.000Z", "durationMs": 640, "subrequests": 4 }
}
Not graded: blocked or challenged scans
reliability is "ok", or "blocked_or_challenged" when the site answered with a block, rate-limit or bot-challenge page (final HTTP 401/403/429/503, a cf-mitigated header, or a redirect to a captcha such as Google's /sorry/). The headers we received then belong to that block page, not the real site, so no grade is given:
{
"reliability": "blocked_or_challenged",
"score": null,
"grade": null,
"gradeWithheld": true,
"gradeWithheldReason": "The site blocked, rate-limited or challenged our scanner (final HTTP 429, redirected to a captcha/challenge page). …",
"unreliableRawScore": { "score": 15, "note": "UNRELIABLE: computed from the block/challenge page, not the real site. Do not display as a grade." },
"findings": [ … ] // describe the block page only
}
The page shows a grey "Not graded: blocked" box instead of a letter, with the warning above the result. For normal scans gradeWithheld is false, gradeWithheldReason is null and there is no unreliableRawScore. Please don't publish unreliableRawScore as a grade.
Finding ids: https, hsts, csp, framing, xcto, referrer, permissions, coop, corp, cookies, coep (info), xxss (info), leaks (penalty). status is pass, warn, fail or info. Cookie values are always redacted.
Errors
Errors return JSON {"error": {"code", "message"}} (plus redirects when a redirect chain was followed).
| HTTP | code | When |
|---|---|---|
| 400 | invalid_url, invalid_scheme | Missing or malformed URL, credentials in the URL, a scheme other than http/https |
| 403 | blocked_target, blocked_port, blocked_redirect | Private, loopback, link-local, metadata or special-use address or name; a port other than 80/443/8080/8443; a redirect to any of those |
| 405 | method_not_allowed | Anything but GET/HEAD/OPTIONS |
| 422 | dns_not_found | The hostname does not exist (NXDOMAIN) or has no A/AAAA records |
| 429 | rate_limited | Too many scans from your IP (retry after 60 s) |
| 502 | fetch_failed, too_many_redirects, redirect_loop, bad_redirect, dns_error | The site could not be reached, it redirects more than 10 times, or DNS resolution failed (dns_error: SERVFAIL, REFUSED or no resolver answered; retry later) |
| 504 | timeout | No response within 5 s per request (20 s per scan) |
GET /api/health
{ "ok": true, "service": "headerguard", "version": "0.4.0", "publicLaunch": true,
"build": "1a2b3c4d", "deployId": "1a2b3c4d-…", "deployedAt": "2026-10-03T16:40:00.000Z" }
build / deployId identify the exact deployed Worker version (from Cloudflare's version metadata); deployedAt is when it was uploaded.
HeaderGuard Pro ($9/mo)
Higher limits for developers and CI. Polar is the merchant of record; cancel anytime via the customer portal. Support: digitalpromohub.support@gmail.com.
- About 120 scans/min per license key (free: about 30/min per IP).
- Batch:
POST /api/scan/batchwith JSON{ "urls": ["a.com","b.com"] }— at most 5 URLs; each is SSRF-checked like a normal scan. - Free plan (no key): one-off scans, the embeddable badge, and full results stay free.
Send your key as X-License-Key: HDRG-… or Authorization: Bearer HDRG-…. Responses include plan: "free" or plan: "pro". Invalid or expired keys return 401/403 JSON with an upgradeUrl (never echo the key).
curl -H "X-License-Key: HDRG-****" \
"https://headerguard.mike-tusa.workers.dev/api/scan?url=example.com"
curl -X POST -H "Authorization: Bearer HDRG-****" -H "content-type: application/json" \
-d '{"urls":["example.com","github.com"]}' \
"https://headerguard.mike-tusa.workers.dev/api/scan/batch"
Billing terms: payment processed by Polar; we never see your card. License keys are validated against our license store and are not written to request logs.
MCP server for AI agents
HeaderGuard is also a remote Model Context Protocol (MCP) server at https://headerguard.mike-tusa.workers.dev/mcp (Streamable HTTP, POST only, stateless, JSON responses). Most MCP clients accept this config; some use a different format (for example, VS Code uses a servers key):
{ "mcpServers": { "headerguard": { "type": "http", "url": "https://headerguard.mike-tusa.workers.dev/mcp" } } }
One read-only tool, scan_headers, with url (required) and include_raw (optional). It runs the same scan as GET /api/scan and shares its 2-minute cache, its per-minute limits and the HeaderGuard Pro license key (Authorization: Bearer HDRG-… or X-License-Key); each tools/call counts as one scan. Over a limit, the tool result has isError: true and retryAfterSeconds. Machine-readable docs: llms.txt · OpenAPI 3.1 spec.
Embeddable grade badge
Free shields-style SVG for READMEs and site footers. No sign-up.
GET /badge/<host>.svg
https://headerguard.mike-tusa.workers.dev/badge/example.com.svg
Markdown:
[](https://headerguard.mike-tusa.workers.dev/?url=example.com&src=badge)
The right-hand side shows the letter grade (A+…F). If the site blocked or challenged the scanner, or the host is invalid / not allowed, the badge is neutral grey (not graded or unknown) and still returns HTTP 200 so embeds do not break. A short-lived grey try later badge is returned when the per-IP badge rate limit is hit.
- Host only: a bare hostname (lowercased). Paths, ports, userinfo and IP literals are rejected. The same SSRF rules as the scanner apply, because the badge reuses the scan code path.
- Caching: graded badges are cached about 24 hours (
s-maxage=86400, browser/camomax-age=3600) via the Workers Cache API. Errors use about 1 hour. A cache hit does not run a scan and does not count against the rate limit. The grade you see can therefore be up to about a day old; open the linked report for a fresh scan. - Report link:
/?url=<host>&src=badgepre-fills the form and runs the scan automatically. The results page includes copy-ready Markdown, HTML and direct-URL snippets. Content-Type: image/svg+xml,X-Content-Type-Options: nosniff, and a strict SVG CSP (default-src 'none'; style-src 'unsafe-inline'). All text in the SVG is escaped.
Scoring
Points per check add up to 100. Penalties and caps are applied after that. Only the final response's headers are graded (after redirects).
| Check | Max | How points are earned |
|---|---|---|
| HTTPS | 10 | 5 if the final URL is HTTPS. 5 if http://host/ ends up on HTTPS within 5 redirects (e.g. http://apex → http://www → https://www; every hop is safety-checked), or if plain HTTP does not answer at all. If the scan ends on a different host (e.g. apex → www), http://<final host>/ must redirect to HTTPS too (reported in httpCheck.finalHost). Couldn't check is not a failure: if either redirect check can't finish (a later hop times out, can't be reached or its DNS lookup fails with an error such as SERVFAIL or REFUSED, or the per-scan request budget runs out), nothing is deducted and an info note says so. Real failures still cost the 5 points: answering without a redirect, redirecting to a blocked address, a broken redirect, 5 or more plain-HTTP-to-plain-HTTP redirects in a row (HTTPS must be reached within 5 redirects), or a redirect to a host that doesn't exist. If the very first http:// request gets no answer at all (refused or timed out), that counts as plain HTTP not being served, as before. |
| Strict-Transport-Security | 15 | max-age ≥ 1 year: 12 · ≥ 180 days: 9 · shorter: 4 · missing, 0 or invalid: 0. +2 includeSubDomains. +1 preload (only counts with ≥ 1 year and includeSubDomains). Not applicable (0) without HTTPS. |
| Content-Security-Policy | 25 | Enforced policy present: 6. +3 scripts restricted (script-src or default-src). +5 no effective 'unsafe-inline' (a nonce or hash neutralizes it). +3 no 'unsafe-eval'. +3 no broad script sources (*, http:, https:, data:, blob:; ignored with 'strict-dynamic'). +2 object-src 'none' (directly or via default-src 'none'). +1 base-uri. +1 frame-ancestors. +1 default-src. Report-Only only: 3. Missing: 0. With several enforced policies, a check passes if any policy passes. |
| Framing | 10 | X-Frame-Options: DENY or SAMEORIGIN, or CSP frame-ancestors: 10. ALLOW-FROM, invalid or missing: 0. |
| X-Content-Type-Options | 10 | nosniff: 10. Otherwise 0. |
| Referrer-Policy | 10 | no-referrer, same-origin, strict-origin, strict-origin-when-cross-origin: 10 · origin, origin-when-cross-origin: 6 · missing or invalid: 5 (browsers default to strict-origin-when-cross-origin) · no-referrer-when-downgrade: 3 · unsafe-url: 0. The last valid value in a list wins. |
| Permissions-Policy | 5 | At least one feature directive: 5 · only legacy Feature-Policy or only interest-cohort: 2 · missing: 0. |
| Cross-Origin-Opener-Policy | 5 | same-origin: 5 · same-origin-allow-popups / noopener-allow-popups: 4 · unsafe-none or missing: 0. |
| Cross-Origin-Resource-Policy | 5 | same-origin or same-site: 5 · cross-origin: 2 · missing: 0. |
| Cookies | 5 | No Set-Cookie on the response: 5. Otherwise 5 × (cookies with both Secure and SameSite) ÷ (all cookies), rounded. 0 without HTTPS. Missing HttpOnly is noted but not scored. |
| Cross-Origin-Embedder-Policy | 0 | Information only. It can break third-party embeds and is only needed for cross-origin isolation. |
| X-XSS-Protection | 0 | Information only. Absent or 0 is recommended; 1 / 1; mode=block gets a warning. |
| Version leaks | −5 | −2 each (at most −5) for a Server header with a version number, X-Powered-By, X-AspNet-Version or X-AspNetMvc-Version. |
Cap: if the final page is not served over HTTPS, the score is capped at 39 (F).
Not graded: blocked or challenged scans get no score or letter (see above).
| Grade | A+ | A | B | C | D | F |
|---|---|---|---|---|---|---|
| Score | ≥ 95 | ≥ 85 | ≥ 70 | ≥ 55 | ≥ 40 | < 40 |
Fix snippets
Each weak or missing header comes with a snippet for nginx, Apache (.htaccess), Cloudflare (Pages _headers or Workers), Netlify _headers, Vercel vercel.json and Express with helmet. The values are deliberately conservative:
- HSTS
max-age=31536000; includeSubDomains. No preload by default; read hstspreload.org first. - CSP: a safe starter policy sent as
Content-Security-Policy-Report-Only. It needs tuning for your site (inline scripts, analytics, fonts and CDNs will be reported until you allow them). Enforce it only after the reports are clean. - X-Frame-Options SAMEORIGIN, Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy camera, microphone and geolocation off, COOP/CORP same-origin (with notes about popups and cross-site assets).
Always test changes on staging first.
Limitations
- Only one response is graded. Other pages, error pages and assets may send different headers.
- A CSP set with a
<meta>tag is not seen, because the page body is never read. - Some sites send different headers to bots, block cloud IP ranges, or require JavaScript challenges; results then reflect what our server received and are flagged with
reliability: "blocked_or_challenged". For example, google.com currently answers our Cloudflare-hosted scanner with a 429 captcha page. - Only ports 80, 443, 8080 and 8443 can be scanned, and only public addresses.
Abuse reports
HeaderGuard sends ordinary GET requests (headers only, at most a few per scan) with a user agent containing HeaderGuard/0.1; +https://headerguard.mike-tusa.workers.dev/docs, and scans are rate-limited per IP. If you believe it is being used against your site, email digitalpromohub.support@gmail.com with the time (and timezone) and the URL that was requested.
Privacy
See the privacy note: no stored URLs or results beyond 2 minutes of in-memory caching, no logs, no cookies, no analytics.