Scanning

Run agent-readiness scans and stream progress. Scans are rate-limited per IP and cached; cached responses never consume quota.

POST/api/scan

Scan 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
competitors
string
query
Pass 1 to include a `competitors` object in the response: the category top-5 leaders plus the neighbor window (2 above / self / 2 below) drawn from the leaderboard. Returned only for domains with a market category - unclassified domains (Community, or no leaderboard row) get `competitors: null`. Omitted by default so the plain response stays lean.
format
string
query
Pass `audit` to receive the versioned, allowlisted audit shape (AuditScanResult / AuditScoreResult) instead of the default body: every field is documented, carries a `contractVersion`, and internal fields are dropped. Omit it and the response is unchanged from previous releases. On GET /api/scan/stream, only the terminal `scan_complete` event's `result` is projected - all other events are identical to the default stream.
include
string
query
Opt-in expansion list (comma-separated). `essentials` adds one response key, `essentials`: an alternate reading of the same scan carrying its own `score` (0-100 or null - required checks share 80 points, recommended 20, forward-looking signals upside-only), the required/recommended buckets, label copy, per-surface sub-scores, access signals, and a `checks` map keyed by check id. That map holds the essentials INTERPRETATION only (tier, bonus, fraction, occurrences, `essentialsGain`) - name, status, details, ora's recommendation, and `estScoreGain` for the same id stay in `layers[].checks[]`, so nothing serializes twice; join on the id. The pre-sorted `issues` list and `scoreEvidence` are arrays of ids resolving in that map. Omit the parameter and the response is byte-identical to previous releases. On GET /api/checks, `essentials` instead adds `essentialsTier`, `essentialsBonusOnly`, and `essentialsExcluded` to every catalog check (an excluded check never enters the essentials model; since 1.19.1 that is the two robots.txt policy checks). Experimental: `siteType` sits outside the versioned contract, so its shape may change without a version bump, and pinning the contract major does not freeze it. `siteType` adds one response key, `siteType`: the same scan scored against what the site is (`content`, `business`, `app`, or `store`). It carries `type`, `source` (`declared`, `self-declared`, `classified`, `default`, `blocked`, or `unavailable`), `confidence` (`high` or `low`), `basis`, `score` (0-100 or null), per-layer `layers` (`id`, `score`, `maxScore`), per-check `states` (`req`, `bonus`, `gated`, or `emerging`), and `gatedOut` (each excluded check id with the gate that excluded it). A site-type score uses a different rubric from the canonical `score` and can land above or below it: never compare the two, only another `siteType.score` of the same `type`. When no type resolves, the key is `{ type: null, source: "unavailable", confidence: "low", score: null, layers: [] }` with a `basis` saying why.
REQUEST BODYrequired
url
string
required
The domain, MCP server URL, or MCP app URL to scan. The server detects which kind of input was provided and runs the appropriate check set.e.g. stripe.com
mcpUrl
string
Optional MCP server URL to inspect as the sole MCP target. A failed endpoint is never replaced by a discovered server
maxAgeSeconds
integer
How stale a stored result may be and still be returned instead of running a new scan. Defaults to 21600 (6 hours) and is clamped server-side to [3600, 86400] rather than rejected. See the 200 response's `servedFromCache` / `resultAgeSeconds` fields.e.g. 21600
force
boolean
Always run a live scan, whatever the age of the stored result. This is the only way to bypass the freshness window entirely.
ephemeral
boolean
Store the result as disposable: it is excluded from the leaderboard, the sites-scanned coverage count, research statistics, and score history, is served with Cache-Control: no-store, and is deleted after a few days. Intended for a local site exposed through a tunnel, or any host that will not exist tomorrow. Public tunnel hostnames (trycloudflare.com, ngrok, and similar) are stored this way whether or not the flag is set. Rejected with 400 EPHEMERAL_CLOBBER when the domain already has a real stored scan, since a disposable result would replace it.
siteType
string
Optional. With `?include=siteType`, scores the response's `siteType` reading against this type instead of the inferred one. Absent, the type is inferred.content | business | app | store
RESPONSES
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. -> ScanResult
202Scan 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. -> ScanResult
400Invalid 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). -> ErrorResponse
422MCP authentication is required. No score or grade was produced. The failed attempt does not overwrite a previous measured scan. -> McpAuthRequiredResponse
429Rate 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. -> ErrorResponse
500Scan failed
GET/api/scan/stream

Stream 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
domain
string
query
required
The domain, MCP server URL, or MCP app URL to scan. The server detects which kind of input was provided and runs the appropriate check set.
mcp
string
query
Optional MCP server URL to inspect as the sole MCP target; it is never replaced by a discovered server
maxAgeSeconds
integer
query
Same freshness window as POST /api/scan: how stale a stored result may be and still be streamed back instead of running a new scan. Defaults to 21600 (6 hours), clamped to [3600, 86400].
force
string
query
Pass 1 to always run a live scan, whatever the age of the stored result.
ephemeral
string
query
Pass 1 to store the result as disposable - the same flag POST /api/scan takes in its body, with the same 400 EPHEMERAL_CLOBBER refusal when the domain already has a real stored scan.
format
string
query
Pass `audit` to receive the versioned, allowlisted audit shape (AuditScanResult / AuditScoreResult) instead of the default body: every field is documented, carries a `contractVersion`, and internal fields are dropped. Omit it and the response is unchanged from previous releases. On GET /api/scan/stream, only the terminal `scan_complete` event's `result` is projected - all other events are identical to the default stream.
include
string
query
Opt-in expansion list (comma-separated). Experimental: `siteType` sits outside the versioned contract, so its shape may change without a version bump, and pinning the contract major does not freeze it. `siteType` adds one response key, `siteType`: the same scan scored against what the site is (`content`, `business`, `app`, or `store`). It carries `type`, `source` (`declared`, `self-declared`, `classified`, `default`, `blocked`, or `unavailable`), `confidence` (`high` or `low`), `basis`, `score` (0-100 or null), per-layer `layers` (`id`, `score`, `maxScore`), per-check `states` (`req`, `bonus`, `gated`, or `emerging`), and `gatedOut` (each excluded check id with the gate that excluded it). A site-type score uses a different rubric from the canonical `score` and can land above or below it: never compare the two, only another `siteType.score` of the same `type`. When no type resolves, the key is `{ type: null, source: "unavailable", confidence: "low", score: null, layers: [] }` with a `basis` saying why. On the stream the key rides the terminal `scan_complete` event beside `result`, never inside it.
siteType
string
query
Read-time lens for `include=siteType`: scores the scan against this type instead of the resolved one, without rescanning or storing anything. An unrecognized value is ignored, not rejected.
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 parameter
429Rate 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.
POST/api/scan/checks

Run 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
url
string
required
The website, MCP server, or API URL to run checks against. A bare domain like example.com is accepted.
checkIds
array
required
Array of check ids from GET /api/checks - one id minimum, up to the catalogued check count. Duplicate ids are deduplicated; ids the catalog does not list are rejected with error code UNKNOWN_CHECK_IDS.
mcpUrl
string
Optional URL of the target's MCP server, matching the same field on POST /api/scan. It pins the MCP endpoint; a failed handshake never substitutes a discovered server. When omitted, ora auto-discovers MCP endpoints. An empty string is treated as absent.
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. -> RunChecksResponse
400Invalid 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. -> ErrorResponse
429Rate 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. -> ErrorResponse
500Selective run failed