CI/CD Integration
Add accessibility checks to your deployment pipeline. AllyProof can block or warn on pull requests that introduce WCAG violations, catching regressions before they reach production.
Generate an API Key
Go to Settings > API Keys and click Create Key. Give it a descriptive name (e.g. github-actions-ci) and copy the key immediately — it is shown only once.
Store the key as a secret in your CI environment:
- GitHub Actions:
Settings > Secrets > ALLYPROOF_API_KEY - GitLab CI:
Settings > CI/CD > Variables > ALLYPROOF_API_KEY
The Scan Endpoint
Trigger a scan by sending a POST request to the scan API. The site must already be added and verified in your AllyProof dashboard.
POST https://allyproof.com/api/v1/scan
x-api-key: <API_KEY>
Content-Type: application/json
{
"url": "https://example.com",
"threshold": 85,
"wait": true,
"max_pages": 20,
"source": "github_actions"
}
Request parameters
| Field | Type | Required | Description |
|---|---|---|---|
url | string (URL) | Yes | The verified site URL to scan. |
threshold | number (0-100) | No | Minimum score to pass. Default: 0 (always passes). |
wait | boolean | No | If true, holds the connection until the scan completes (up to 270 s, then returns a poll_url). Only suitable for small sites — prefer async + polling. Default: false. |
max_pages | number (1-100) | No | Maximum pages to scan. If omitted, scans all discovered pages up to the site's configured limit. |
source | string | No | Which system triggered the scan. Shown in the Source column of the site's scan history. Defaults to api. |
Naming the source
Your scan history lists every scan a site has had, from every entry point — the dashboard, scheduled runs, the CLI, the MCP tools, and your pipelines. source is what tells a release gate apart from someone clicking Scan Now three weeks later.
Set it to one of github_actions, gitlab_ci, jenkins, circleci, bitbucket, azure_pipelines, other_ci. Common spellings such as github or gitlab are accepted and normalised; anything unrecognised falls back to api rather than failing the request, so a typo in a workflow file can never break a build.
Why you have to say it. The examples below run curl inside your runner, and curl identifies itself as curl — the runner's identity never reaches us. We would rather ask than guess and label your GitLab pipeline as GitHub. The CLI is the exception: it reads GITHUB_ACTIONS, GITLAB_CI, JENKINS_URL and friends from the environment and declares the right source on its own, so allyproof scan in a pipeline needs no configuration.
Note: The API key is passed via the x-api-key header, not the Authorization header.
Response Formats
Synchronous (wait: true)
When wait is true, the API holds the connection open until the scan completes and returns results directly. Recommended only for small sites: a long-held HTTP connection can be cut by proxies and corporate gateways before the scan finishes. For CI pipelines, use the asynchronous pattern below — trigger, then poll with short requests.
{
"pass": true,
"score": 92,
"threshold": 85,
"scan_id": "scan-uuid",
"site": { "name": "My Site", "url": "https://example.com" },
"summary": {
"pages_scanned": 12,
"pages_failed": 0,
"total_violations": 3,
"critical": 0,
"serious": 1,
"moderate": 2,
"minor": 0
},
"duration_ms": 14200,
"top_violations": [
{
"rule_id": "color-contrast",
"impact": "serious",
"description": "Elements must meet minimum color contrast ratio thresholds",
"pages_affected": 3
}
],
"pages": [
{ "url": "example.com/", "violations": 1, "critical": 0 },
{ "url": "example.com/about", "violations": 2, "critical": 0 }
],
"dashboard_url": "https://allyproof.com/sites/site-uuid"
}
Asynchronous (default)
Without wait: true, the API returns immediately with a scan ID and a poll URL. The response has HTTP status 202 Accepted.
{
"scan_id": "scan-uuid",
"status": "pending",
"poll_url": "/api/v1/scan?id=scan-uuid",
"message": "Scan started. Poll the poll_url for results, or use wait=true to block."
}
To poll for results, hit the URL the response gave you with the same API key:
GET https://allyproof.com/api/v1/scan?id=scan-uuid
x-api-key: <API_KEY>
Threshold Configuration
The threshold parameter sets the minimum acceptable accessibility score. When the scan completes:
- If the score is at or above the threshold, the response includes
"pass": true - If the score is below the threshold, the response includes
"pass": false
Use this in your CI script to decide whether to pass or fail the pipeline step. A threshold of 85 is a reasonable starting point for most sites. Raise it gradually as you fix existing violations.
GitHub Actions Example
Save this as .github/workflows/accessibility.yml:
name: Accessibility Check
on:
pull_request:
branches: [main]
jobs:
a11y-scan:
runs-on: ubuntu-latest
steps:
- name: Run AllyProof scan
id: scan
run: |
RESPONSE=$(curl -s -X POST \
https://allyproof.com/api/v1/scan \
-H "x-api-key: ${{ secrets.ALLYPROOF_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{
"url": "${{ vars.SITE_URL }}",
"wait": true,
"threshold": 85,
"source": "github_actions"
}')
SCORE=$(echo "$RESPONSE" | jq -r '.score')
PASS=$(echo "$RESPONSE" | jq -r '.pass')
VIOLATIONS=$(echo "$RESPONSE" | jq -r '.summary.total_violations')
URL=$(echo "$RESPONSE" | jq -r '.dashboard_url')
echo "score=$SCORE" >> "$GITHUB_OUTPUT"
echo "pass=$PASS" >> "$GITHUB_OUTPUT"
echo "violations=$VIOLATIONS" >> "$GITHUB_OUTPUT"
echo "url=$URL" >> "$GITHUB_OUTPUT"
- name: Comment on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const score = '${{ steps.scan.outputs.score }}';
const violations = '${{ steps.scan.outputs.violations }}';
const url = '${{ steps.scan.outputs.url }}';
const passed = '${{ steps.scan.outputs.pass }}' === 'true';
const emoji = passed ? '✅' : '❌';
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `${emoji} **AllyProof Scan**\n\nScore: **${score}/100** | Violations: **${violations}**\n\n[View full report](${url})`
});
- name: Fail if below threshold
if: steps.scan.outputs.pass == 'false'
run: |
echo "Accessibility score ${{ steps.scan.outputs.score }} is below threshold"
exit 1
GitLab CI Example
Save this as .gitlab-ci.yml:
accessibility:
stage: test
image: alpine:latest
before_script:
- apk add --no-cache curl jq
script:
- |
RESPONSE=$(curl -s -X POST \
https://allyproof.com/api/v1/scan \
-H "x-api-key: $ALLYPROOF_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"url\": \"$SITE_URL\",
\"wait\": true,
\"threshold\": 85,
\"source\": \"gitlab_ci\"
}")
SCORE=$(echo "$RESPONSE" | jq -r '.score')
PASS=$(echo "$RESPONSE" | jq -r '.pass')
echo "Accessibility Score: $SCORE/100"
if [ "$PASS" = "false" ]; then
echo "Score below threshold — failing pipeline"
exit 1
fi
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
Best Practices
- Run scans on pull requests, not just on merge to main — catch regressions early.
- Start with a low threshold (e.g.
70) and raise it gradually as you fix existing violations. - Use
max_pagesto limit scan scope for faster CI runs. - Set up email notifications as a backup so stakeholders see results even if they don't check CI.
- Use the
dashboard_urlin PR comments so developers can jump straight to the full report. - Set
sourceso the scan history says which pipeline ran each scan — without it every automated scan reads as a generic API call.
Limitations
- Automated scanning detects common WCAG 2.2 AA failure patterns, not every failure, and a green build is not a conformance result. Criteria that require human judgment (e.g., meaningful alt text, logical reading order) are not tested at all.
- Scans with
wait: truereturn apoll_urlafter 270 seconds if the scan is still running. For anything but small sites, use async mode and poll. - The site must be verified in your AllyProof dashboard before the API can scan it.