is-aesthetic

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: GET and POST on /api/v1/report, described below and in /openapi.json.
  • Markdown: send Accept: text/markdown to a report page, or open /r/<host>/report.md.
  • Command line: pip install is-aes, then is-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"
StatusMeaning
200The 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.
202The 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.
404not_found: no report for this host yet. Start one with POST.
422The 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": "…" }
}
  • score and grade are null when nothing could be evaluated reliably. That is not a zero.
  • captureStatus says whether the requested page was fully captured (complete, partial or blocked), with a captureReason when it wasn't. Extra pages never decide it; a page that wasn't scanned carries a notScannedReason.
  • checks has one entry per check per page. outcome is pass, warn, fail or excluded. Excluded checks carry an exclusionReason and never count against the score.
  • evidence lists 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 cropGeometryVersion is 1, cropBox records the retained crop's [x, y, width, height] in document CSS pixels at that viewport; box still describes the full element. cropSize is the exported pixel size, and cropScaleXY gives exported pixels per CSS pixel on each axis (cropScale is 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.
  • engine pins 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.md

GET /api/v1/report also answers in Markdown when asked with Accept: text/markdown.

Errors and limits

Errors are JSON: {"error": {"code", "message", "retryable"}}.

StatusCodeWhen
400invalid_url, invalid_requestNot an absolute http(s) URL, has a user name, password or port, or the body isn't JSON.
403unsafe_urlThe address resolves to a private, internal or reserved network.
403robots_deniedThe site's robots.txt doesn't allow our scanner.
404not_foundNo report for this host yet.
413request_too_largeThe request body is over 4 KB.
429rate_limited, capacityToo many scans from you, or every scanner is busy. Retry after Retry-After.
503unavailableThe 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.com

Or run it without installing, with uv or as a Python module:

uvx is-aes vercel.com
python -m is_aes vercel.com

It 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 --wait

From 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