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).

HTTPcodeWhen
400invalid_url, invalid_schemeMissing or malformed URL, credentials in the URL, a scheme other than http/https
403blocked_target, blocked_port, blocked_redirectPrivate, loopback, link-local, metadata or special-use address or name; a port other than 80/443/8080/8443; a redirect to any of those
405method_not_allowedAnything but GET/HEAD/OPTIONS
422dns_not_foundThe hostname does not exist (NXDOMAIN) or has no A/AAAA records
429rate_limitedToo many scans from your IP (retry after 60 s)
502fetch_failed, too_many_redirects, redirect_loop, bad_redirect, dns_errorThe 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)
504timeoutNo 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.

Checkout — HeaderGuard Pro

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:

[![security headers](https://headerguard.mike-tusa.workers.dev/badge/example.com.svg)](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.

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).

CheckMaxHow points are earned
HTTPS105 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-Security15max-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-Policy25Enforced 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.
Framing10X-Frame-Options: DENY or SAMEORIGIN, or CSP frame-ancestors: 10. ALLOW-FROM, invalid or missing: 0.
X-Content-Type-Options10nosniff: 10. Otherwise 0.
Referrer-Policy10no-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-Policy5At least one feature directive: 5 · only legacy Feature-Policy or only interest-cohort: 2 · missing: 0.
Cross-Origin-Opener-Policy5same-origin: 5 · same-origin-allow-popups / noopener-allow-popups: 4 · unsafe-none or missing: 0.
Cross-Origin-Resource-Policy5same-origin or same-site: 5 · cross-origin: 2 · missing: 0.
Cookies5No 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-Policy0Information only. It can break third-party embeds and is only needed for cross-origin isolation.
X-XSS-Protection0Information 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).

GradeA+ABCDF
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:

Always test changes on staging first.

Limitations

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.