{
 "openapi": "3.1.0",
 "info": {
  "title": "CrawlCheck API",
  "version": "1",
  "summary": "What a non-browser client actually receives from a domain",
  "description": "Unversioned and unstable. 26 endpoints are documented here, the same set as /docs/api; everything else this service answers is internal and may change without notice. Endpoints marked licence-gated answer 404 without a valid key.",
  "contact": {
   "name": "CrawlCheck",
   "url": "https://crawlcheck.io/policy",
   "email": "hello@crawlcheck.io"
  },
  "license": {
   "name": "Terms",
   "url": "https://crawlcheck.io/policy"
  }
 },
 "servers": [
  {
   "url": "https://crawlcheck.io"
  }
 ],
 "externalDocs": {
  "url": "https://crawlcheck.io/docs/api",
  "description": "Human-readable reference"
 },
 "paths": {
  "/api/scan": {
   "post": {
    "operationId": "scan",
    "summary": "Scan a domain as several crawler identities and return a graded record",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "domain"
        ],
        "properties": {
         "domain": {
          "type": "string",
          "description": "A bare domain or a full URL",
          "examples": [
           "example.com"
          ]
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The scan record",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ScanRecord"
        }
       }
      }
     },
     "400": {
      "description": "Not a public domain, or no JSON body"
     },
     "429": {
      "description": "Rate limited: 5 requests per 10 seconds per address"
     }
    }
   }
  },
  "/api/v1/scan": {
   "post": {
    "operationId": "scanDomainV1",
    "summary": "Versioned scan: returns PublicReportV1 (stable allowlist, JSON Schema at /api/v1/schema)",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "domain"
        ],
        "properties": {
         "domain": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "PublicReportV1"
     },
     "400": {
      "description": "ErrorV1: bad_request"
     },
     "429": {
      "description": "ErrorV1: rate_limited"
     }
    }
   }
  },
  "/api/v1/reports/{id}": {
   "get": {
    "operationId": "getReportV1",
    "summary": "A stored report as PublicReportV1 (licence or read-scoped API key)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "PublicReportV1"
     },
     "401": {
      "description": "ErrorV1: unauthorised"
     },
     "404": {
      "description": "ErrorV1: not_found"
     }
    }
   }
  },
  "/api/v1/schema": {
   "get": {
    "operationId": "getSchemaV1",
    "summary": "JSON Schema for every v1 response",
    "responses": {
     "200": {
      "description": "JSON Schema 2020-12"
     }
    }
   }
  },
  "/api/keys": {
   "get": {
    "operationId": "listApiKeys",
    "summary": "List the scoped API keys on a licence (names, scopes, last use; never the keys)",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "keys[]"
     },
     "401": {
      "description": "No valid licence key"
     }
    }
   },
   "post": {
    "operationId": "createApiKey",
    "summary": "Create a scoped API key (shown once; stored as a SHA-256)",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "scopes": {
          "type": "array",
          "items": {
           "type": "string",
           "enum": [
            "scan",
            "read",
            "share"
           ]
          }
         },
         "domains": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "days": {
          "type": "integer",
          "minimum": 1,
          "maximum": 365
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "key, id, scopes, domains, expires"
     },
     "400": {
      "description": "No usable scope"
     },
     "401": {
      "description": "No valid licence key"
     },
     "403": {
      "description": "Domain not on the licence"
     },
     "409": {
      "description": "Twenty-key limit reached"
     }
    }
   },
   "delete": {
    "operationId": "revokeApiKey",
    "summary": "Revoke an API key by id",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     },
     {
      "name": "id",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[0-9a-f]{12}$"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Revoked"
     },
     "404": {
      "description": "No such key on this licence"
     }
    }
   }
  },
  "/api/share": {
   "post": {
    "operationId": "createShareLink",
    "summary": "Create a scoped share link for one report (licence required; up to 90 days; revocable)",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "rid"
        ],
        "properties": {
         "rid": {
          "type": "string",
          "description": "Report id, or a report URL"
         },
         "days": {
          "type": "integer",
          "minimum": 1,
          "maximum": 90,
          "default": 30
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "url, rid, expires, revoke"
     },
     "400": {
      "description": "No usable report id"
     },
     "401": {
      "description": "No valid licence key"
     },
     "403": {
      "description": "Report domain is not on this licence"
     },
     "404": {
      "description": "No such report"
     }
    }
   },
   "delete": {
    "operationId": "revokeShareLink",
    "summary": "Revoke a share link created with the same licence",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     },
     {
      "name": "s",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[0-9a-f]{32}$"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Revoked"
     },
     "400": {
      "description": "No usable token"
     },
     "401": {
      "description": "No valid licence key"
     },
     "404": {
      "description": "No such token on this licence"
     }
    }
   }
  },
  "/api/scan/licensed": {
   "post": {
    "operationId": "scanDomainLicensed",
    "summary": "Scan a domain on the licensed lane (valid licence key required; 60 scans per ten minutes per key; 404 without a key)",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "domain"
        ],
        "properties": {
         "domain": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The scan record, identical to /api/scan"
     },
     "404": {
      "description": "No valid licence key"
     },
     "429": {
      "description": "Key allowance used for this ten-minute window"
     }
    }
   }
  },
  "/api/tool/wba": {
   "get": {
    "operationId": "checkBotKeyDirectory",
    "summary": "Audit a Web Bot Auth key directory: no redirects, content type, RFC 7638 thumbprints, and which keys the directory's own signatures prove",
    "parameters": [
     {
      "name": "agent",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The signing agent's host or https origin, e.g. crawlcheck.io"
     }
    ],
    "responses": {
     "200": {
      "description": "The directory audit, every step shown"
     },
     "400": {
      "description": "No usable agent named"
     }
    }
   },
   "post": {
    "operationId": "verifySignedBotRequest",
    "summary": "Verify a pasted Web Bot Auth request: the agent's directory, then the request's signature against a key the directory proves it holds",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "request": {
          "type": "string",
          "description": "Pasted request headers: Host, Signature-Agent, Signature-Input, Signature, optionally the request line"
         },
         "agent": {
          "type": "string",
          "description": "Alternatively, only the agent whose directory to audit"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The verdict with every step shown"
     },
     "400": {
      "description": "Nothing to check"
     }
    }
   }
  },
  "/api/tool/verify": {
   "post": {
    "operationId": "verifyCrawlerLog",
    "summary": "Adjudicate crawler identity claims in raw access-log lines against each operator's published IP ranges",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "lines"
        ],
        "properties": {
         "lines": {
          "type": "string",
          "description": "Up to 200 raw log lines, 20 KB, newline separated"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Per-line verdicts and a tally",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/VerifyResult"
        }
       }
      }
     },
     "400": {
      "description": "No lines"
     }
    }
   }
  },
  "/api/depth": {
   "post": {
    "operationId": "depthCrawl",
    "summary": "Crawl every URL the site declares (up to 300) and map the links between them: orphans, undeclared pages, per-page status, canonical, text ratio, JSON-LD, noindex. Licence-gated; 404 without a key. Returns 202 with a job id; the result lands at /api/depth/{id} within about a minute",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "domain"
        ],
        "properties": {
         "domain": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "202": {
      "description": "{id, state, url}; poll /api/depth/{id}"
     },
     "400": {
      "description": "No domain"
     },
     "404": {
      "description": "No valid licence key"
     }
    }
   }
  },
  "/api/depth/{id}": {
   "get": {
    "operationId": "depthResult",
    "summary": "The depth-crawl record for a job id: orphans, undeclared, pages[], medians and stated limits",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "Unknown id"
     }
    }
   }
  },
  "/api/flow": {
   "get": {
    "operationId": "crawlerFlow",
    "summary": "The observed data flow for a domain: where each crawler identity's path dies (edge, robots.txt as a file, robots.txt rules) and which stores it reached. Uses the latest record on file, or scans first when there is none (fresh=1). Answer-engine output is drawn, never measured",
    "parameters": [
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "agent",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "fresh",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "1"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "flowmap 1.0: nodes, edges, agents, breaks, summary; evidence (records from 2026-09-20): what each identity received (status, bytes, SHA-256, compared with a browser), what was kept, the manifest and the seal state; with agent=, one identity's path and a one-line answer",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "No such identity in the scan"
     }
    }
   }
  },
  "/api/flow/gate": {
   "get": {
    "operationId": "flowGate",
    "summary": "CI gate for a domain the key covers: 200 pass, 412 fail when a crawler path open at the last sealed scan closed, a file broke, or a declared path is violated at critical/high",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     },
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "fresh",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "1"
       ]
      }
     },
     {
      "name": "since",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "pass (or no baseline yet, stated in summary)"
     },
     "404": {
      "description": "No licence covering the domain"
     },
     "409": {
      "description": "No scan on record"
     },
     "412": {
      "description": "fail: broke[] and declared_violations[]"
     }
    }
   }
  },
  "/api/flow/declared": {
   "get": {
    "operationId": "flowDeclaredGet",
    "summary": "The owner's declared flow for a domain the key covers, with parity against the latest scan",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     },
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "declared, parity[], in_parity"
     },
     "404": {
      "description": "No licence covering the domain, or no declaration"
     }
    }
   },
   "put": {
    "operationId": "flowDeclaredPut",
    "summary": "Set the declared flow: agents {id: reach|blocked|any}, stores {id: present|absent|any}, or {seed: observed} to start from the latest scan",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     },
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "declared, parity[]"
     },
     "400": {
      "description": "Invalid entries"
     },
     "404": {
      "description": "No licence covering the domain"
     },
     "409": {
      "description": "No scan to seed from"
     }
    }
   },
   "delete": {
    "operationId": "flowDeclaredDelete",
    "summary": "Remove the declared flow",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Deleted"
     }
    }
   }
  },
  "/api/tool/robots": {
   "get": {
    "operationId": "robotsResolve",
    "summary": "Resolve every named answer engine, search index and training crawler against a domain's robots.txt the way a crawler does: most-specific group only, longest match, allow wins a tie",
    "parameters": [
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "path",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "default": "/"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Per agent: allowed at the path, allowed_root, named, shadowed",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "No domain"
     }
    }
   }
  },
  "/api/tool/llms": {
   "get": {
    "operationId": "llmsDraft",
    "summary": "Draft an llms.txt from the domain's own homepage and up to 25 declared pages, using their titles and descriptions. A draft, not a publication",
    "parameters": [
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "format",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "json",
        "txt"
       ]
      },
      "description": "txt returns the file itself"
     }
    ],
    "responses": {
     "200": {
      "description": "text plus how many pages were declared, read and carried their own description; text/plain when format=txt"
     },
     "400": {
      "description": "No domain"
     }
    }
   }
  },
  "/api/video": {
   "get": {
    "operationId": "videoCheck",
    "summary": "One YouTube video against the video anchor model: twelve checks, each naming what it reads. Cached six hours",
    "parameters": [
     {
      "name": "v",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "A video id or URL"
     },
     {
      "name": "fresh",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "1"
       ]
      },
      "description": "Bypass the cache"
     }
    ],
    "responses": {
     "200": {
      "description": "video, chapters, captions, storyboard, thumbnail, checks{rows, passed, scored}, not_measurable",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "No such video"
     }
    }
   }
  },
  "/api/video/channel": {
   "get": {
    "operationId": "videoChannel",
    "summary": "Every upload on a channel, read 50 at a time. The upload list is free; the per-check tally (checks=1) is for Watch licences and above",
    "parameters": [
     {
      "name": "handle",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "@channel; give this or id"
     },
     {
      "name": "id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Channel id"
     },
     {
      "name": "checks",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "1"
       ]
      },
      "description": "Check every upload and tally per check"
     }
    ],
    "responses": {
     "200": {
      "description": "rollup{check: {pass, judged, na}} and videos[] sorted by views",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "No such channel"
     }
    }
   }
  },
  "/api/video/site": {
   "get": {
    "operationId": "videoSite",
    "summary": "The site half: homepage plus up to 60 sitemap pages read for YouTube embeds, facades and page-builder widgets, VideoObject nodes and their required properties, and - with a handle - how many of the channel's videos the site carries",
    "parameters": [
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "handle",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "pages[] with embeds and VideoObject counts, videos{} keyed by id, rows[] with passed/scored",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "No domain"
     }
    }
   }
  },
  "/api/fix/robots": {
   "get": {
    "operationId": "fixRobots",
    "summary": "The served robots.txt, corrected: * group rules copied into every named group that lacked them, a Sitemap line added when the file never named the sitemap it serves, a minimal replacement when the served file was HTML. Every change is listed at the top of the file",
    "parameters": [
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "format",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "json"
       ]
      },
      "description": "The notes as data instead of the file"
     }
    ],
    "responses": {
     "200": {
      "description": "text/plain robots.txt, or JSON when format=json"
     },
     "400": {
      "description": "No domain"
     }
    }
   }
  },
  "/api/fix/entitymap": {
   "get": {
    "operationId": "fixEntitymap",
    "summary": "A starter entitymap.json built from the latest scan record: name, phone, coordinates and every declared service area as entities with SERVES relations. Fields the page never stated are marked TODO, never guessed. Needs a prior scan",
    "parameters": [
     {
      "name": "domain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "application/json, downloadable as entitymap.json"
     },
     "404": {
      "description": "No prior scan for this domain"
     }
    }
   }
  },
  "/api/entity/observe": {
   "post": {
    "operationId": "entityObserve",
    "summary": "File a profile page as the owner's browser saw it, for the entity corroboration section. Consulted only where the scanner's own fetch was unverifiable; never overrides a live result; the row is labelled owner-browser. Licence-gated and bound to the licence's domains; the page's canonical must name the url's host. Kept 30 days",
    "parameters": [
     {
      "name": "x-crawlcheck-key",
      "in": "header",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^cc_[0-9a-f]{32}$"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "url",
         "html"
        ],
        "properties": {
         "url": {
          "type": "string",
          "description": "The profile URL as declared in sameAs"
         },
         "html": {
          "type": "string",
          "description": "The whole page as the browser saw it; at least 512 bytes, the first 200 KB are kept"
         },
         "vantage": {
          "type": "string",
          "description": "Defaults to owner-browser"
         }
        }
       }
      },
      "application/x-www-form-urlencoded": {
       "schema": {
        "type": "object",
        "required": [
         "payload"
        ],
        "properties": {
         "payload": {
          "type": "string",
          "description": "The same JSON object as a form field"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "key, sha, bytes, at, expires_days, bound_to (the licence's domains)"
     },
     "400": {
      "description": "No url or html, page under 512 bytes, or the page's canonical names another host"
     },
     "404": {
      "description": "No valid licence key"
     }
    }
   }
  },
  "/api/public/counts": {
   "get": {
    "operationId": "publicCounts",
    "summary": "The figures this site quotes about itself: sites_measured, domains_in_corpus (two different quantities), crawler_visits (a rolling window), queue, identities_sent, sections, sections_scored (derived from the weights map), findings_published, guides_published and an at timestamp. CORS open, cached 60 seconds",
    "responses": {
     "200": {
      "description": "JSON",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/registry": {
   "get": {
    "operationId": "registry",
    "summary": "The registry: which measured domains serve an llms.txt, an agents.md, a media kit, a reciprocity-tested entity graph, an AI access policy that names crawlers, or agent-callable surfaces",
    "parameters": [
     {
      "name": "host",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "One domain. Returns that row, or 404 when the domain is not listed."
     },
     {
      "name": "shelf",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "llms",
        "agents",
        "mediakit",
        "entity",
        "aipolicy",
        "agentapi"
       ]
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 500,
       "default": 100
      }
     },
     {
      "name": "offset",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 0,
       "default": 0
      },
      "description": "Rows to skip. The response carries next, the URL of the following page, and complete: true on the last one"
     },
     {
      "name": "cursor",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Opaque cursor from a previous response"
     }
    ],
    "responses": {
     "200": {
      "description": "Shelf counts and rows",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "That domain is not listed"
     }
    }
   }
  },
  "/api/telemetry": {
   "get": {
    "operationId": "telemetry",
    "summary": "Verified-crawler telemetry observed at this origin",
    "responses": {
     "200": {
      "description": "JSON",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/corpus/state": {
   "get": {
    "operationId": "corpusState",
    "summary": "Coverage of the public dataset by platform, rendering, size, language and kind",
    "responses": {
     "200": {
      "description": "JSON",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  }
 },
 "components": {
  "schemas": {
   "ScanRecord": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "description": "Report id; the report renders at /r/{id} and the record at /r/{id}.json"
     },
     "domain": {
      "type": "string"
     },
     "grade": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "A",
       "B",
       "C",
       "D",
       "F",
       null
      ],
      "description": "null when the origin refused the scanner or the scan could not be graded"
     },
     "overall": {
      "type": [
       "integer",
       "null"
      ],
      "minimum": 0,
      "maximum": 100,
      "description": "AI visibility: reach, then read, then quote"
     },
     "refused": {
      "type": "boolean",
      "description": "The homepage and a path that cannot exist answered identically: a wall, not a site. Nothing is scored"
     },
     "section_scores": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "string"
        },
        "title": {
         "type": "string"
        },
        "score": {
         "type": [
          "integer",
          "null"
         ]
        }
       }
      }
     },
     "findings": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "code": {
         "type": "string"
        },
        "severity": {
         "type": "integer",
         "minimum": 1,
         "maximum": 5
        },
        "path": {
         "type": "string"
        },
        "title": {
         "type": "string"
        },
        "detail": {
         "type": "string"
        },
        "evidence": {}
       }
      }
     },
     "self_audit": {
      "type": "object",
      "description": "Invariants checked on this record itself; a finding here is a defect in the scanner, never in the site"
     }
    }
   },
   "VerifyResult": {
    "type": "object",
    "properties": {
     "tally": {
      "type": "object",
      "properties": {
       "verified": {
        "type": "integer"
       },
       "spoofed": {
        "type": "integer"
       },
       "unverifiable": {
        "type": "integer"
       },
       "no_bot": {
        "type": "integer"
       }
      }
     },
     "forged_rate_pct": {
      "type": [
       "number",
       "null"
      ]
     },
     "denominator_note": {
      "type": "string"
     },
     "rows": {
      "type": "array",
      "items": {
       "type": "object"
      }
     }
    }
   }
  }
 }
}