Docs
The report, anywhere you need it
Every report is a web page, a JSON object and a Markdown document. No key is needed. Be gentle: scans run in real browsers.
Overview
- Web:
https://is-aesthetic.com/r/<host>shows the latest report for a host, for example /r/www.wikipedia.org. - API:
GETandPOSTon/api/v1/report, described below and in /openapi.json. - Markdown: send
Accept: text/markdownto a report page, or open/r/<host>/report.md. - Command line:
pip install is-aes, thenis-aes vercel.com. An npm client is coming soon.
Reports are kept per host. A host keeps its www., so www.example.com and example.com are separate reports. A new scan of a host replaces its report once it is complete; until then the last complete report stays, with a note.
Read a report
GET/api/v1/report?url=<absolute URL>
Returns the latest complete report for the URL's host. It never starts a scan. Percent-encode the URL; it must include https:// or http://.
curl "https://is-aesthetic.com/api/v1/report?url=https%3A%2F%2Fexample.com"| Status | Meaning |
|---|---|
| 200 | The report itself (see below). While a newer scan of the host is queued or running, or after one failed, this is still the last complete report, with a rescan object: status, started and, when it failed, a reason. |
| 202 | The host's first scan is queued or running. The body is the job: id, url, host, status, createdAt, updatedAt, pollUrl. Poll again after Retry-After seconds. |
| 404 | not_found: no report for this host yet. Start one with POST. |
| 422 | The host's first scan failed. The body has status: "failed" and an error; a new POST retries. |
Start a scan
POST/api/v1/report
Body: {"url": "https://example.com"}. Returns 202 with the job. Posting again while that host is queued or running returns the same job and doesn't start a second scan. Poll GET every three seconds, or as Retry-After says, and give up after a couple of minutes.
curl -X POST https://is-aesthetic.com/api/v1/report \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'The report object
Reports follow JSON Schema report v1. A shortened example:
{
"schemaVersion": "1.0",
"status": "complete",
"url": "https://example.com/",
"host": "example.com",
"scannedAt": "2026-10-24T01:12:09.000Z",
"score": 74.2,
"grade": "C",
"captureStatus": "complete",
"domains": [{ "id": 1, "name": "Visual communication", "score": 81.3, "evaluated": 6, "excluded": 2 }, …],
"tiers": [{ "tier": "essential", "earned": 58.4, "possible": 80, "evaluated": 11, "excluded": 3 }, …],
"checks": [{
"id": "C158",
"name": "Targets are big enough",
"domain": 4,
"tier": "essential",
"outcome": "fail",
"measured": "2 targets too small",
"threshold": { "pass": "Targets meet the 24 px size or spacing requirement, or an exception", "warn": "", "fail": "…" },
"evidence": [{ "cropUrl": "/api/v1/evidence?path=…", "box": [769, 1110, 50, 18], "viewport": 1440,
"selector": "main > … > a", "cropSize": [100, 36], "cropScale": 2 }],
"fix": "Give small controls at least 24 × 24 px of hit area …",
"pageUrl": "https://example.com/"
}, …],
"notCovered": ["Text contrast is not measured yet …"],
"coverage": { "evaluated": 23, "excluded": 7, "rulesRun": ["C006.1", …] },
"engine": { "revision": "…", "registrySha256": "…", "axeVersion": "4.11.0", "artifactSha256": "…" }
}scoreandgradearenullwhen nothing could be evaluated reliably. That is not a zero.captureStatussays whether the requested page was fully captured (complete,partialorblocked), with acaptureReasonwhen it wasn't. Extra pages never decide it; a page that wasn't scanned carries anotScannedReason.checkshas one entry per check per page.outcomeispass,warn,failorexcluded. Excluded checks carry anexclusionReasonand never count against the score.evidencelists the flagged elements: a crop (at most 600 px, served through/api/v1/evidence), the element's box in CSS pixels, the viewport width and a CSS selector.- When
cropGeometryVersionis1,cropBoxrecords the retained crop's[x, y, width, height]in document CSS pixels at that viewport;boxstill describes the full element.cropSizeis the exported pixel size, andcropScaleXYgives exported pixels per CSS pixel on each axis (cropScaleis the x ratio). Crops can be partial or offset, and resized images can be 1×. Older reports omit this versioned geometry; keep their saved values as they are. enginepins the exact engine revision and rule registry that produced the report. See scoring.
Markdown
For agents and terminals, the same report as Markdown, with findings, thresholds, fixes and exclusions:
curl -H "Accept: text/markdown" https://is-aesthetic.com/r/example.com
curl https://is-aesthetic.com/r/example.com/report.mdGET /api/v1/report also answers in Markdown when asked with Accept: text/markdown.
Errors and limits
Errors are JSON: {"error": {"code", "message", "retryable"}}.
| Status | Code | When |
|---|---|---|
| 400 | invalid_url, invalid_request | Not an absolute http(s) URL, has a user name, password or port, or the body isn't JSON. |
| 403 | unsafe_url | The address resolves to a private, internal or reserved network. |
| 403 | robots_denied | The site's robots.txt doesn't allow our scanner. |
| 404 | not_found | No report for this host yet. |
| 413 | request_too_large | The request body is over 4 KB. |
| 429 | rate_limited, capacity | Too many scans from you, or every scanner is busy. Retry after Retry-After. |
| 503 | unavailable | The scanner or report storage is temporarily unavailable. Retry after Retry-After. |
Each IP address can start five new scans an hour, and at most three scans run at once across the service. Reading reports isn't limited in the same way, but please cache what you fetch.
Command-line client
The Python client is on PyPI and needs Python 3.9 or later.
pip install is-aes
is-aes vercel.comOr run it without installing, with uv or as a Python module:
uvx is-aes vercel.com
python -m is_aes vercel.comIt prints the latest report for the site, or starts a scan when there is none. --wait polls until the report is ready, --timeout caps the wait in seconds (120 by default), and --json prints the raw report or job.
is-aes vercel.com --wait --timeout 120
is-aes vercel.com --json --waitFrom Python:
from is_aes import check
report = check("vercel.com", wait=True)
print(report["score"], report["grade"])An npm client is coming soon.
OpenAPI and llms.txt
- /openapi.json describes the API in OpenAPI 3.1, including the report schema.
- /llms.txt is a short guide to the site for language models and agents.
- /schema/report-v1.json is the JSON Schema for a report.