Skip to content
A11ySignal

Documentation

The API

The same endpoints the dashboard calls. Pull scores into your own reporting, kick off a scan from a deploy hook, or fail a build when a release breaks something.

Available on the Agency plan and above.

Authenticating

Create a key under Settings. It is shown once. Send it as X-Api-Key on every request.

curl https://api.a11ysignal.com/v1/sites \
  -H "X-Api-Key: a11y_xxxxxxxxxxxxxxxxxxxxxxxx"

A key is scoped to one workspace and has admin rights within it. Revoking a key takes effect immediately.

Endpoints

GET/v1/sitesEvery site in the workspace, with its latest score and schedule.list
POST/v1/sitesAdd a site. Body matches the form in the dashboard.create
POST/v1/sites/:id/scanQueue a scan now. Returns the scan id, or the running one if there already is one.trigger
GET/v1/scansScans across the workspace, newest first. ?siteId= narrows it.history
GET/v1/scans/:idScore, per-page results, failing rules and the diff against the previous scan.detail
GET/v1/scans/:id/issuesIndividual findings with selectors and markup. ?impact=critical&ruleId=color-contrastpaged
GET/v1/organization/usageWhere you are against your plan.limits

Failing a build on a regression

Two ways. The simplest needs no API key at all — run the scanner directly in CI against a preview deployment:

npx @a11ysignal/scan https://preview.example.com \
  --max-pages 25 --fail-on serious

The other triggers a hosted scan after deploy and waits for the verdict, so the result lands in your dashboard history too:

SCAN=$(curl -sX POST https://api.a11ysignal.com/v1/sites/$SITE_ID/scan \
  -H "X-Api-Key: $A11YSIGNAL_KEY" | jq -r .id)

# Poll until it finishes, then read the verdict.
until [ "$(curl -s https://api.a11ysignal.com/v1/scans/$SCAN \
  -H "X-Api-Key: $A11YSIGNAL_KEY" | jq -r .status)" != "running" ]; do sleep 5; done

curl -s https://api.a11ysignal.com/v1/scans/$SCAN -H "X-Api-Key: $A11YSIGNAL_KEY" \
  | jq -e '.counts.critical == 0 and .counts.serious == 0'

Shapes you will get back

A scan detail, trimmed to the fields most people use:

{
  "id": "cm...",
  "status": "completed",
  "startUrl": "https://example.com/",
  "score": 87.4,
  "counts": { "critical": 0, "serious": 9, "moderate": 2, "minor": 0 },
  "pagesScanned": 24,
  "pagesFailed": 0,
  "axeVersion": "4.13.0",
  "diff": {
    "newIssues":   [{ "url": "…/checkout", "ruleId": "label", "impact": "critical", "count": 2 }],
    "fixedIssues": [{ "url": "…/",         "ruleId": "image-alt", "impact": "critical", "count": 5 }],
    "scoreDelta": -3.5
  },
  "ruleSummary": [
    { "ruleId": "color-contrast", "impact": "serious", "elements": 9, "pages": 4 }
  ]
}

score is null when a crawl read no pages at all — a start URL that 404s, or a robots.txt that disallows everything. Treat that as a failure, not a pass; the scan itself is marked failed with a reason.

Limits and errors

429rate limitPer key. On-demand scans are additionally capped per day by plan.120/min
403planThe API needs Agency or above; the body says which feature is missing.feature
400validationBody validation failures come back as a field-to-messages map.fieldErrors
409conflictThe URL is already monitored in this workspace.duplicate