{
  "openapi": "3.1.0",
  "info": {
    "title": "HTTPS or Not — Public API",
    "version": "1.1.0",
    "summary": "Free, no-key HTTPS / SSL / security-headers / HTTP3 check as JSON.",
    "description": "Machine-readable HTTPS, SSL, security-headers, HTTP/2 + HTTP/3, CAA, DNSSEC, HSTS preload and mixed-content check.\n\n- **No API key.** Rate limited per IP on the upstream backend.\n- **Same payload shape** as the in-app checker on https://httpsornot.com.\n- We call the JSON output **\"porcelain\"** (after `git --porcelain`): stable, scriptable, machine-friendly.\n\nSee the full UI documentation at https://httpsornot.com/api-docs.\n\n> Authenticated backend routes (`/check/:domain`, `/check-bulk` on cert-backend) are used **only** by the httpsornot.com frontend via proxy and are intentionally **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 for a domain",
        "description": "Returns a full check result as JSON. Rate limit: **30 requests per hour per IP** (enforced on the upstream cert-backend).\n\n`domain` should be a hostname only (`example.com`); we strip leading `https?://` and any path. Internationalised domains should be supplied in ASCII (Punycode).",
        "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"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Check completed. May still include an `error` field if the target was unreachable, redirected to a blocked host, or HTTPS handshake failed.",
            "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,
                      "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": 92,
                        "label": "Excellent",
                        "factors": [
                          { "name": "HTTPS Connection", "passed": true, "points": 25, "maxPoints": 25 },
                          { "name": "HTTP→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": "Fast Response Time", "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 }
                        ],
                        "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": "Strict-Transport-Security", "present": false, "recommendation": "max-age >= 31536000; includeSubDomains; preload" },
                          { "name": "Content-Security-Policy", "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"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid `domain` parameter.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorBody" },
                "example": { "error": "Missing or invalid query parameter: domain" }
              }
            }
          },
          "429": {
            "description": "Too many requests from this IP.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorBody" },
                "example": { "error": "Too many requests. Limit: 30 per hour per IP. See https://httpsornot.com/api-docs" }
              }
            },
            "headers": {
              "Retry-After": { "schema": { "type": "string" }, "description": "Suggested retry delay (seconds)" },
              "RateLimit-Limit": { "schema": { "type": "string" } },
              "RateLimit-Remaining": { "schema": { "type": "string" } },
              "RateLimit-Reset": { "schema": { "type": "string" }, "description": "Unix timestamp when the limit resets" }
            }
          },
          "502": {
            "description": "Upstream check failed (network / backend unreachable).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorBody" }
              }
            }
          }
        }
      }
    },
    "/api/public/check": {
      "get": {
        "tags": ["Check"],
        "operationId": "getPublicCheckAlias",
        "summary": "Alias for /api/porcelain",
        "description": "Identical request and response shape as `GET /api/porcelain`. Kept as a stable convenience alias.",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "minLength": 3, "maxLength": 253 }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CheckResult" }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
            }
          },
          "429": { "description": "Too many requests" }
        }
      }
    },
    "/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" } }
            }
          }
        }
      }
    }
  },
  "components": {
    "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" }
        }
      },
      "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" },
          "error": { "type": "string" }
        }
      },
      "SecurityHeader": {
        "type": "object",
        "required": ["name", "present"],
        "properties": {
          "name": {
            "type": "string",
            "examples": [
              "Strict-Transport-Security",
              "Content-Security-Policy",
              "X-Content-Type-Options",
              "X-Frame-Options",
              "Referrer-Policy",
              "Permissions-Policy"
            ]
          },
          "present": { "type": "boolean" },
          "value": { "type": "string", "description": "Raw header value when present." },
          "recommendation": {
            "type": "string",
            "description": "Suggested value when missing or weak (informational)."
          }
        }
      },
      "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", "factors"],
        "description": "Composite security grade based on eight weighted factors (max 100 points).",
        "properties": {
          "grade": {
            "type": "string",
            "enum": ["A+", "A", "A-", "B+", "B", "C", "F"]
          },
          "score": { "type": "integer", "minimum": 0, "maximum": 100 },
          "label": { "type": "string", "examples": ["Excellent", "Good", "Fair"] },
          "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" }
        }
      },
      "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.",
        "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."
          },
          "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."
          },
          "statusCode": { "type": "integer", "minimum": 100, "maximum": 599 },
          "finalUrl": { "type": "string" },
          "checkedAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the result was produced (server clock)."
          },
          "sslCertificate": { "$ref": "#/components/schemas/SSLCertificateInfo" },
          "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" },
          "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."
          }
        }
      }
    }
  }
}
