Add an accessibility gate to your pipeline. Submit a scan, wait for the result, and fail the build if thresholds are not met.
Use the SSE endpoint GET /v1/ci/scans/{scan_id}/events to open a single connection that streams progress updates and closes automatically with the final result. This is more efficient than polling and works well for scans of any size.
The stream sends keepalive comments every 15 seconds (lines starting with :) to keep proxies alive. Filter them out with grep '^data:'. The last data: line is always the terminal event containing score, violation counts, and pass/fail verdict.
jobs:
accessibility:
runs-on: ubuntu-latest
steps:
- name: Submit scan
id: scan
run: |
RESPONSE=$(curl -sf -X POST https://api.a11yrisk.eu/v1/ci/scans \
-H "X-API-Key: ${{ secrets.A11YRISK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-staging-url.com",
"max_pages": 10,
"min_score": 80,
"fail_on_severity": "critical"
}')
echo "scan_id=$(echo $RESPONSE | jq -r .scan_id)" >> $GITHUB_OUTPUT
- name: Wait for result (SSE)
run: |
SCAN_ID=${{ steps.scan.outputs.scan_id }}
# curl blocks until the server closes the stream (terminal event reached).
# grep strips keepalive lines; tail -1 gives the terminal event.
TERMINAL=$(curl -sfN \
-H "X-API-Key: ${{ secrets.A11YRISK_API_KEY }}" \
"https://api.a11yrisk.eu/v1/ci/scans/$SCAN_ID/events" \
| grep "^data:" | tail -1)
DATA="${TERMINAL#data: }"
EVENT=$(echo "$DATA" | jq -r '.event')
PASSED=$(echo "$DATA" | jq -r '.passed // "null"')
case "$EVENT:$PASSED" in
completed:true|completed:null)
echo "Scan passed. Score: $(echo "$DATA" | jq '.score')"
;;
completed:*)
echo "Scan failed: $(echo "$DATA" | jq -r '.message')"
exit 1
;;
*)
echo "Scan did not complete (event: $EVENT)"
exit 1
;;
esacaccessibility:
stage: test
image: alpine
before_script:
- apk add --no-cache curl jq bash
script:
- |
bash -c '
SCAN_ID=$(curl -sf -X POST https://api.a11yrisk.eu/v1/ci/scans \
-H "X-API-Key: $A11YRISK_API_KEY" \
-H "Content-Type: application/json" \
-d "{"url":"https://your-staging-url.com","max_pages":10,"min_score":80}" \
| jq -r .scan_id)
TERMINAL=$(curl -sfN \
-H "X-API-Key: $A11YRISK_API_KEY" \
"https://api.a11yrisk.eu/v1/ci/scans/$SCAN_ID/events" \
| grep "^data:" | tail -1)
DATA="${TERMINAL#data: }"
EVENT=$(echo "$DATA" | jq -r ".event")
PASSED=$(echo "$DATA" | jq -r ".passed // "null"")
case "$EVENT:$PASSED" in
completed:true|completed:null) echo "Passed. Score: $(echo "$DATA" | jq .score)" ;;
completed:*) echo "Failed: $(echo "$DATA" | jq -r .message)"; exit 1 ;;
*) echo "Did not complete (event: $EVENT)"; exit 1 ;;
esac
'Poll GET /v1/ci/scans/{scan_id} every 5–10 seconds. Returns 202 while in progress, 200 on pass, 422 on fail. Use this as a fallback if your environment blocks long-lived HTTP connections.
- name: Poll for result
run: |
SCAN_ID=${{ steps.scan.outputs.scan_id }}
for i in $(seq 1 60); do
RESULT=$(curl -sf https://api.a11yrisk.eu/v1/ci/scans/$SCAN_ID \
-H "X-API-Key: ${{ secrets.A11YRISK_API_KEY }}" \
-w "\n%{http_code}")
HTTP_CODE=$(echo "$RESULT" | tail -1)
BODY=$(echo "$RESULT" | head -1)
if [ "$HTTP_CODE" = "200" ]; then
echo "Passed. Score: $(echo $BODY | jq .score)"
exit 0
elif [ "$HTTP_CODE" = "422" ]; then
echo "Failed: $(echo $BODY | jq -r .message)"
exit 1
fi
echo "Still running (attempt $i/60)…"
sleep 10
done
echo "Timed out"
exit 1For large sites (100+ pages) or nightly audits, blocking your pipeline for 30–60 minutes is impractical. Instead, decouple submission from result checking: submit the scan early in the pipeline, then check the result in a later step after other work has run.
This works because the scan continues running in the background regardless of whether your pipeline is connected. The scan_id is stable — you can check it minutes or hours later.
jobs:
# Job 1: Submit scan at the start of your workflow, alongside other work.
submit-a11y-scan:
runs-on: ubuntu-latest
outputs:
scan_id: ${{ steps.submit.outputs.scan_id }}
steps:
- name: Submit scan
id: submit
run: |
SCAN_ID=$(curl -sf -X POST https://api.a11yrisk.eu/v1/ci/scans \
-H "X-API-Key: ${{ secrets.A11YRISK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-staging-url.com","max_pages":200,"min_score":75}' \
| jq -r .scan_id)
echo "scan_id=$SCAN_ID" >> $GITHUB_OUTPUT
# ... other jobs run in parallel (build, unit tests, deploy to staging, etc.)
# Job 2: Gate on the a11y result. Runs after other jobs, giving the scan time to finish.
check-a11y-result:
runs-on: ubuntu-latest
needs: [submit-a11y-scan, deploy-staging] # waits for deploy before checking
steps:
- name: Wait for scan result (SSE)
run: |
SCAN_ID=${{ needs.submit-a11y-scan.outputs.scan_id }}
TERMINAL=$(curl -sfN \
-H "X-API-Key: ${{ secrets.A11YRISK_API_KEY }}" \
"https://api.a11yrisk.eu/v1/ci/scans/$SCAN_ID/events" \
| grep "^data:" | tail -1)
DATA="${TERMINAL#data: }"
PASSED=$(echo "$DATA" | jq -r '.passed // "null"')
[ "$PASSED" = "false" ] && echo "Failed: $(echo "$DATA" | jq -r .message)" && exit 1
echo "Passed. Score: $(echo "$DATA" | jq .score)"By the time check-a11y-result starts, the scan has likely already finished (it ran while your deploy was happening). The SSE connection returns immediately if the scan is already in a terminal state.
All thresholds are optional. Omit them to run a scan without a pass/fail verdict (passed will be null).
| Parameter | Type | Effect |
|---|---|---|
min_score | 0–100 | Fail if overall accessibility score is below this value. |
max_violations | integer | Fail if total violation count exceeds this number. |
fail_on_severity | string | Fail if any violation exists at this severity or higher. Values: critical, serious, moderate, minor. |
fail_on_truncation | boolean | Default true. Return 402 if your credit balance can't cover max_pages. Set false to run a partial scan instead. |
Each scan reserves one credit per page requested (max_pages). Plan accordingly:
GET /v1/billing/credits before large runs.