{"openapi":"3.0.3","info":{"title":"WebAnalyzer API","version":"1.0.0","description":"Free REST API for WebAnalyzer (webanalyzer.dev): analyze any public web page and get a 0-100 quality score across 8 dimensions (SEO, GEO/AI readiness, performance, accessibility, code quality, UI/UX, data & tracking, security) computed from 290+ deterministic checks.\n\n**Authentication:** none. The API is deliberately open and keyless — no signup, no API key, no OAuth. Abuse is controlled with per-IP rate limits (standard RateLimit headers on every response).\n\n**Versioning & deprecation policy** (full text: https://webanalyzer.dev/deprecation-policy.md): the API is path-versioned (/api/v1). Backwards-incompatible changes only ever ship under a new /api/v2 prefix; v1 stays alive for at least 6 months after a deprecation notice. Deprecated operations are announced at https://webanalyzer.dev/developers and carry Deprecation (RFC 9745) and Sunset (RFC 8594) headers on their responses for the whole grace period.\n\n**Sandbox:** pass dryRun: true to POST /api/v1/analyze for a deterministic sample response — build and test an integration without spending an analysis or touching a real site.\n\n**Scoring note:** scores use a deliberately demanding curve with critical caps — a 70 is a good site. Do not compare against linear percentage graders.","contact":{"name":"WebAnalyzer","url":"https://webanalyzer.dev/contact","email":"hello@webanalyzer.dev"},"termsOfService":"https://webanalyzer.dev/privacy"},"externalDocs":{"description":"Developer portal","url":"https://webanalyzer.dev/developers"},"servers":[{"url":"https://webanalyzer.dev","description":"Production"},{"url":"https://sandbox.webanalyzer.dev","description":"Sandbox environment: same paths and response shapes, deterministic sample data, no side effects — POST /api/v1/analyze here (or pass dryRun: true against production) to test an integration."},{"url":"https://webanalyzer.dev/api/v1/sandbox","description":"Sandbox as a path prefix on the production host: POST /analyze under it."}],"security":[],"paths":{"/api/v1":{"get":{"operationId":"getApiIndex","summary":"API index","description":"Lists every endpoint, the auth story and the versioning policy in machine-readable form.","responses":{"200":{"description":"API index.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"},"description":{"type":"string"},"authentication":{"type":"string"},"openapi":{"type":"string","format":"uri"},"documentation":{"type":"string","format":"uri"},"mcpServer":{"type":"string","format":"uri"},"versioningPolicy":{"type":"string"}}}}}}}}},"/api/v1/analyze":{"post":{"operationId":"analyzeUrl","summary":"Analyze a public web page","description":"Runs the full 290+ check analysis against a public URL and returns the overall 0-100 score, letter grade, per-dimension scores and top issues. Synchronous — typically completes in 5-20 seconds. Not for localhost, intranet hosts or pages behind a login. Retries are safe: send an Idempotency-Key header and a repeated request returns the original result instead of re-running the analysis.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Any unique string. Repeating a request with the same key replays the first result instead of re-analyzing.","schema":{"type":"string","maxLength":200}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Full URL of the public page to analyze.","example":"https://example.com"},"dryRun":{"type":"boolean","default":false,"description":"Sandbox mode: return a deterministic sample response without running an analysis or touching the URL. For building and testing integrations."}}}}}},"responses":{"200":{"description":"Analysis complete.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisSummary"}}}},"400":{"description":"Malformed request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The URL could not be analyzed (unreachable, blocked host, not public).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — honor Retry-After.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analyses":{"post":{"operationId":"startAnalysisJob","summary":"Start analyses as async jobs (single or batch)","description":"The asynchronous twin of analyzeUrl: returns 202 Accepted immediately with a job id and a status URL to poll (GET /api/v1/analyses/{id}). Also the batch endpoint: pass urls (an array, up to 3) instead of url to fan out one job per URL in a single request — each URL spends one slot of the same per-IP rate budget.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Pass exactly one of url (single job) or urls (batch).","properties":{"url":{"type":"string","format":"uri","description":"Full URL of the public page to analyze (single job).","example":"https://example.com"},"urls":{"type":"array","minItems":1,"maxItems":3,"description":"Batch mode: an array of URLs, one job each.","items":{"type":"string","format":"uri"}}}}}}},"responses":{"202":{"description":"Job(s) accepted. Single url: an AnalysisJob (Location header carries the status URL). Batch urls: {jobs: AnalysisJob[]}. Poll each statusUrl.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}},"Location":{"description":"Status URL for the job.","schema":{"type":"string","format":"uri"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisJob"}}}},"400":{"description":"Malformed request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The URL is invalid, blocked or not public.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — honor Retry-After.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analyses/{id}":{"get":{"operationId":"getAnalysisJob","summary":"Poll an async analysis job","description":"Returns the job status: running (poll again after Retry-After seconds), complete (with the analysis result), or failed.","parameters":[{"name":"id","in":"path","required":true,"description":"Job id from startAnalysisJob.","schema":{"type":"string"}}],"responses":{"200":{"description":"Current job state.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisJob"}}}},"404":{"description":"Unknown job id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — honor Retry-After.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analyses/batch":{"post":{"operationId":"startAnalysisJobsBatch","summary":"Batch: start one async job per URL","description":"The batch endpoint in its plainest shape: the request body is a JSON array of up to 3 URLs. Fans out one async analysis job per URL and returns 202 with {jobs: AnalysisJob[]}. Each URL spends one slot of the same per-IP rate budget.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","minItems":1,"maxItems":3,"description":"URLs to analyze, one job each.","items":{"type":"string","format":"uri"}}}}},"responses":{"202":{"description":"Jobs accepted; poll each job's statusUrl.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["jobs"],"properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/AnalysisJob"}}}}}}},"400":{"description":"Body is not an array of 1-3 URLs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — honor Retry-After.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandbox/analyze":{"post":{"operationId":"sandboxAnalyze","summary":"Sandbox: sample analysis, no side effects","description":"The sandbox environment: always returns the same deterministic sample analysis — nothing is fetched, nothing is stored. Point a client here (or pass dryRun: true to analyzeUrl) to build and test an integration without touching production data or spending an analysis.","tags":["sandbox"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","description":"Accepted and ignored — the response is fixed sample data."}}}},"responses":{"200":{"description":"The fixed sample analysis (id: \"sandbox\", sandbox: true).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisSummary"}}}}}}},"/api/v1/leaderboard":{"get":{"operationId":"getLeaderboard","summary":"Get the public leaderboard","description":"Highest-scoring analyzed sites, best first. Cursor-paginated: pass the nextCursor from one page as cursor for the next.","parameters":[{"name":"limit","in":"query","required":false,"description":"Entries per page, 1-100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"cursor","in":"query","required":false,"description":"Opaque pagination token from a previous response's nextCursor.","schema":{"type":"string"}}],"responses":{"200":{"description":"One page of the leaderboard.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["entries","nextCursor"],"properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/LeaderboardEntry"}},"nextCursor":{"type":"string","nullable":true,"description":"Token for the next page, or null on the last page."},"viewUrl":{"type":"string","format":"uri"}}}}}},"400":{"description":"Invalid limit or cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — honor Retry-After.","headers":{"RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"string"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"optionalBearer":{"type":"http","scheme":"bearer","description":"OPTIONAL. Every endpoint works with no credential at all. For agent stacks that require an OAuth flow, register at /api/v1/oauth/register (RFC 7591) and exchange client credentials for a token at /api/v1/oauth/token — discovery at /.well-known/oauth-authorization-server (RFC 8414). Tokens grant no extra privilege."}},"schemas":{"Error":{"type":"object","description":"Every non-2xx response carries this structured error.","required":["error"],"properties":{"error":{"type":"object","required":["code","message","status"],"properties":{"code":{"type":"string","description":"Stable machine-readable error code.","enum":["invalid_request","invalid_url","blocked_host","fetch_failed","rate_limited","not_found","method_not_allowed","internal_error"]},"message":{"type":"string","description":"Human-readable explanation of what went wrong."},"status":{"type":"integer","description":"HTTP status code, duplicated in the body for logging convenience."},"documentation":{"type":"string","format":"uri","description":"Link to the error documentation."}}}}},"DimensionScore":{"type":"object","description":"Score for one of the 8 quality dimensions.","required":["key","label","score","grade"],"properties":{"key":{"type":"string","description":"Stable dimension identifier.","enum":["seo","geo","performance","accessibility","codeQuality","uiux","dataTracking","security"]},"label":{"type":"string","description":"Human-readable dimension name."},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Dimension score, 0-100."},"grade":{"type":"string","description":"Letter grade, A+ through F."},"summary":{"type":"string","description":"One-sentence summary of the dimension result."}}},"Issue":{"type":"object","description":"One of the most important problems found on the page.","required":["dimension","title"],"properties":{"dimension":{"type":"string","description":"Dimension the issue belongs to."},"title":{"type":"string","description":"What is wrong, in one sentence."},"detail":{"type":"string","description":"Specifics of the finding on this page."}}},"AnalysisSummary":{"type":"object","description":"The result of analyzing one URL.","required":["id","url","score","grade","dimensions"],"properties":{"id":{"type":"string","description":"Analysis id — also the key for the badge and report URLs."},"url":{"type":"string","format":"uri","description":"Final URL after redirects."},"title":{"type":"string","nullable":true,"description":"Page title."},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Overall 0-100 score (demanding curve)."},"grade":{"type":"string","description":"Letter grade, A+ through F."},"dimensions":{"type":"array","items":{"$ref":"#/components/schemas/DimensionScore"}},"topIssues":{"type":"array","items":{"$ref":"#/components/schemas/Issue"}},"badgeUrl":{"type":"string","format":"uri","description":"Embeddable SVG score badge."},"reportUrl":{"type":"string","format":"uri","description":"Human-readable report page."},"scoringNote":{"type":"string"}}},"AnalysisJob":{"type":"object","description":"State of an asynchronous analysis job.","required":["id","status"],"properties":{"id":{"type":"string","description":"Opaque job id."},"status":{"type":"string","enum":["running","complete","failed"],"description":"Job state."},"statusUrl":{"type":"string","format":"uri","description":"URL to poll for this job."},"estimatedSeconds":{"type":"integer","description":"Rough seconds until completion while running."},"result":{"$ref":"#/components/schemas/AnalysisSummary"},"error":{"type":"object","description":"Present when status is failed.","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}},"LeaderboardEntry":{"type":"object","required":["rank","name","url","score"],"properties":{"rank":{"type":"integer","minimum":1},"name":{"type":"string","description":"Display name of the site."},"url":{"type":"string","format":"uri"},"score":{"type":"integer","minimum":0,"maximum":100}}}}}}