Scan API reference

How to authenticate a request, and the two endpoints the Scan API answers: one creates a scan and one reads it back. Each comes with its fields and the headers a response carries.

Authentication

Every request carries your secret key as a bearer token:

Authorization: Bearer awsk_YOUR_SECRET_KEY

An X-API-Key header is also read, mainly so that pasting the wrong key gives you a clear answer rather than a baffling 401 — Authorization: Bearer is the documented form. A rejected request answers 401 with a WWW-Authenticate challenge and a machine-readable error.reason of missing, malformed, widget_key, unknown, revoked or expired.

Endpoints

Two of them. Both live under https://app.accesswiser.com/api/v1, both answer JSON, and both send Cache-Control: private, no-store.

POST /scans — create a scan

Request bodies are capped at 4 KB. Fields:

POST /scans request body fields
FieldTypeNotes
urlstringRequired. The page to scan. http and https only; if you omit the scheme, https:// is added. The host must be one of the sites added to your account — the site your plan covers and where your widget is installed — and any other host is refused with 403 site_not_added before a request is made to it. The scanner never connects to a private or internal address.
scanClass"quick" | "evidence"Optional, defaults to "quick". Any other value is a 400 — it is never silently downgraded, because that would bill the wrong pot. Both classes run the same analysis today; the class decides which allowance and credit pot pays. See scan classes, allowances and credits.
restaurantbooleanOptional, defaults to false. Adds the restaurant-vertical detectors (PDF menus, menus published as images of text, third-party ordering and reservation flows) on top of the standard checks.
ecommercebooleanOptional, defaults to false. Adds the online-store detectors (cart and checkout update announcements, redundant address entry, authentication alternatives) on top of the standard checks. These findings are review items for a person to verify, not automated failures.
webhookUrlstringOptional. An https URL we call when the scan reaches a terminal state. Validated when you create the scan, not at delivery time, so an unusable callback fails your request with a 400 instead of silently never calling back.

Optional header: Idempotency-Key, 1–255 printable ASCII characters. Replaying a key that already produced a scan returns that same scan with "idempotent": true and an Idempotency-Replayed: true header, and costs nothing. A key whose first request is still in flight gets 409 idempotency_key_in_flight.

A successful create is 202 Accepted with a Location header. Note chargedTo, which tells you whether the scan came out of your monthly allowance or out of purchased credits. The Quickstart shows a full 202 response.

GET /scans/{id} — status and result

The id is a UUID; anything else is a 404. So is a scan belonging to another account — deliberately, so this endpoint cannot be used to discover that someone else's scan exists.

curl https://app.accesswiser.com/api/v1/scans/6f1c0f6e-6c2c-4a3b-9f5a-2a1d3b4c5d6e \
  -H "Authorization: Bearer awsk_YOUR_SECRET_KEY"
{
  "scanId": "6f1c0f6e-6c2c-4a3b-9f5a-2a1d3b4c5d6e",
  "status": "completed",
  "scanClass": "quick",
  "url": "https://your-site.example.com/pricing",
  "healthScore": 82,
  "summary": {
    "totalIssues": 14,
    "critical": 1,
    "serious": 4,
    "moderate": 6,
    "minor": 3,
    "incomplete": 2
  },
  "findings": [
    {
      "findingId": "9d7f2a10-4c3b-4e5d-8a6f-1b2c3d4e5f60",
      "ruleId": "menu-image",
      "impact": "critical",
      "criteria": [
        { "id": "1.1.1", "level": "A" },
        { "id": "1.4.5", "level": "AA" }
      ],
      "section508": true,
      "en301549": true,
      "help": "The menu is published as an image, so its text is unavailable to a screen reader.",
      "helpUrl": "https://www.w3.org/WAI/WCAG22/Understanding/images-of-text.html",
      "selector": "main > img:nth-of-type(2)",
      "html": "<img src=\"/menu.png\" width=\"900\">",
      "fixSummary": "Publish the menu as HTML text, or provide the same content in text alongside the image.",
      "category": "restaurant"
    }
  ],
  "findingsPagination": {
    "limit": 50,
    "offset": 0,
    "returned": 14,
    "total": 14,
    "impact": null,
    "hasMore": false,
    "nextUrl": null
  },
  "engineVersion": "axe-core@4.10.2",
  "error": null,
  "createdAt": "2026-07-31T09:14:02.117Z",
  "startedAt": "2026-07-31T09:14:02.402Z",
  "completedAt": "2026-07-31T09:14:29.884Z",
  "terminal": true,
  "disclaimer": "Automated testing detects only a portion of ..."
}
GET /scans/{id} response fields
FieldTypeNotes
status"pending" | "running" | "completed" | "failed"The scan's current state.
terminalbooleantrue once the status is completed or failed. Poll on this rather than hard-coding the status vocabulary.
healthScorenumber | nullnull unless the scan completed. An internal progress indicator only — not a legal determination, certification or guarantee, and not a compliance score.
summaryobject | nullnull unless the scan completed. Counts only: totalIssues, critical, serious, moderate, minor, incomplete. The per-issue detail is in findings.
findingsobject[] | nullOne page of what the scan found, in our own schema — no engine result envelope, no nodes or tags arrays. Each finding carries findingId, ruleId, impact, criteria (the WCAG 2.2 success criteria it fails, each with its own level — a finding routinely spans levels, so there is no single level for the finding), section508, en301549, help, helpUrl, selector, html, fixSummary and category. null — not []— until the scan completes, because an empty array from a running scan would read as “we looked and found nothing”.
findingsPaginationobject | nulllimit, offset, returned, total, the impact filter in force, hasMore, and a ready-made nextUrl. Control it with ?limit= (1–200, default 50), ?offset= and ?impact=critical,serious. Values outside those bounds are a 400, never a silent correction. The order is stable and repeatable but not ranked by severity — filter for the severities you act on rather than reading page one.
engineVersionstring | nullWhich version of the scanning engine produced the result.
errorstring | nullPopulated only when status is "failed".
createdAt / startedAt / completedAtstring | nullISO-8601 timestamps. startedAt and completedAt are null until they happen.
disclaimerstringPresent on every scan-bearing response. Surface it wherever you surface the numbers.

A completed scan returns its findingsas well as the counts — the element, the WCAG 2.2 success criteria it fails with a level on each criterion, and what to change. They come back in AccessWiser's own schema rather than the underlying test engine's, so there is no result envelope to unpick. Findings are paginated: follow findingsPagination.nextUrl until it is null.

A finding is a barrier we detected. An empty page is not evidence that a page has none — automated testing finds a portion of what is there, and cannot replace manual testing with assistive technology.

A scan that our workers cannot finish inside the time limit is marked failed and is not charged: it stops counting against your monthly allowance, and if it was paid for with a credit, the credit is returned.

Response headers

Allowance state rides on headers as well as the body, so a CI job can read it without parsing JSON — including from a 429, whose body is not much use to you.

  • X-Scan-Class — which pot the numbers below refer to.
  • X-Scan-Allowance-Limit, X-Scan-Allowance-Remaining, X-Scan-Allowance-Reset— this month's allowance for that class and when it resets.
  • X-Scan-Credits-Remaining — purchased credits of that class, a separate pot.
  • On a rate-limited response: Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.