Developers

Accessibility checks in your CI

Scan the key pages of every preview deployment with the same engine that monitors your live site, and stop a pull request that adds serious accessibility issues before it ships.

Included in the Growth (500 checked pages a month) and Agency (2,000) plans.

How it works

  1. 01

    Create a key

    In your dashboard's API & CI tab. Save it as a secret in your CI, for example EQUALVIA_API_KEY.

  2. 02

    Add one step

    After your preview deployment, run our script with up to 10 page URLs. No dependencies, Node 18 or newer.

  3. 03

    Get a verdict

    The job fails on issues at or above the severity you choose, and lists each one with the element and the WCAG criterion.

Set up your CI

The script (/ci/equalvia-check.mjs) starts a check, waits for the result and prints it. On GitHub Actions it also adds error annotations and a table to the job summary.

GitHub Actions: after every successful preview deployment

# .github/workflows/accessibility.yml
name: Accessibility
on: deployment_status

jobs:
  equalvia:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - name: EqualVia accessibility check
        env:
          EQUALVIA_API_KEY: ${{ secrets.EQUALVIA_API_KEY }}
          URL: ${{ github.event.deployment_status.target_url }}
        run: |
          curl -fsSL https://equalvia.com/ci/equalvia-check.mjs -o equalvia-check.mjs
          node equalvia-check.mjs "$URL/" "$URL/pricing" "$URL/contact" --fail-on serious

Vercel, Netlify and most hosts report preview deployments to GitHub, which is what the deployment_status trigger listens to. Want the script pinned? Commit your own copy to the repository instead of downloading it on every run.

GitLab CI: after the job that deploys a review app

# .gitlab-ci.yml (after the job that deploys the review app)
accessibility:
  stage: test
  image: node:20
  variables:
    EQUALVIA_URLS: "$CI_ENVIRONMENT_URL/ $CI_ENVIRONMENT_URL/pricing"
  script:
    - curl -fsSL https://equalvia.com/ci/equalvia-check.mjs -o equalvia-check.mjs
    - node equalvia-check.mjs --fail-on serious

Any other CI (Bitbucket, CircleCI, Jenkins, Azure DevOps) works the same way: download the script and run it with Node.

Script options

OptionWhat it does
<url> …Pages to check, up to 10. Or set EQUALVIA_URLS (separated by spaces).
--fail-on <level>Lowest severity that fails the job: critical, serious (default), moderate, minor, or none (report only).
--baseline <siteId>Only issues that this monitored site's latest scan doesn't have count. See below.
--label <text>Shown in the dashboard's check list. Default: branch and commit from the CI's variables.
--locale <en|hu>Language of the issue titles. Default: your account's.
--timeout <seconds>How long to wait for the result. Default: 300.
--jsonPrint the raw result as JSON instead of the summary.

Exit codes

CodeMeaning
0Passed.
1Failed: issues at or above the threshold, or a page that couldn't be checked.
2The check couldn't run: missing or revoked key, plan, used-up allowance, network.

Only new issues

A site rarely starts with zero issues, and a CI check that fails on every run gets switched off. With a baseline, a page's issues are compared with the same page of your monitored site's latest scan (matched by path, so a preview URL finds its live twin), and only the ones that aren't there yet count. Rules you marked as ignored on the site are skipped too. The site ids are in the dashboard's API & CI tab or GET /api/v1/sites.

node equalvia-check.mjs "$URL/" "$URL/pricing" --fail-on serious --baseline SITE_ID

Good to know

  • The pages must be reachable from the internet: our servers load them. For a protected preview, use a public staging URL or your host's bypass option (for Vercel, its protection-bypass query parameter; note that the URL is stored with the check).
  • Every URL in a check uses one page of the monthly allowance, whatever the result. The allowance resets at the start of each month (UTC).
  • A page that doesn't load, answers with an error status or redirects to an error page fails the check: a broken deployment should never pass.
  • Checks are separate from monitoring: they don't change your sites' reports, history or email digests.
  • Automated testing finds a large share of accessibility issues, not all of them. Keep the guided manual checks in your release routine.

REST API

The script is a thin wrapper around a small REST API, which you can call from anything: your own scripts, a ticket tracker or an internal dashboard.

Authentication

Send your key in the Authorization header: Authorization: Bearer eqv_… Every response is JSON. Errors look like {"error": {"code": "…", "message": "…"}}.

Endpoints

MethodPathReturns
POST/api/v1/checksStarts a check of 1 to 10 URLs. Answers 202 with the running check and your allowance.
GET/api/v1/checks/{id}One check with every page's issues. Poll it every few seconds until status isn't "running".
GET/api/v1/checksYour 20 most recent checks, without page details.
GET/api/v1/sitesYour monitored sites with their latest scan's issue counts.
GET/api/v1/sites/{id}/issuesEvery issue of a site's latest scan: rule, severity, WCAG and EN 301 549 references, page, element, HTML and suggested fix. ?locale=hu for Hungarian titles.
GET/api/v1/usagePages used this month, your limit and when it resets.

Starting a check

The body of POST /api/v1/checks:

FieldMeaning
urlsRequired. 1 to 10 absolute http(s) URLs.
failOncritical, serious (default), moderate, minor or none.
baselineSiteIdOptional. One of your monitored sites: only issues it doesn't have count.
labelOptional. Up to 120 characters, e.g. the branch and commit.
localeOptional. en or hu, the language of the issue titles.
curl -X POST https://equalvia.com/api/v1/checks \
  -H "Authorization: Bearer $EQUALVIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://preview.example.com/"], "failOn": "serious"}'
curl https://equalvia.com/api/v1/checks/CHECK_ID \
  -H "Authorization: Bearer $EQUALVIA_API_KEY"

The result

passed is null while the check runs. failing counts the elements that decided the result (at or above failOn, and new with a baseline); totals counts everything found. With a baseline, newTotals, each issue's newCount and each element's new flag show what's new.

{
  "check": {
    "id": "cm1x8k2q40001",
    "status": "completed",
    "passed": false,
    "failOn": "serious",
    "baselineSiteId": null,
    "label": "feature/checkout @ 3f6e478",
    "pageCount": 1,
    "pagesWithErrors": 0,
    "totals": { "critical": 1, "serious": 2, "moderate": 0, "minor": 1, "total": 4 },
    "newTotals": null,
    "failing": 3,
    "pages": [
      {
        "url": "https://preview.example.com/",
        "httpStatus": 200,
        "issues": [
          {
            "ruleId": "image-alt",
            "title": "Images without alternative text",
            "impact": "critical",
            "wcag": ["1.1.1"],
            "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/image-alt",
            "count": 1,
            "elements": [{ "target": ".hero > img", "html": "<img src=\"/hero.jpg\">" }]
          }
        ]
      }
    ]
  }
}

Errors

The code field is stable; the message is for people.

HTTPCodeMeaning
400INVALID_REQUESTThe body or a URL isn't valid.
401UNAUTHORIZEDMissing, unknown or revoked key.
403PLAN_REQUIREDThe account's plan doesn't include API access.
403QUOTA_EXCEEDEDThe monthly page allowance would be exceeded.
404NOT_FOUND / BASELINE_NOT_FOUNDNo such check or site in your account.
409BUSY2 checks are already running.
429RATE_LIMITEDToo many requests with the key; see Retry-After.

Limits

  • 10 URLs per check, 2 checks running at once per account
  • 120 requests per minute per key
  • 500 checked pages a month on Growth, 2,000 on Agency
  • Up to 10 keys per account