{
  "openapi": "3.1.0",
  "info": {
    "title": "HeaderGuard API",
    "version": "0.4.0",
    "summary": "Website security-headers scanner: HSTS, CSP, framing, COOP/CORP/COEP, cookies, version leaks.",
    "description": "Free JSON API behind https://headerguard.mike-tusa.workers.dev. No key needed: about 30 scans per minute per IP (IPv6 per /64). HeaderGuard Pro ($9/mo, https://buy.polar.sh/polar_cl_isRRVJTATDI60ftjw6GnmlDneRdw46fI2cQcm0n6pnD) allows about 120 scans per minute per license and batch scans of up to 5 URLs. Results are cached for 2 minutes per edge isolate; cache hits do not count. There are no X-RateLimit-* headers; a 429 carries Retry-After. A key that is not a valid HeaderGuard Pro license is rejected (401/403) on every route instead of falling back to the free plan. AI agents can also use the remote MCP server at https://headerguard.mike-tusa.workers.dev/mcp (see /llms.txt).",
    "contact": {
      "name": "HeaderGuard",
      "email": "digitalpromohub.support@gmail.com",
      "url": "https://headerguard.mike-tusa.workers.dev"
    }
  },
  "servers": [
    {
      "url": "https://headerguard.mike-tusa.workers.dev"
    }
  ],
  "externalDocs": {
    "description": "Human-readable API docs",
    "url": "https://headerguard.mike-tusa.workers.dev/docs"
  },
  "tags": [
    {
      "name": "scan"
    },
    {
      "name": "badge"
    },
    {
      "name": "meta"
    },
    {
      "name": "mcp"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "HeaderGuard Pro license key (prefix `HDRG`). Optional."
      },
      "licenseKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-License-Key",
        "description": "Same license key as bearerAuth, alternative header."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "additionalProperties": true,
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "plan": {
                "type": "string",
                "enum": [
                  "free",
                  "pro"
                ]
              },
              "upgradeUrl": {
                "type": "string",
                "format": "uri"
              },
              "limitPerMin": {
                "type": "integer"
              },
              "keyHint": {
                "type": "string",
                "description": "Masked form of the key that was sent (never the full key)"
              },
              "redirects": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "Note": {
        "type": "object",
        "required": [
          "level",
          "text"
        ],
        "properties": {
          "level": {
            "type": "string",
            "enum": [
              "pass",
              "info",
              "warn",
              "fail"
            ]
          },
          "text": {
            "type": "string"
          }
        }
      },
      "Finding": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "points",
          "max",
          "notes"
        ],
        "additionalProperties": true,
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "https",
              "hsts",
              "csp",
              "framing",
              "xcto",
              "referrer",
              "permissions",
              "coop",
              "corp",
              "cookies",
              "coep",
              "xxss",
              "leaks"
            ]
          },
          "name": {
            "type": "string"
          },
          "present": {
            "type": "boolean"
          },
          "value": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pass",
              "warn",
              "fail",
              "info"
            ]
          },
          "points": {
            "type": "number"
          },
          "max": {
            "type": "number"
          },
          "summary": {
            "type": "string"
          },
          "notes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Note"
            }
          }
        }
      },
      "ScanResult": {
        "type": "object",
        "required": [
          "input",
          "url",
          "finalUrl",
          "finalStatus",
          "redirects",
          "httpCheck",
          "score",
          "grade",
          "gradeWithheld",
          "gradeWithheldReason",
          "scoring",
          "findings",
          "reliability",
          "notes",
          "fixes",
          "headers",
          "meta",
          "plan"
        ],
        "properties": {
          "input": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "finalUrl": {
            "type": "string"
          },
          "finalStatus": {
            "type": "integer"
          },
          "redirects": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "httpCheck": {
            "type": "object",
            "description": "Whether plain http:// redirects to HTTPS (and, if the scan ended on another host, `finalHost`)"
          },
          "score": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "null when gradeWithheld"
          },
          "grade": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "A+",
              "A",
              "B",
              "C",
              "D",
              "F",
              null
            ]
          },
          "gradeWithheld": {
            "type": "boolean"
          },
          "gradeWithheldReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "unreliableRawScore": {
            "type": "object",
            "description": "Only when gradeWithheld; never display as a grade"
          },
          "scoring": {
            "type": "object",
            "properties": {
              "base": {
                "type": "integer"
              },
              "leakPenalty": {
                "type": "integer"
              },
              "cappedNoHttps": {
                "type": "boolean"
              },
              "max": {
                "type": "integer",
                "const": 100
              }
            }
          },
          "findings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Finding"
            }
          },
          "reliability": {
            "type": "string",
            "enum": [
              "ok",
              "blocked_or_challenged"
            ]
          },
          "notes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Note"
            }
          },
          "fixes": {
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "combined": {
                "type": [
                  "object",
                  "null"
                ]
              },
              "platforms": {
                "type": "object"
              }
            }
          },
          "headers": {
            "type": "object",
            "description": "Response headers of the final page (cookie values redacted)"
          },
          "meta": {
            "type": "object",
            "properties": {
              "version": {
                "type": "string"
              },
              "scannedAt": {
                "type": "string",
                "format": "date-time"
              },
              "durationMs": {
                "type": "integer"
              },
              "subrequests": {
                "type": "integer"
              }
            }
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro"
            ]
          }
        }
      },
      "JsonRpcMessage": {
        "type": "object",
        "required": [
          "jsonrpc"
        ],
        "properties": {
          "jsonrpc": {
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer"
            ]
          },
          "method": {
            "type": "string"
          },
          "params": {
            "type": "object"
          }
        }
      }
    }
  },
  "paths": {
    "/api/scan": {
      "get": {
        "tags": [
          "scan"
        ],
        "operationId": "scanHeaders",
        "summary": "Scan a website's HTTP security headers",
        "security": [
          {},
          {
            "bearerAuth": []
          },
          {
            "licenseKeyHeader": []
          }
        ],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": false,
            "description": "URL or bare domain (required unless `domain` is given). A bare domain is scanned as https://<domain>/, falling back to plain HTTP if HTTPS does not answer.",
            "schema": {
              "type": "string",
              "maxLength": 2048
            },
            "example": "example.com"
          },
          {
            "name": "domain",
            "in": "query",
            "required": false,
            "description": "Alias for `url`.",
            "schema": {
              "type": "string",
              "maxLength": 2048
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Scan result",
            "headers": {
              "x-cache": {
                "description": "`HIT` (served from the 2-minute result cache; no quota used) or `MISS`",
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS"
                  ]
                }
              },
              "x-plan": {
                "description": "`free` or `pro`",
                "schema": {
                  "type": "string",
                  "enum": [
                    "free",
                    "pro"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanResult"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_url` or `invalid_scheme`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Key sent but not a valid HeaderGuard Pro license: `license_invalid`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`blocked_target` or `blocked_port` (private, internal or non-standard-port targets), or Key sent but `license_expired`, `license_revoked`, `license_inactive` or `license_wrong_product`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`method_not_allowed`: use GET",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`dns_not_found`: the host does not resolve (NXDOMAIN or no A/AAAA records)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` (per-minute limit for your IP or Pro license; includes `upgradeUrl` and `limitPerMin`)",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`fetch_failed`, `dns_error`, `too_many_redirects`, `redirect_loop`, `bad_redirect` or `budget_exceeded`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "`timeout`: the site did not answer in time",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/scan/batch": {
      "post": {
        "tags": [
          "scan"
        ],
        "operationId": "scanBatch",
        "summary": "HeaderGuard Pro: scan up to 5 URLs in one request",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "licenseKeyHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "urls"
                ],
                "properties": {
                  "urls": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5,
                    "items": {
                      "type": "string",
                      "maxLength": 2048
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per URL (each `ok: true` with the scan fields, or `ok: false` with `error`). Each URL counts against the Pro per-minute limit.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plan",
                    "count",
                    "results"
                  ],
                  "properties": {
                    "plan": {
                      "const": "pro"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_urls` or `batch_too_large`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`pro_required` (no key sent) or Key sent but not a valid HeaderGuard Pro license: `license_invalid`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Key sent but `license_expired`, `license_revoked`, `license_inactive` or `license_wrong_product`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`method_not_allowed`: use POST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` (Pro per-minute limit)",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "tags": [
          "meta"
        ],
        "operationId": "health",
        "summary": "Liveness and deployed version",
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "service",
                    "version",
                    "publicLaunch",
                    "build",
                    "deployId",
                    "deployedAt"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "service": {
                      "const": "headerguard"
                    },
                    "version": {
                      "type": "string",
                      "examples": [
                        "0.4.0"
                      ]
                    },
                    "publicLaunch": {
                      "type": "boolean"
                    },
                    "build": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "deployId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "deployedAt": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Key sent but not a valid HeaderGuard Pro license: `license_invalid`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Key sent but `license_expired`, `license_revoked`, `license_inactive` or `license_wrong_product`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/badge/{host}.svg": {
      "get": {
        "tags": [
          "badge"
        ],
        "operationId": "badge",
        "summary": "Embeddable grade badge (SVG)",
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "host",
            "in": "path",
            "required": true,
            "description": "Bare hostname (no scheme, path, port or IP literal)",
            "schema": {
              "type": "string",
              "maxLength": 253
            },
            "example": "example.com"
          }
        ],
        "responses": {
          "200": {
            "description": "Always 200 so embeds don't break: the grade, or a grey `not graded` / `unknown` / `try later` badge. Graded badges are cached about 24 h, errors about 1 h. Uncached badges count against a per-IP badge limit of about 30/min.",
            "headers": {
              "x-cache": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS"
                  ]
                }
              },
              "x-badge-status": {
                "description": "`graded`, `not_graded`, `rate_limited` or an error code",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Key sent but not a valid HeaderGuard Pro license: `license_invalid`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Key sent but `license_expired`, `license_revoked`, `license_inactive` or `license_wrong_product`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": [
          "mcp"
        ],
        "operationId": "mcp",
        "summary": "Remote MCP server (Streamable HTTP, JSON-RPC 2.0)",
        "description": "Model Context Protocol endpoint. Stateless, JSON responses only (no SSE, no Mcp-Session-Id). Supported protocol versions: 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05. Modern (2026-07-28) requests carry params._meta and the MCP-Protocol-Version, Mcp-Method and Mcp-Name headers; legacy clients use initialize. Tool: scan_headers. Same keys, cache and limits as /api/scan; each tools/call counts as one scan. Body max 65536 bytes; legacy batches max 10 messages with at most one tools/call.",
        "security": [
          {},
          {
            "bearerAuth": []
          },
          {
            "licenseKeyHeader": []
          }
        ],
        "parameters": [
          {
            "name": "MCP-Protocol-Version",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "2026-07-28",
                "2025-11-25",
                "2025-06-18",
                "2025-03-26",
                "2024-11-05"
              ]
            }
          },
          {
            "name": "Mcp-Method",
            "in": "header",
            "required": false,
            "description": "Required for 2026-07-28; must equal the body method",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Mcp-Name",
            "in": "header",
            "required": false,
            "description": "Required for 2026-07-28 tools/call; must equal params.name",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/JsonRpcMessage"
                  },
                  {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "$ref": "#/components/schemas/JsonRpcMessage"
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response (or batch of responses). Tool failures, including rate limits, are results with isError: true (and a Retry-After header when rate limited).",
            "headers": {
              "x-cache": {
                "description": "`HIT` (served from the 2-minute result cache; no quota used) or `MISS`",
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS"
                  ]
                }
              },
              "x-plan": {
                "description": "`free` or `pro`",
                "schema": {
                  "type": "string",
                  "enum": [
                    "free",
                    "pro"
                  ]
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "array"
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Notification(s) accepted; no body"
          },
          "400": {
            "description": "Parse error (-32700), invalid request (-32600), header mismatch (-32020), unsupported protocol version (-32022) or missing _meta (-32602)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Key sent but not a valid HeaderGuard Pro license (JSON-RPC error -32000, data.code = license_invalid)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "description": "Origin header present but not allowed, or a license that is expired, revoked, inactive or for another product (data.code = license_*)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Unknown method in a 2026-07-28 request (-32601)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "413": {
            "description": "Body larger than the cap",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Too many non-scan MCP messages from this IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected internal error (`internal_error`); per-message failures are JSON-RPC -32603 inside a 200 response instead",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Service temporarily unavailable (Cloudflare platform error; the Worker itself does not emit 503)",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "mcp"
        ],
        "operationId": "mcp_get_not_allowed",
        "summary": "Not supported (stateless server: no SSE stream, no sessions)",
        "responses": {
          "405": {
            "description": "Always 405 with `Allow: POST, OPTIONS` and a JSON-RPC error body",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "POST, OPTIONS"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "mcp"
        ],
        "operationId": "mcp_delete_not_allowed",
        "summary": "Not supported (stateless server: no SSE stream, no sessions)",
        "responses": {
          "405": {
            "description": "Always 405 with `Allow: POST, OPTIONS` and a JSON-RPC error body",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "POST, OPTIONS"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  }
}