{
  "openapi": "3.1.0",
  "info": {
    "title": "HTTPS or Not — Public API",
    "version": "1.4.1",
    "summary": "HTTPS / SSL / security-headers / HTTP3 check as JSON. Optional account API key.",
    "description": "Machine-readable HTTPS, SSL, security-headers, HTTP/2 + HTTP/3, CAA, DNSSEC, HSTS preload and mixed-content check.\n\n- **Preferred endpoint:** `GET /api/v1/check?domain=` (aliases: `/api/porcelain`, `/api/public/check`).\n- **Anonymous:** 120 requests/hour per IP.\n- **Optional API key:** `X-API-Key` or `Authorization: Bearer` (create in the dashboard). Skips the per-IP cap. The hourly burst is per key; the monthly quota is shared by all keys on the account and set by the plan (Free 10,000 / Solo 100,000 / Agency 500,000 per UTC calendar month). We store a hash of the key, never the secret after issuance. Keys apply only to this public check endpoint, not to the site bulk form or `POST /check-bulk`.\n- **CI policy (optional):** `minGrade`, `headersMinGrade`, `requireHsts` — policy failure returns **422** with the full result plus `policyFailures`.\n- Same payload shape as the in-app checker. Grading contract: https://httpsornot.com/methodology\n\nSee https://httpsornot.com/api-docs.\n\n> Authenticated backend routes used by the httpsornot.com frontend (except this public check) are not part of this public contract.",
    "license": {
      "name": "Free to use, attribution appreciated",
      "url": "https://httpsornot.com/terms"
    },
    "contact": {
      "name": "httpsornot.com",
      "url": "https://httpsornot.com/support"
    }
  },
  "servers": [
    {
      "url": "https://httpsornot.com",
      "description": "Production (Cloudflare Pages, proxied to backend)"
    }
  ],
  "tags": [
    {
      "name": "Check",
      "description": "Run a domain HTTPS / SSL / security check."
    },
    {
      "name": "System",
      "description": "Service health and metadata."
    }
  ],
  "paths": {
    "/api/porcelain": {
      "get": {
        "tags": [
          "Check"
        ],
        "operationId": "getPorcelainCheck",
        "summary": "Run HTTPS check (porcelain alias)",
        "description": "Stable alias of `GET /api/v1/check`. Named after `git --porcelain`: machine-readable JSON. Prefer `/api/v1/check` in new integrations.",
        "security": [
          {},
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "description": "Hostname to check. Scheme and path are stripped before processing.",
            "schema": {
              "type": "string",
              "minLength": 3,
              "maxLength": 253,
              "examples": [
                "example.com",
                "github.com"
              ]
            }
          },
          {
            "name": "minGrade",
            "in": "query",
            "required": false,
            "description": "Minimum composite `sslGrade.grade` (inclusive). Scale: A+, A, A-, B+, B, C, F. Failure → 422. See /methodology. Different from headersMinGrade.",
            "schema": {
              "type": "string",
              "enum": [
                "A+",
                "A",
                "A-",
                "B+",
                "B",
                "C",
                "F"
              ]
            }
          },
          {
            "name": "headersMinGrade",
            "in": "query",
            "required": false,
            "description": "Minimum `securityHeaders.grade` (inclusive). Headers-only scale: A+, A, B, C, D, F (no A-/B+). Closest to legacy SecurityHeaders.com X-Grade. Failure → 422.",
            "schema": {
              "type": "string",
              "enum": [
                "A+",
                "A",
                "B",
                "C",
                "D",
                "F"
              ]
            }
          },
          {
            "name": "requireHsts",
            "in": "query",
            "required": false,
            "description": "If true, require `hstsStatus.enabled`. Failure → 422.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Check completed. May include `error` if the target was unreachable. With CI policy params and a pass, `policyOk` is true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckResult"
                },
                "examples": {
                  "happyPath": {
                    "summary": "Healthy site (HTTPS, A grade, headers present)",
                    "value": {
                      "domain": "example.com",
                      "httpRedirects": true,
                      "httpsWorks": true,
                      "httpPort80Reachable": true,
                      "redirectChain": [
                        "http://example.com/",
                        "https://example.com/"
                      ],
                      "totalResponseTime": 184,
                      "statusCode": 200,
                      "finalUrl": "https://example.com/",
                      "checkedAt": "2026-04-22T10:15:30.000Z",
                      "sslCertificate": {
                        "valid": true,
                        "issuer": "DigiCert Inc",
                        "subject": "CN=*.example.com",
                        "subjectAltNames": [
                          "example.com",
                          "*.example.com"
                        ],
                        "domainMatchesCertificate": true,
                        "validFrom": "2026-01-15T00:00:00.000Z",
                        "validTo": "2027-02-15T23:59:59.000Z",
                        "daysUntilExpiry": 299
                      },
                      "sslGrade": {
                        "grade": "A",
                        "score": 90,
                        "label": "Very Good",
                        "gradingVersion": "3.1",
                        "factors": [
                          {
                            "name": "HTTPS Connection",
                            "passed": true,
                            "points": 25,
                            "maxPoints": 25
                          },
                          {
                            "name": "HTTP to HTTPS Redirect",
                            "passed": true,
                            "points": 20,
                            "maxPoints": 20
                          },
                          {
                            "name": "Valid SSL Certificate",
                            "passed": true,
                            "points": 20,
                            "maxPoints": 20
                          },
                          {
                            "name": "Certificate Not Expiring Soon",
                            "passed": true,
                            "points": 10,
                            "maxPoints": 10
                          },
                          {
                            "name": "Short Redirect Chain",
                            "passed": true,
                            "points": 5,
                            "maxPoints": 5
                          },
                          {
                            "name": "HSTS Enabled",
                            "passed": false,
                            "points": 0,
                            "maxPoints": 5
                          },
                          {
                            "name": "Security Headers",
                            "passed": false,
                            "points": 0,
                            "maxPoints": 5
                          },
                          {
                            "name": "Modern TLS (no 1.0/1.1)",
                            "passed": true,
                            "points": 10,
                            "maxPoints": 10
                          }
                        ],
                        "improvementsForNextGrade": [
                          "Enable HSTS with a long max-age and preload directive.",
                          "Add Content-Security-Policy header."
                        ]
                      },
                      "tlsProtocols": {
                        "tls10": false,
                        "tls11": false,
                        "tls12": true,
                        "tls13": true,
                        "minSupported": "TLS 1.2",
                        "maxSupported": "TLS 1.3",
                        "insecureProtocolsEnabled": false
                      },
                      "http2": {
                        "supported": true,
                        "alpnProtocol": "h2"
                      },
                      "http3": {
                        "supported": true,
                        "altSvc": "h3=\":443\"; ma=2592000"
                      },
                      "hstsStatus": {
                        "enabled": false
                      },
                      "securityHeaders": {
                        "passed": 3,
                        "total": 6,
                        "grade": "C",
                        "headers": [
                          {
                            "name": "HSTS",
                            "present": false,
                            "recommendation": "max-age >= 31536000; includeSubDomains; preload"
                          },
                          {
                            "name": "CSP",
                            "present": false,
                            "recommendation": "Define a strict policy"
                          },
                          {
                            "name": "X-Content-Type-Options",
                            "present": true,
                            "value": "nosniff"
                          },
                          {
                            "name": "X-Frame-Options",
                            "present": true,
                            "value": "DENY"
                          },
                          {
                            "name": "Referrer-Policy",
                            "present": true,
                            "value": "strict-origin-when-cross-origin"
                          },
                          {
                            "name": "Permissions-Policy",
                            "present": false
                          }
                        ]
                      },
                      "caa": {
                        "present": true,
                        "issuers": [
                          "digicert.com",
                          "letsencrypt.org"
                        ]
                      },
                      "dnssec": {
                        "validated": true
                      },
                      "hstsPreloadList": {
                        "onPreloadList": false
                      }
                    }
                  },
                  "blockedDomain": {
                    "summary": "Domain not allowed (e.g. localhost / IP)",
                    "value": {
                      "domain": "localhost",
                      "httpRedirects": false,
                      "httpsWorks": false,
                      "redirectChain": [],
                      "error": "Checks are not allowed for this domain"
                    }
                  },
                  "botProtection": {
                    "summary": "CDN / bot challenge blocked the headers probe — grade withheld",
                    "value": {
                      "domain": "protected.example",
                      "httpRedirects": true,
                      "httpsWorks": true,
                      "httpPort80Reachable": true,
                      "redirectChain": [
                        "http://protected.example/",
                        "https://protected.example/"
                      ],
                      "botProtectionBlocked": true,
                      "error": "Bot protection blocked the check; headers and grade are withheld.",
                      "checkedAt": "2026-08-02T10:15:30.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid domain or invalid policy parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "401": {
            "description": "An API key was supplied but is invalid or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "422": {
            "description": "Check completed but CI policy failed (or bot protection blocked while policy was requested). Body includes the full CheckResult plus `policyOk: false` and `policyFailures`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PolicyFailureResult"
                },
                "example": {
                  "domain": "example.com",
                  "httpRedirects": true,
                  "httpsWorks": true,
                  "redirectChain": [
                    "https://example.com/"
                  ],
                  "sslGrade": {
                    "grade": "C",
                    "score": 50,
                    "label": "Fair",
                    "gradingVersion": "3.1",
                    "factors": []
                  },
                  "policyOk": false,
                  "error": "Policy failed",
                  "policyFailures": [
                    {
                      "code": "min_grade",
                      "message": "sslGrade.grade is C, below minGrade=B."
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Too many requests. Anonymous calls use 120/hour per IP. Keyed calls use a per-key hourly burst and an account-wide monthly quota. This is not a policy failure (those are 422). See https://httpsornot.com/api-docs#api-keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "Too many requests",
                  "limit": "120 requests per hour per IP",
                  "hint": "https://httpsornot.com/api-docs#rate-limit"
                }
              }
            }
          },
          "502": {
            "description": "Upstream check failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/check": {
      "get": {
        "tags": [
          "Check"
        ],
        "operationId": "getPublicCheckAlias",
        "summary": "Alias for /api/v1/check",
        "description": "Identical to `GET /api/v1/check` / `/api/porcelain`.",
        "security": [
          {},
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "description": "Hostname to check. Scheme and path are stripped before processing.",
            "schema": {
              "type": "string",
              "minLength": 3,
              "maxLength": 253,
              "examples": [
                "example.com",
                "github.com"
              ]
            }
          },
          {
            "name": "minGrade",
            "in": "query",
            "required": false,
            "description": "Minimum composite `sslGrade.grade` (inclusive). Scale: A+, A, A-, B+, B, C, F. Failure → 422. See /methodology. Different from headersMinGrade.",
            "schema": {
              "type": "string",
              "enum": [
                "A+",
                "A",
                "A-",
                "B+",
                "B",
                "C",
                "F"
              ]
            }
          },
          {
            "name": "headersMinGrade",
            "in": "query",
            "required": false,
            "description": "Minimum `securityHeaders.grade` (inclusive). Headers-only scale: A+, A, B, C, D, F (no A-/B+). Closest to legacy SecurityHeaders.com X-Grade. Failure → 422.",
            "schema": {
              "type": "string",
              "enum": [
                "A+",
                "A",
                "B",
                "C",
                "D",
                "F"
              ]
            }
          },
          {
            "name": "requireHsts",
            "in": "query",
            "required": false,
            "description": "If true, require `hstsStatus.enabled`. Failure → 422.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Check completed. May include `error` if the target was unreachable. With CI policy params and a pass, `policyOk` is true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckResult"
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid domain or invalid policy parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "401": {
            "description": "An API key was supplied but is invalid or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "422": {
            "description": "Check completed but CI policy failed (or bot protection blocked while policy was requested). Body includes the full CheckResult plus `policyOk: false` and `policyFailures`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PolicyFailureResult"
                },
                "example": {
                  "domain": "example.com",
                  "httpRedirects": true,
                  "httpsWorks": true,
                  "redirectChain": [
                    "https://example.com/"
                  ],
                  "sslGrade": {
                    "grade": "C",
                    "score": 50,
                    "label": "Fair",
                    "gradingVersion": "3.1",
                    "factors": []
                  },
                  "policyOk": false,
                  "error": "Policy failed",
                  "policyFailures": [
                    {
                      "code": "min_grade",
                      "message": "sslGrade.grade is C, below minGrade=B."
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Too many requests. Anonymous calls use 120/hour per IP. Keyed calls use a per-key hourly burst and an account-wide monthly quota. This is not a policy failure (those are 422). See https://httpsornot.com/api-docs#api-keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "Too many requests",
                  "limit": "120 requests per hour per IP",
                  "hint": "https://httpsornot.com/api-docs#rate-limit"
                }
              }
            }
          },
          "502": {
            "description": "Upstream check failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "tags": [
          "System"
        ],
        "operationId": "getHealth",
        "summary": "Service health check",
        "description": "Returns a simple status payload for monitoring. Not rate limited.",
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                },
                "example": {
                  "status": "ok",
                  "service": "cert-backend",
                  "timestamp": "2026-04-22T10:15:30.000Z"
                }
              }
            }
          },
          "502": {
            "description": "Upstream backend unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/check": {
      "get": {
        "tags": [
          "Check"
        ],
        "operationId": "getV1Check",
        "summary": "Run HTTPS check (versioned, preferred)",
        "description": "Preferred public endpoint. Returns a full check result as JSON. Anonymous: **120 requests per hour per IP**. With an account API key (`X-API-Key` / `Authorization: Bearer`): per-key hourly burst plus one plan-based monthly quota shared by all account keys (Free 10,000 / Solo 100,000 / Agency 500,000 per UTC calendar month); see https://httpsornot.com/api-docs#api-keys. One valid domain request counts as one check, including cache hits, unreachable targets, and CI policy responses.\n\nOptional CI policy query params (`minGrade`, `headersMinGrade`, `requireHsts`) return **422** when the check runs but the policy fails — suitable for `curl -f` in GitHub Actions.\n\nAliases (same behaviour): `/api/porcelain`, `/api/public/check`.",
        "security": [
          {},
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "description": "Hostname to check. Scheme and path are stripped before processing.",
            "schema": {
              "type": "string",
              "minLength": 3,
              "maxLength": 253,
              "examples": [
                "example.com",
                "github.com"
              ]
            }
          },
          {
            "name": "minGrade",
            "in": "query",
            "required": false,
            "description": "Minimum composite `sslGrade.grade` (inclusive). Scale: A+, A, A-, B+, B, C, F. Failure → 422. See /methodology. Different from headersMinGrade.",
            "schema": {
              "type": "string",
              "enum": [
                "A+",
                "A",
                "A-",
                "B+",
                "B",
                "C",
                "F"
              ]
            }
          },
          {
            "name": "headersMinGrade",
            "in": "query",
            "required": false,
            "description": "Minimum `securityHeaders.grade` (inclusive). Headers-only scale: A+, A, B, C, D, F (no A-/B+). Closest to legacy SecurityHeaders.com X-Grade. Failure → 422.",
            "schema": {
              "type": "string",
              "enum": [
                "A+",
                "A",
                "B",
                "C",
                "D",
                "F"
              ]
            }
          },
          {
            "name": "requireHsts",
            "in": "query",
            "required": false,
            "description": "If true, require `hstsStatus.enabled`. Failure → 422.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Check completed. If CI policy params were set and passed, `policyOk` is true.",
            "headers": {
              "X-Quota-Limit": {
                "description": "Account monthly quota; present on keyed requests.",
                "schema": { "type": "integer" }
              },
              "X-Quota-Remaining": {
                "description": "Checks remaining in the account UTC calendar-month quota.",
                "schema": { "type": "integer" }
              },
              "X-Quota-Reset": {
                "description": "UTC timestamp when the account monthly quota resets.",
                "schema": { "type": "string", "format": "date-time" }
              },
              "X-RateLimit-Limit": {
                "description": "Hourly burst limit for this key.",
                "schema": { "type": "integer" }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in this key's current UTC hour.",
                "schema": { "type": "integer" }
              },
              "X-RateLimit-Reset": {
                "description": "UTC timestamp when this key's hourly burst resets.",
                "schema": { "type": "string", "format": "date-time" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckResult"
                },
                "examples": {
                  "happyPath": {
                    "summary": "Healthy site (HTTPS, A grade, headers present)",
                    "value": {
                      "domain": "example.com",
                      "httpRedirects": true,
                      "httpsWorks": true,
                      "httpPort80Reachable": true,
                      "redirectChain": [
                        "http://example.com/",
                        "https://example.com/"
                      ],
                      "totalResponseTime": 184,
                      "statusCode": 200,
                      "finalUrl": "https://example.com/",
                      "checkedAt": "2026-04-22T10:15:30.000Z",
                      "sslCertificate": {
                        "valid": true,
                        "issuer": "DigiCert Inc",
                        "subject": "CN=*.example.com",
                        "subjectAltNames": [
                          "example.com",
                          "*.example.com"
                        ],
                        "domainMatchesCertificate": true,
                        "validFrom": "2026-01-15T00:00:00.000Z",
                        "validTo": "2027-02-15T23:59:59.000Z",
                        "daysUntilExpiry": 299
                      },
                      "sslGrade": {
                        "grade": "A",
                        "score": 90,
                        "label": "Very Good",
                        "gradingVersion": "3.1",
                        "factors": [
                          {
                            "name": "HTTPS Connection",
                            "passed": true,
                            "points": 25,
                            "maxPoints": 25
                          },
                          {
                            "name": "HTTP to HTTPS Redirect",
                            "passed": true,
                            "points": 20,
                            "maxPoints": 20
                          },
                          {
                            "name": "Valid SSL Certificate",
                            "passed": true,
                            "points": 20,
                            "maxPoints": 20
                          },
                          {
                            "name": "Certificate Not Expiring Soon",
                            "passed": true,
                            "points": 10,
                            "maxPoints": 10
                          },
                          {
                            "name": "Short Redirect Chain",
                            "passed": true,
                            "points": 5,
                            "maxPoints": 5
                          },
                          {
                            "name": "HSTS Enabled",
                            "passed": false,
                            "points": 0,
                            "maxPoints": 5
                          },
                          {
                            "name": "Security Headers",
                            "passed": false,
                            "points": 0,
                            "maxPoints": 5
                          },
                          {
                            "name": "Modern TLS (no 1.0/1.1)",
                            "passed": true,
                            "points": 10,
                            "maxPoints": 10
                          }
                        ],
                        "improvementsForNextGrade": [
                          "Enable HSTS with a long max-age and preload directive.",
                          "Add Content-Security-Policy header."
                        ]
                      },
                      "tlsProtocols": {
                        "tls10": false,
                        "tls11": false,
                        "tls12": true,
                        "tls13": true,
                        "minSupported": "TLS 1.2",
                        "maxSupported": "TLS 1.3",
                        "insecureProtocolsEnabled": false
                      },
                      "http2": {
                        "supported": true,
                        "alpnProtocol": "h2"
                      },
                      "http3": {
                        "supported": true,
                        "altSvc": "h3=\":443\"; ma=2592000"
                      },
                      "hstsStatus": {
                        "enabled": false
                      },
                      "securityHeaders": {
                        "passed": 3,
                        "total": 6,
                        "grade": "C",
                        "headers": [
                          {
                            "name": "HSTS",
                            "present": false,
                            "recommendation": "max-age >= 31536000; includeSubDomains; preload"
                          },
                          {
                            "name": "CSP",
                            "present": false,
                            "recommendation": "Define a strict policy"
                          },
                          {
                            "name": "X-Content-Type-Options",
                            "present": true,
                            "value": "nosniff"
                          },
                          {
                            "name": "X-Frame-Options",
                            "present": true,
                            "value": "DENY"
                          },
                          {
                            "name": "Referrer-Policy",
                            "present": true,
                            "value": "strict-origin-when-cross-origin"
                          },
                          {
                            "name": "Permissions-Policy",
                            "present": false
                          }
                        ]
                      },
                      "caa": {
                        "present": true,
                        "issuers": [
                          "digicert.com",
                          "letsencrypt.org"
                        ]
                      },
                      "dnssec": {
                        "validated": true
                      },
                      "hstsPreloadList": {
                        "onPreloadList": false
                      }
                    }
                  },
                  "blockedDomain": {
                    "summary": "Domain not allowed (e.g. localhost / IP)",
                    "value": {
                      "domain": "localhost",
                      "httpRedirects": false,
                      "httpsWorks": false,
                      "redirectChain": [],
                      "error": "Checks are not allowed for this domain"
                    }
                  },
                  "botProtection": {
                    "summary": "CDN / bot challenge blocked the headers probe — grade withheld",
                    "value": {
                      "domain": "protected.example",
                      "httpRedirects": true,
                      "httpsWorks": true,
                      "httpPort80Reachable": true,
                      "redirectChain": [
                        "http://protected.example/",
                        "https://protected.example/"
                      ],
                      "botProtectionBlocked": true,
                      "error": "Bot protection blocked the check; headers and grade are withheld.",
                      "checkedAt": "2026-08-02T10:15:30.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid domain or invalid policy parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "401": {
            "description": "An API key was supplied but is invalid or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "422": {
            "description": "Check completed but CI policy failed (or bot protection blocked while policy was requested). Body includes the full CheckResult plus `policyOk: false` and `policyFailures`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PolicyFailureResult"
                },
                "example": {
                  "domain": "example.com",
                  "httpRedirects": true,
                  "httpsWorks": true,
                  "redirectChain": [
                    "https://example.com/"
                  ],
                  "sslGrade": {
                    "grade": "C",
                    "score": 50,
                    "label": "Fair",
                    "gradingVersion": "3.1",
                    "factors": []
                  },
                  "policyOk": false,
                  "error": "Policy failed",
                  "policyFailures": [
                    {
                      "code": "min_grade",
                      "message": "sslGrade.grade is C, below minGrade=B."
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Too many requests. Anonymous calls use 120/hour per IP. Keyed calls use a per-key hourly burst and an account-wide monthly quota. This is not a policy failure (those are 422). See https://httpsornot.com/api-docs#api-keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "example": {
                  "error": "Too many requests",
                  "limit": "120 requests per hour per IP",
                  "hint": "https://httpsornot.com/api-docs#rate-limit"
                }
              }
            }
          },
          "502": {
            "description": "Upstream check failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Optional account API key from https://httpsornot.com/dashboard"
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Same key as X-API-Key, sent as Authorization: Bearer"
      }
    },
    "schemas": {
      "ErrorBody": {
        "type": "object",
        "description": "Generic error envelope. `hint` and `domain` are optional context fields.",
        "properties": {
          "error": {
            "type": "string"
          },
          "hint": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "limit": {
            "description": "Human-readable limit that was exceeded.",
            "type": "string"
          },
          "remaining": {
            "type": "integer",
            "minimum": 0
          },
          "resetAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "HealthStatus": {
        "type": "object",
        "required": [
          "status",
          "service",
          "timestamp"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "service": {
            "type": "string",
            "examples": [
              "cert-backend"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RedirectStepDetail": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "statusCode": {
            "type": "integer",
            "minimum": 100,
            "maximum": 599
          },
          "responseTime": {
            "type": "number",
            "description": "Time in milliseconds for this hop."
          }
        }
      },
      "SSLCertificateInfo": {
        "type": "object",
        "required": [
          "valid"
        ],
        "description": "Parsed SSL/TLS certificate. `daysUntilExpiry` may be negative for expired certs.",
        "properties": {
          "valid": {
            "type": "boolean"
          },
          "issuer": {
            "type": "string",
            "examples": [
              "Let's Encrypt",
              "DigiCert Inc"
            ]
          },
          "subject": {
            "type": "string",
            "examples": [
              "CN=example.com"
            ]
          },
          "subjectAltNames": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Subject Alternative Names from the cert (DNS entries)."
          },
          "serialNumber": {
            "type": "string"
          },
          "fingerprint": {
            "type": "string",
            "description": "Cert fingerprint (SHA-256 preferred, otherwise SHA-1)."
          },
          "fingerprintAlgorithm": {
            "type": "string",
            "enum": [
              "SHA-256",
              "SHA-1"
            ]
          },
          "domainMatchesCertificate": {
            "type": "boolean",
            "description": "True if the checked domain is covered by CN or a SAN (including wildcard match)."
          },
          "validFrom": {
            "type": "string",
            "format": "date-time"
          },
          "validTo": {
            "type": "string",
            "format": "date-time"
          },
          "daysUntilExpiry": {
            "type": "integer"
          },
          "ocspStapled": {
            "type": "boolean",
            "description": "True when the TLS handshake included a non-empty OCSP staple. Informational; does not affect sslGrade."
          },
          "error": {
            "type": "string"
          }
        }
      },
      "SecurityHeader": {
        "type": "object",
        "required": [
          "name",
          "present"
        ],
        "description": "`present` means the header was sent AND its value actually provides the protection. Weak values set `present: false` and `weakValue: true` so UI can show \"Weak\" instead of \"Missing\".",
        "properties": {
          "name": {
            "type": "string",
            "examples": [
              "HSTS",
              "CSP",
              "X-Content-Type-Options",
              "X-Frame-Options",
              "Referrer-Policy",
              "Permissions-Policy"
            ]
          },
          "present": {
            "type": "boolean",
            "description": "True only when the header is sent with a value that delivers the intended protection."
          },
          "value": {
            "type": "string",
            "description": "Raw header value when the server sent one (including weak values)."
          },
          "recommendation": {
            "type": "string",
            "description": "Suggested value when missing or weak (informational)."
          },
          "weakValue": {
            "type": "boolean",
            "description": "True when the header was sent but with a value that does not deliver the protection (e.g. HSTS max-age=0, Referrer-Policy no-referrer-when-downgrade). Counted as not passed."
          }
        }
      },
      "SecurityHeadersResult": {
        "type": "object",
        "required": [
          "passed",
          "total",
          "headers"
        ],
        "description": "Six security headers are evaluated. CSP is weighted heavily and is required for an A+ grade.",
        "properties": {
          "passed": {
            "type": "integer",
            "minimum": 0,
            "maximum": 6
          },
          "total": {
            "type": "integer",
            "examples": [
              6
            ]
          },
          "headers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SecurityHeader"
            }
          },
          "grade": {
            "type": "string",
            "description": "Letter grade derived from how many headers passed."
          },
          "cspWarnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Informational warnings about CSP weaknesses (`unsafe-inline`, `unsafe-eval`, …). No grade impact."
          },
          "xssProtectionWarning": {
            "type": "string",
            "description": "Set when X-XSS-Protection is present with a non-zero value (deprecated and risky)."
          },
          "htmlCacheNoStoreWarning": {
            "type": "string",
            "description": "Set when the HTML response uses `Cache-Control: no-store` (disables conditional-request caching)."
          }
        }
      },
      "CookieFlags": {
        "type": "object",
        "required": [
          "secure",
          "httpOnly"
        ],
        "description": "Parsed Secure / HttpOnly / SameSite flags from a single Set-Cookie header.",
        "properties": {
          "secure": {
            "type": "boolean"
          },
          "httpOnly": {
            "type": "boolean"
          },
          "sameSite": {
            "type": "string",
            "examples": [
              "Strict",
              "Lax",
              "None"
            ],
            "description": "Original SameSite value as advertised by the server (case preserved)."
          }
        }
      },
      "CookieSecurityResult": {
        "type": "object",
        "required": [
          "cookies"
        ],
        "properties": {
          "cookies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CookieFlags"
            }
          }
        }
      },
      "InfoDisclosureHeader": {
        "type": "object",
        "required": [
          "name",
          "value"
        ],
        "description": "Headers that may leak server / tech-stack info (Server, X-Powered-By, X-Generator). Informational only — no grade impact.",
        "properties": {
          "name": {
            "type": "string"
          },
          "value": {
            "type": "string"
          }
        }
      },
      "HstsStatus": {
        "type": "object",
        "required": [
          "enabled"
        ],
        "description": "Parsed Strict-Transport-Security state. `enabled=false` when header is absent or `max-age=0`.",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "maxAge": {
            "type": "integer",
            "minimum": 0
          },
          "includeSubDomains": {
            "type": "boolean"
          },
          "preload": {
            "type": "boolean",
            "description": "Server advertises the `preload` directive."
          },
          "preloadStatus": {
            "type": "string",
            "enum": [
              "preloaded",
              "pending",
              "eligible",
              "not-preloaded",
              "unknown"
            ],
            "description": "Authoritative preload-list state. `eligible` means the header asks for preload but the domain is not yet on the list."
          }
        }
      },
      "SSLGrade": {
        "type": "object",
        "required": [
          "grade",
          "score",
          "label",
          "gradingVersion",
          "factors"
        ],
        "description": "Composite security grade based on eight weighted factors (max 100 points). Absent when the check was blocked by bot protection — see `botProtectionBlocked`.",
        "properties": {
          "grade": {
            "type": "string",
            "enum": [
              "A+",
              "A",
              "A-",
              "B+",
              "B",
              "C",
              "F"
            ]
          },
          "score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "label": {
            "type": "string",
            "examples": [
              "Excellent",
              "Very Good",
              "Good",
              "Fair",
              "Poor"
            ]
          },
          "gradingVersion": {
            "type": "string",
            "description": "Version of the grading contract that produced this result. Bumped whenever weights, thresholds, or pass/fail semantics change — i.e. whenever the same site could score differently than before. Pin this if you gate a build on the grade, and treat a change as a signal to re-baseline rather than a regression in your site.",
            "examples": [
              "3.1"
            ]
          },
          "factors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "passed",
                "points",
                "maxPoints"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "passed": {
                  "type": "boolean"
                },
                "points": {
                  "type": "integer",
                  "minimum": 0
                },
                "maxPoints": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          },
          "improvementsForNextGrade": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Short tips on what to fix to reach the next grade tier."
          }
        }
      },
      "MixedContentResource": {
        "type": "object",
        "required": [
          "url",
          "type",
          "category"
        ],
        "properties": {
          "url": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "image",
              "script",
              "stylesheet",
              "iframe",
              "video",
              "audio"
            ]
          },
          "category": {
            "type": "string",
            "enum": [
              "passive",
              "active"
            ],
            "description": "`active` = blocked by modern browsers; `passive` = warning only."
          }
        }
      },
      "MixedContentResult": {
        "type": "object",
        "required": [
          "detected",
          "resources"
        ],
        "description": "HTTP-loaded sub-resources detected on an HTTPS page.",
        "properties": {
          "detected": {
            "type": "boolean"
          },
          "resources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MixedContentResource"
            }
          }
        }
      },
      "TlsProtocolsResult": {
        "type": "object",
        "required": [
          "tls10",
          "tls11",
          "tls12",
          "tls13"
        ],
        "description": "Which TLS versions the server actually accepts. Probed via direct TLS handshakes (not just self-reported).",
        "properties": {
          "tls10": {
            "type": "boolean"
          },
          "tls11": {
            "type": "boolean"
          },
          "tls12": {
            "type": "boolean"
          },
          "tls13": {
            "type": "boolean"
          },
          "minSupported": {
            "type": "string",
            "examples": [
              "TLS 1.2"
            ]
          },
          "maxSupported": {
            "type": "string",
            "examples": [
              "TLS 1.3"
            ]
          },
          "insecureProtocolsEnabled": {
            "type": "boolean",
            "description": "True if TLS 1.0 or 1.1 is still accepted."
          }
        }
      },
      "Http2Result": {
        "type": "object",
        "required": [
          "supported"
        ],
        "description": "HTTP/2 negotiated via TLS ALPN on port 443.",
        "properties": {
          "supported": {
            "type": "boolean",
            "description": "True iff the ALPN handshake selected `h2`."
          },
          "alpnProtocol": {
            "type": "string",
            "examples": [
              "h2",
              "http/1.1"
            ],
            "description": "Whatever ALPN protocol was actually negotiated."
          }
        }
      },
      "Http3Result": {
        "type": "object",
        "required": [
          "supported"
        ],
        "description": "HTTP/3 detection has two layers:\n- `supported` — server advertises HTTP/3 via Alt-Svc (`h3`, `h3-*`, or `quic`).\n- `confirmed` — we actively negotiated HTTP/3 with the server (QUIC over UDP/443).\n\nWhen `confirmed` is omitted the active probe was unavailable on the backend host (e.g. the shipped `curl` was built without HTTP/3 support).",
        "properties": {
          "supported": {
            "type": "boolean"
          },
          "altSvc": {
            "type": "string",
            "description": "Raw Alt-Svc header value when HTTP/3 is advertised.",
            "examples": [
              "h3=\":443\"; ma=2592000"
            ]
          },
          "confirmed": {
            "type": "boolean",
            "description": "true = HTTP/3 negotiated, false = tried but server fell back to h2/h1, omitted = probe unavailable on backend."
          },
          "confirmMethod": {
            "type": "string",
            "enum": [
              "curl"
            ],
            "description": "How the active confirmation was obtained."
          }
        }
      },
      "CaaResult": {
        "type": "object",
        "required": [
          "present"
        ],
        "description": "DNS Certification Authority Authorization record state for the apex domain.",
        "properties": {
          "present": {
            "type": "boolean"
          },
          "issuers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Allowed issuer domains when CAA is present.",
            "examples": [
              [
                "letsencrypt.org",
                "digicert.com"
              ]
            ]
          },
          "error": {
            "type": "string"
          }
        }
      },
      "HstsPreloadResult": {
        "type": "object",
        "required": [
          "onPreloadList"
        ],
        "description": "Authoritative check against the Chromium HSTS preload list.",
        "properties": {
          "onPreloadList": {
            "type": "boolean"
          },
          "includeSubdomains": {
            "type": "boolean",
            "description": "Whether the preload-list entry covers subdomains."
          },
          "error": {
            "type": "string"
          }
        }
      },
      "DnssecResult": {
        "type": "object",
        "required": [
          "validated"
        ],
        "description": "DNSSEC validation observed on the apex domain (Google Public DNS AD flag).",
        "properties": {
          "validated": {
            "type": "boolean"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "HttpsRecordResult": {
        "type": "object",
        "required": [
          "present"
        ],
        "description": "DNS HTTPS (SVCB/HTTPS) record state for the apex — informational, no grade impact.",
        "properties": {
          "present": {
            "type": "boolean"
          },
          "alpn": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "ALPN protocols advertised in the HTTPS record when present."
          },
          "ech": {
            "type": "boolean",
            "description": "True when Encrypted Client Hello (ECH) parameters are advertised."
          },
          "rawData": {
            "type": "string"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "CheckResult": {
        "type": "object",
        "required": [
          "domain",
          "httpRedirects",
          "httpsWorks",
          "redirectChain"
        ],
        "description": "Full check payload. Most nested objects are optional and are only included when the corresponding probe succeeded. When `error` is set, only the basic redirect / reachability fields are guaranteed to be populated. When `botProtectionBlocked` is true, `sslGrade` and security-header details are withheld — see https://httpsornot.com/methodology.",
        "properties": {
          "domain": {
            "type": "string"
          },
          "httpRedirects": {
            "type": "boolean",
            "description": "True if a plain `http://` request to the domain redirects to HTTPS."
          },
          "httpsWorks": {
            "type": "boolean",
            "description": "True if a `https://` request completes the TLS handshake and returns a non-error status."
          },
          "httpPort80Reachable": {
            "type": "boolean",
            "description": "True if TCP/HTTP on port 80 answered. False when port 80 is closed or timed out (browsers often auto-upgrade to HTTPS). Affects grading: a missing redirect only hard-caps at C when port 80 is reachable. Omitted on older stored reports."
          },
          "botProtectionBlocked": {
            "type": "boolean",
            "description": "True when a CDN/WAF challenge page blocked our headers probe. When set, `sslGrade` and header results are withheld rather than inventing a grade for a page we never saw. Usually omitted when false."
          },
          "redirectChain": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ordered list of URLs visited during the HTTP→HTTPS redirect chain."
          },
          "redirectChainDetails": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RedirectStepDetail"
            }
          },
          "totalResponseTime": {
            "type": "number",
            "description": "Total time to follow the redirect chain to the final URL, in ms. Shown in the UI; does not affect the grade."
          },
          "statusCode": {
            "type": "integer",
            "minimum": 100,
            "maximum": 599
          },
          "finalUrl": {
            "type": "string"
          },
          "redirectChangesHostname": {
            "type": "boolean",
            "description": "True when the redirect chain ends on a different hostname than `domain` (e.g. brand-a.com → brand-b.com, or apex → www). When set, `sslCertificate` is for the requested host; `finalSslCertificate` is for the landing host; security headers, HSTS, and TLS probes reflect the final host. Usually omitted when false."
          },
          "finalHostname": {
            "type": "string",
            "description": "Landing HTTPS hostname when `redirectChangesHostname` is true."
          },
          "checkedAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the result was produced (server clock)."
          },
          "sslCertificate": {
            "$ref": "#/components/schemas/SSLCertificateInfo",
            "description": "Certificate for the requested `domain` (SNI). Primary for monitoring, expiry reminders, and domain-match checks."
          },
          "finalSslCertificate": {
            "$ref": "#/components/schemas/SSLCertificateInfo",
            "description": "Certificate for `finalHostname` when the redirect lands on another host. Omitted when the final host equals `domain`."
          },
          "securityHeaders": {
            "$ref": "#/components/schemas/SecurityHeadersResult"
          },
          "hstsStatus": {
            "$ref": "#/components/schemas/HstsStatus"
          },
          "sslGrade": {
            "$ref": "#/components/schemas/SSLGrade"
          },
          "mixedContent": {
            "$ref": "#/components/schemas/MixedContentResult"
          },
          "tlsProtocols": {
            "$ref": "#/components/schemas/TlsProtocolsResult"
          },
          "http2": {
            "$ref": "#/components/schemas/Http2Result"
          },
          "http3": {
            "$ref": "#/components/schemas/Http3Result"
          },
          "caa": {
            "$ref": "#/components/schemas/CaaResult"
          },
          "dnssec": {
            "$ref": "#/components/schemas/DnssecResult"
          },
          "httpsRecord": {
            "$ref": "#/components/schemas/HttpsRecordResult"
          },
          "hstsPreloadList": {
            "$ref": "#/components/schemas/HstsPreloadResult"
          },
          "cookieFlags": {
            "$ref": "#/components/schemas/CookieSecurityResult"
          },
          "infoDisclosureHeaders": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InfoDisclosureHeader"
            }
          },
          "error": {
            "type": "string",
            "description": "Human-readable error if the check could not complete normally."
          },
          "policyOk": {
            "type": "boolean",
            "description": "Present when CI policy query params were supplied. true on 200; false on 422."
          },
          "policyFailures": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PolicyFailure"
            },
            "description": "Present on 422 policy failures."
          }
        }
      },
      "PolicyFailure": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "examples": [
              "min_grade",
              "headers_min_grade",
              "require_hsts",
              "bot_protection_blocked",
              "grade_unavailable"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "PolicyFailureResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CheckResult"
          },
          {
            "type": "object",
            "required": [
              "policyOk",
              "policyFailures"
            ],
            "properties": {
              "policyOk": {
                "type": "boolean",
                "enum": [
                  false
                ]
              },
              "policyFailures": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PolicyFailure"
                }
              },
              "error": {
                "type": "string",
                "examples": [
                  "Policy failed"
                ]
              }
            }
          }
        ]
      }
    }
  }
}
