Scanning
Run agent-readiness scans and stream progress. Scans are rate-limited per IP and cached; cached responses never consume quota.
/api/scanScan a domain, MCP server URL, or MCP App URL for agent-readiness
Runs a full agent-readiness scan on the given URL. Accepts a domain, MCP server URL, or MCP App URL (server that supports the MCP Apps extension `io.modelcontextprotocol/ui`) - the server auto-detects which kind of input was provided and selects the appropriate check set. Catalog-style listing pages are folded into the `mcp` kind by classifying the first validated embedded MCP URL. Returns score, grade, and detailed layer breakdown. The response includes an optional `urlKind` field indicating the detected kind ('domain', 'mcp', or 'mcp-app'). Scoring completes within the request, but deeper analysis can continue asynchronously afterwards: when the returned analysisStatus is 'partial', the response is a 202 Accepted with a Location header pointing at the polling endpoint for the remaining work; a 200 means analysis is already complete. For real-time progress updates, use GET /api/scan/stream which serves a text/event-stream. A complete stored result younger than the freshness window is returned as-is (servedFromCache, resultAgeSeconds, Age header) without running or persisting a scan - pass force: true, or widen/narrow maxAgeSeconds, to control that. Rate limited two ways: 10 requests per minute per IP (burst) and a durable daily scan budget shared with the other scan entry points - both return 429 with a Retry-After header.
PARAMETERS
REQUEST BODYrequired
stripe.com21600RESPONSES
200Scan completed successfully and analysis is complete. With `?competitors=1`, also carries a `competitors` object (category leaders + neighbor window drawn from the leaderboard). With `?format=audit` the body is `#/components/schemas/AuditScanResult` instead of the shape below. A response served from the freshness window instead of a new scan additionally carries `servedFromCache: true` and `resultAgeSeconds` (on both body shapes) plus an `Age` header, and consumed no scan. -> ScanResult202Scan accepted and scored, but analysis is still in progress (analysisStatus is 'partial'). The body is the same shape as a 200 (including `competitors` when `?competitors=1` was passed, and `#/components/schemas/AuditScanResult` when `?format=audit` was passed). Poll the Location header URL (GET /api/score/{domain}) until analysisStatus is 'complete' and pendingChecks is empty. -> ScanResult400Invalid input, or `ephemeral: true` for a domain that already has a real stored scan (body carries `code: "EPHEMERAL_CLOBBER"`; storing a disposable result would replace the real one). -> ErrorResponse422MCP authentication is required. No score or grade was produced. The failed attempt does not overwrite a previous measured scan. -> McpAuthRequiredResponse429Rate limit exceeded - either the 10-per-minute burst cap or the durable daily quota (30 scans per rolling 24h per IP; 6 per day for force=true). Cache-served responses never count against the daily quota. The response includes a Retry-After header indicating seconds until the next request is allowed, and a JSON body with error and retry_after_ms. -> ErrorResponse500Scan failed/api/scan/streamStream an agent-readiness scan as Server-Sent Events
Runs a full agent-readiness scan on the given URL and streams progress as text/event-stream. Accepts a domain, MCP server URL, or MCP App URL (server that supports the MCP Apps extension `io.modelcontextprotocol/ui`) - the server auto-detects which kind of input was provided and selects the appropriate check set. Catalog-style listing pages are folded into the `mcp` kind by classifying the first validated embedded MCP URL. The stream emits a `kind_detecting` event immediately after the cheap reachability probe, followed by exactly one `kind_detected` event with payload `{ kind: 'domain' | 'mcp' | 'mcp-app', mcpUrl?: string, embeddedMcpUrls?: string[], hint?: string }` once URL-kind detection resolves. Subsequent events include `scan_init`, `layer_start`, `check_start`, `check_complete`, `layer_complete`, and finally `scan_complete` whose payload mirrors the ScanResult schema (including the optional `urlKind` field indicating the detected kind). The same freshness gate as POST /api/scan applies: a hit is a `kind_detected` frame followed by the terminal `scan_complete` event, rather than a full run. Rate limited two ways: 10 requests per minute per IP (burst) and a durable daily scan budget shared with the other scan entry points - both return 429 with a Retry-After header.
PARAMETERS
RESPONSES
200Server-Sent Events stream of scan progress. If MCP authentication is required, the stream ends with an error event carrying code MCP_AUTH_REQUIRED, mcpAuthRequired: true, mcpUrl and urlKind, without scan_complete or a score. With `?format=audit`, the terminal `scan_complete` event's `result` is `#/components/schemas/AuditScanResult`; every other event is unchanged. When a stored result inside the freshness window answers the request, the stream is a `kind_detected` frame followed by a terminal `scan_complete` event carrying `servedFromCache: true` and `resultAgeSeconds`, and the response carries an `Age` header. After `scan_complete`, a live scan may emit two correction events before the stream closes, both optional: `relevance_assessed` (`{ naCheckIds, reasons, score, grade }`, only when some checks are judged not applicable for this product) carries the corrected `score` and `grade`, and `summary_ready` (`{ agenticSummary }`) carries the summary. Consume events until the stream closes. Closing does not mean the analysis is final: when deep checks are still pending, the stream closes early and a background worker finishes relevance and summary later. Read GET /api/score/{domain} after the stream closes and poll it while `analysisStatus` is `partial`. A freshness-window hit is already final and emits neither event.400Missing or invalid domain parameter429Rate limit exceeded - either the 10-per-minute burst cap or the durable daily quota (30 scans per rolling 24h per IP; 6 per day for force=1). The response carries a Retry-After header with the seconds until the caller's window frees up./api/scan/checksRun a selected subset of checks against a URL
Runs only the checks you select against the given URL and returns per-check results - the re-verify step after shipping a fix, with check ids from GET /api/checks. The run always executes; results are never served from a cache, so a re-check reflects the fix you just deployed (allow for DNS and CDN caches clearing). For most or all of the catalog, use POST /api/scan instead: same budget unit, and it returns a score. The response carries no aggregate score; GET /api/score/{domain} is the score surface. Callers holding a scan API key also get stored-scan patching - see storedScanUpdated on the response. Rate limited two ways: 10 requests per minute per IP, and one run spends one unit of the daily scan budget shared with POST /api/scan, spent once the target has been probed and classified; requests rejected earlier (invalid input, unknown ids, unreachable) spend nothing. Scan API key callers are exempt from both - an exemption, not a larger allowance; keys are issued manually on request - contact ora.
REQUEST BODYrequired
RESPONSES
200Per-check results for the selection - at least one entry per requested id, including 'na' entries for ids that cannot apply to the detected kind. Always synchronous and complete: there is no 202 and no pending status. -> RunChecksResponse400Invalid input - either a schema failure (`{ error, details }` with the Zod detail, whose checkIds bounds messages point at POST /api/scan for full runs) or an id the catalog does not list (`code: "UNKNOWN_CHECK_IDS"` with the offending ids echoed in `details` and GET /api/checks as the pointer), or an invalid / unreachable domain. -> ErrorResponse429Rate limit exceeded - either the 10-per-minute burst cap (body `{ error }`) or the durable daily scan budget shared with POST /api/scan (body also carries `retry_after_ms`). Both carry a Retry-After header with the seconds until the caller's window frees up. -> ErrorResponse500Selective run failed