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
- 01
Create a key
In your dashboard's API & CI tab. Save it as a secret in your CI, for example EQUALVIA_API_KEY.
- 02
Add one step
After your preview deployment, run our script with up to 10 page URLs. No dependencies, Node 18 or newer.
- 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 seriousVercel, 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 seriousAny other CI (Bitbucket, CircleCI, Jenkins, Azure DevOps) works the same way: download the script and run it with Node.
Script options
| Option | What 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. |
--json | Print the raw result as JSON instead of the summary. |
Exit codes
| Code | Meaning |
|---|---|
0 | Passed. |
1 | Failed: issues at or above the threshold, or a page that couldn't be checked. |
2 | The 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_IDGood 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
| Method | Path | Returns |
|---|---|---|
POST | /api/v1/checks | Starts 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/checks | Your 20 most recent checks, without page details. |
GET | /api/v1/sites | Your monitored sites with their latest scan's issue counts. |
GET | /api/v1/sites/{id}/issues | Every 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/usage | Pages used this month, your limit and when it resets. |
Starting a check
The body of POST /api/v1/checks:
| Field | Meaning |
|---|---|
urls | Required. 1 to 10 absolute http(s) URLs. |
failOn | critical, serious (default), moderate, minor or none. |
baselineSiteId | Optional. One of your monitored sites: only issues it doesn't have count. |
label | Optional. Up to 120 characters, e.g. the branch and commit. |
locale | Optional. 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.
| HTTP | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | The body or a URL isn't valid. |
401 | UNAUTHORIZED | Missing, unknown or revoked key. |
403 | PLAN_REQUIRED | The account's plan doesn't include API access. |
403 | QUOTA_EXCEEDED | The monthly page allowance would be exceeded. |
404 | NOT_FOUND / BASELINE_NOT_FOUND | No such check or site in your account. |
409 | BUSY | 2 checks are already running. |
429 | RATE_LIMITED | Too 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