{
  "openapi": "3.0.3",
  "info": {
    "title": "Genchi Scan API",
    "version": "1.0.0",
    "description": "Submit a URL, Genchi Scan fully renders the page and reports what's wrong - dead links and images, mixed content, TLS issues, slow responses, detected technologies, and more. Submit a scan, then poll for the findings.",
    "contact": { "name": "Genchi Scan", "url": "https://genchi.dk/docs" }
  },
  "servers": [{ "url": "https://api.genchi.dk", "description": "Production" }],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Scans", "description": "Submit scans and retrieve findings." },
    { "name": "Providers", "description": "Standalone hosting/DNS provider checks - no browser involved, own quota." }
  ],
  "paths": {
    "/v1/scans": {
      "post": {
        "tags": ["Scans"],
        "summary": "Create a scan",
        "description": "Queues a scan and meters one invocation against your monthly quota. Returns immediately with an id; poll GET /v1/scans/{id} for findings.",
        "operationId": "createScan",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanRequest" } } }
        },
        "responses": {
          "202": {
            "description": "Scan accepted and queued.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanCreated" } } }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "402": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      },
      "get": {
        "tags": ["Scans"],
        "summary": "List recent scans",
        "description": "Your 50 most recent scans as summaries (no findings), newest first.",
        "operationId": "listScans",
        "responses": {
          "200": {
            "description": "Recent scans.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["scans"],
                  "properties": {
                    "scans": { "type": "array", "items": { "$ref": "#/components/schemas/ScanSummary" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/v1/scans/{id}": {
      "get": {
        "tags": ["Scans"],
        "summary": "Get a scan and its findings",
        "description": "Poll until status is completed or failed. A scan moves through pending -> running -> completed (or failed). Most scans finish in a few seconds; poll every 1-2s.",
        "operationId": "getScan",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The scan id returned by POST /v1/scans.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "The scan record. findings is null until the scan completes.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanDetail" } } }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/v2/scans/{id}": {
      "get": {
        "tags": ["Scans"],
        "summary": "Get a scan's content (v2)",
        "description": "Identical to GET /v1/scans/{id}, except findings.dns is omitted - use /v2/providers for hosting/DNS data instead. /v1/scans/{id} keeps returning the full merged record, unchanged.",
        "operationId": "getScanContent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The scan id returned by POST /v1/scans.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "The scan record. findings.dns is never present here.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanDetail" } } }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/v2/providers": {
      "post": {
        "tags": ["Providers"],
        "summary": "Run a standalone provider check",
        "description": "Runs DNS/hosting-provider detection directly - no browser involved. Responds synchronously (no polling) and meters against its own provider-check quota/rate limit, separate from the scan quota, so it can be called far more often than a full content scan.",
        "operationId": "checkProvider",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProviderCheckRequest" } } }
        },
        "responses": {
          "200": {
            "description": "The provider check result. error is set (with dns: null) when the target isn't a valid registrable domain - still a 200, not a failure, and still counted against quota.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProviderCheckDetail" } } }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "402": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      },
      "get": {
        "tags": ["Providers"],
        "summary": "List recent provider checks",
        "description": "Your 50 most recent provider checks, newest first. Optionally filter to one domain with ?url=.",
        "operationId": "listProviderChecks",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": false,
            "description": "Narrow to checks for this exact URL, to track a domain's provider over time.",
            "schema": { "type": "string", "format": "uri" }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent provider checks.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["checks"],
                  "properties": {
                    "checks": { "type": "array", "items": { "$ref": "#/components/schemas/ProviderCheckDetail" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/v2/providers/{id}": {
      "get": {
        "tags": ["Providers"],
        "summary": "Get a provider check",
        "operationId": "getProviderCheck",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The check id returned by POST /v2/providers.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "The provider check record.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProviderCheckDetail" } } }
          },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Send your API key as a bearer token: `Authorization: Bearer gk_live_...`. Create keys on your dashboard."
      }
    },
    "responses": {
      "Error": {
        "description": "Failure envelope. See the `code` field for the machine-readable reason.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "ScanRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The page to scan. Must be a public http(s) URL.",
            "example": "https://example.com"
          },
          "waitMs": {
            "type": "integer",
            "minimum": 0,
            "maximum": 30000,
            "default": 0,
            "description": "Extra settle time after load before collecting findings."
          },
          "viewport": {
            "type": "object",
            "description": "Render viewport in px.",
            "default": { "width": 1280, "height": 800 },
            "properties": {
              "width": { "type": "integer", "minimum": 1, "maximum": 4000, "default": 1280 },
              "height": { "type": "integer", "minimum": 1, "maximum": 4000, "default": 800 }
            }
          },
          "screenshot": {
            "type": "boolean",
            "default": false,
            "description": "Capture full-page screenshots (desktop/tablet/phone). Off by default; only takes effect on plans with the screenshots feature."
          }
        }
      },
      "ScanCreated": {
        "type": "object",
        "required": ["id", "status"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["pending"] },
          "usage": { "$ref": "#/components/schemas/Usage" }
        }
      },
      "Usage": {
        "type": "object",
        "description": "Monthly quota usage after this scan. remaining and limit are null on unlimited plans.",
        "properties": {
          "used": { "type": "integer" },
          "remaining": { "type": "integer", "nullable": true },
          "limit": { "type": "integer", "nullable": true }
        }
      },
      "ProviderCheckRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The domain to check. No browser is involved, so no wait/viewport options.",
            "example": "https://example.com"
          }
        }
      },
      "ProviderCheckDetail": {
        "type": "object",
        "description": "A standalone provider (DNS/hosting) check - v2's browser-free sibling of a scan. Always terminal on response: error is set instead of throwing when the target wasn't a valid domain.",
        "required": ["id", "url", "createdAt", "dns", "error"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "createdAt": { "type": "string", "format": "date-time" },
          "dns": { "oneOf": [{ "$ref": "#/components/schemas/DnsReport" }, { "type": "null" }] },
          "error": { "type": "string", "nullable": true, "description": "Set when the target wasn't a valid registrable domain." }
        }
      },
      "ScanStatus": {
        "type": "string",
        "enum": ["pending", "running", "completed", "failed"]
      },
      "ScanSummary": {
        "type": "object",
        "required": ["id", "url", "status", "errorCount", "createdAt", "finishedAt"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "status": { "$ref": "#/components/schemas/ScanStatus" },
          "errorCount": { "type": "integer", "nullable": true, "description": "Number of issues found; null until completed." },
          "createdAt": { "type": "string", "format": "date-time" },
          "finishedAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "ScanDetail": {
        "allOf": [
          { "$ref": "#/components/schemas/ScanSummary" },
          {
            "type": "object",
            "required": ["startedAt", "findings", "error"],
            "properties": {
              "startedAt": { "type": "string", "format": "date-time", "nullable": true },
              "findings": { "oneOf": [{ "$ref": "#/components/schemas/ScanFindings" }, { "type": "null" }] },
              "error": { "type": "string", "nullable": true }
            }
          }
        ]
      },
      "ScanFindings": {
        "type": "object",
        "description": "The scan findings. Most fields are present only when resultKind is \"full\".",
        "required": ["resultKind"],
        "properties": {
          "resultKind": {
            "type": "string",
            "enum": ["full", "unreachable", "redirect", "unparseable", "timeout", "error", "blocked"],
            "description": "\"full\" on success; otherwise the reason the scan could not complete normally."
          },
          "processing_msec": { "type": "integer" },
          "finalUrl": { "type": "string", "nullable": true, "description": "Where the URL ultimately resolved to." },
          "finalURLDiff": { "type": "boolean" },
          "finalHostnameDiff": { "type": "boolean" },
          "redirectType": { "type": "string", "enum": ["www", "cross_domain", "protocol", "path"], "nullable": true },
          "http_status": { "type": "integer", "nullable": true },
          "sitemap": {
            "type": "object",
            "properties": {
              "checksum": { "type": "string", "nullable": true },
              "count": { "type": "integer" }
            }
          },
          "metrics": { "$ref": "#/components/schemas/ScanMetrics" },
          "cert": { "oneOf": [{ "$ref": "#/components/schemas/ScanCert" }, { "type": "null" }] },
          "securityHeaders": {
            "type": "object",
            "nullable": true,
            "additionalProperties": { "type": "string" },
            "description": "Notable response security headers."
          },
          "canonical": { "type": "string", "nullable": true },
          "technologies": {
            "type": "object",
            "additionalProperties": true,
            "description": "Detected stack keyed by name, each with a version when found, e.g. { \"nginx\": { \"version\": \"1.25\" } }."
          },
          "wordpress": { "oneOf": [{ "$ref": "#/components/schemas/WordpressReport" }, { "type": "null" }] },
          "dns": { "oneOf": [{ "$ref": "#/components/schemas/DnsReport" }, { "type": "null" }] }
        }
      },
      "ScanMetrics": {
        "type": "object",
        "properties": {
          "css_used": {
            "type": "object",
            "nullable": true,
            "properties": { "total": { "type": "integer" }, "used": { "type": "integer" } }
          },
          "image_count_same": { "type": "integer" },
          "image_count_other": { "type": "integer" },
          "ttfb_ms": { "type": "integer", "nullable": true },
          "timing_dns_ms": { "type": "integer", "nullable": true },
          "timing_tcp_ms": { "type": "integer", "nullable": true },
          "timing_tls_ms": { "type": "integer", "nullable": true },
          "timing_wait_ms": { "type": "integer", "nullable": true },
          "page_weight_bytes": { "type": "integer", "nullable": true },
          "dead_links": { "type": "integer" },
          "dead_images": { "type": "integer" },
          "dead_resources": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/DeadResource" }
          },
          "mixed_content": { "type": "integer" },
          "asset_js_same": { "type": "integer" },
          "asset_js_other": { "type": "integer" },
          "asset_css_same": { "type": "integer" },
          "asset_css_other": { "type": "integer" },
          "asset_img_same": { "type": "integer" },
          "asset_img_other": { "type": "integer" },
          "uncached_assets": { "type": "integer" },
          "total_assets": { "type": "integer" }
        }
      },
      "DeadResource": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string" },
          "status": {
            "nullable": true,
            "oneOf": [{ "type": "integer" }, { "type": "string" }],
            "description": "HTTP status; null when the request failed before a response (connection-level)."
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Net error (e.g. ABORTED) when there is no HTTP status."
          }
        }
      },
      "ScanCert": {
        "type": "object",
        "properties": {
          "subject_cn": { "type": "string", "nullable": true },
          "issuer": { "type": "string", "nullable": true },
          "valid_from": { "type": "string", "nullable": true },
          "valid_to": { "type": "string", "nullable": true },
          "san": { "type": "string", "nullable": true },
          "serial_number": { "type": "string", "nullable": true },
          "fingerprint": { "type": "string", "nullable": true },
          "is_self_signed": { "type": "boolean", "nullable": true },
          "error": { "type": "string", "nullable": true }
        }
      },
      "WordpressReport": {
        "type": "object",
        "description": "Present only for WordPress sites. The theme slug and the found plugin list require the WordPress plan feature; other plans receive only plugins.count. Each theme/plugin carries a version when one can be resolved (best-effort).",
        "properties": {
          "theme": {
            "type": "object",
            "properties": {
              "slug": { "type": "string" },
              "version": { "type": "string" }
            }
          },
          "plugins": {
            "type": "object",
            "required": ["count"],
            "properties": {
              "count": { "type": "integer" },
              "found": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": ["slug"],
                  "properties": {
                    "slug": { "type": "string" },
                    "version": { "type": "string" }
                  }
                }
              }
            }
          },
          "security": { "oneOf": [{ "$ref": "#/components/schemas/WpSecurity" }, { "type": "null" }] }
        }
      },
      "WpSecurity": {
        "type": "object",
        "description": "WordPress exposure surfaces, probed in-page during the scan. Present only with the WordPress plan feature.",
        "properties": {
          "wpLogin": {
            "type": "object",
            "description": "Default /wp-login.php reachability (brute-force surface).",
            "properties": {
              "exposed": { "type": "boolean", "description": "True when wp-login.php returns a real login form (not redirected/blocked)." },
              "status": { "type": "integer", "nullable": true }
            }
          },
          "xmlrpc": {
            "type": "object",
            "description": "/xmlrpc.php availability; pingback is the SSRF/DDoS-amplification surface.",
            "properties": {
              "enabled": { "type": "boolean", "description": "True when xmlrpc.php answers an XML-RPC request." },
              "pingback": { "type": "boolean", "description": "True when pingback.ping is advertised." },
              "status": { "type": "integer", "nullable": true }
            }
          }
        }
      },
      "DnsReport": {
        "type": "object",
        "description": "DNS / email / registration posture. The resolves flag and email posture booleans are always present; the detail object (records, providers, WHOIS, GeoIP/ASN) requires the deep_scan plan feature.",
        "required": ["resolves", "email"],
        "properties": {
          "resolves": { "type": "boolean", "description": "Whether the hostname resolved to at least one A/AAAA record." },
          "resolveError": { "type": "string", "nullable": true, "description": "DNS error code when resolution failed, e.g. NOTFOUND, SERVFAIL, TIMEOUT, REFUSED." },
          "email": {
            "type": "object",
            "description": "Presence of common email-authentication / security DNS records.",
            "properties": {
              "spf": { "type": "boolean" },
              "dmarc": { "type": "boolean" },
              "dkim": { "type": "boolean" },
              "mx": { "type": "boolean" },
              "dnssec": { "type": "boolean" }
            }
          },
          "detail": { "$ref": "#/components/schemas/DnsDetail" }
        }
      },
      "DnsDetail": {
        "type": "object",
        "description": "Full DNS detail; present only with the deep_scan plan feature.",
        "properties": {
          "domain": {
            "type": "object",
            "nullable": true,
            "properties": {
              "subDomains": { "type": "string" },
              "domain": { "type": "string" },
              "topLevelDomains": { "type": "string" }
            }
          },
          "soa": {
            "type": "object",
            "properties": {
              "nsname": { "type": "string", "nullable": true },
              "hostmaster": { "type": "string", "nullable": true }
            }
          },
          "aRecords": { "type": "array", "items": { "$ref": "#/components/schemas/DnsAddress" } },
          "mxRecords": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "exchange": { "type": "string" },
                "priority": { "type": "integer" }
              }
            }
          },
          "nsCount": { "type": "integer" },
          "asns": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "asn": { "type": "integer" },
                "organization": { "type": "string", "nullable": true }
              }
            }
          },
          "spf": { "type": "string", "nullable": true },
          "spfValidation": {
            "type": "object",
            "nullable": true,
            "properties": {
              "status": { "type": "string", "nullable": true },
              "comment": { "type": "string", "nullable": true },
              "lookups": { "type": "integer", "nullable": true }
            }
          },
          "dmarc": { "type": "string", "nullable": true },
          "bimi": {
            "type": "object",
            "nullable": true,
            "properties": {
              "status": { "type": "string" },
              "logoUrl": { "type": "string", "nullable": true }
            }
          },
          "hostmasterProvider": { "type": "string", "nullable": true },
          "nameserverProvider": { "type": "string", "nullable": true },
          "whois": {
            "type": "object",
            "nullable": true,
            "properties": {
              "status": { "type": "array", "items": { "type": "string" }, "nullable": true },
              "expires": { "type": "string", "nullable": true },
              "registered": { "type": "string", "nullable": true },
              "registrar": { "type": "string", "nullable": true }
            }
          }
        }
      },
      "DnsAddress": {
        "type": "object",
        "properties": {
          "address": { "type": "string" },
          "family": { "type": "integer", "description": "4 or 6." },
          "isoCode": { "type": "string", "nullable": true, "description": "GeoIP country ISO code." },
          "asn": { "type": "integer", "nullable": true },
          "rdns": { "type": "string", "nullable": true }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable message." },
          "code": {
            "type": "string",
            "description": "Machine-readable reason.",
            "enum": [
              "no_key",
              "bad_key",
              "subscription_inactive",
              "validation",
              "blocked_target",
              "account_suspended",
              "rate_limited",
              "concurrency_limited",
              "quota_exceeded",
              "not_found"
            ]
          },
          "limit": { "type": "integer", "description": "Present on quota_exceeded: the plan's monthly limit." }
        }
      }
    }
  }
}
