API & CI/CD
Automate accessibility.
From the pipeline.
Full programmatic control over every part of the audit lifecycle. Trigger scans, configure weekly, monthly, or yearly crawl schedules, retrieve findings, stream events, and generate reports without manual intervention.
Quick Start
Up and running in three steps.
Get a JWT
The API uses the same JWT as the web app — there are no separate API keys. For CI/CD, obtain an access token from your identity provider (Entra or Google) and exchange it via POST /api/auth/exchange-provider-token to get a JWT.
Trigger a page scan
POST to the scan endpoint for the specific page you want to check. Returns 202 Accepted — the scan runs asynchronously.
curl -sf -X POST \
"https://accessibility.api.developingtoday.net/api/orgs/{orgId}/users/{userId}/audit-projects/{projectId}/pages/{pageId}/scan" \
-H "Authorization: Bearer $JWT"Stream events, then check findings
Connect to the project's SSE event stream to know the moment the scan completes. Then fetch findings for that page and fail the build if violations exist.
event: ScanCompleted
data: {"pageId":"...","findingsCount":3,"impact":"critical"}Reference
Endpoints.
All requests require the Authorization: Bearer <token> header. All responses are JSON.
/api/orgs/{orgId}/users/{userId}/audit-projects/{projectId}/pages/{pageId}/scanQueue a scan for a single page. Returns 202 Accepted immediately — the scan runs asynchronously. Listen on the project event stream for completion.
HTTP/1.1 202 Accepted/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/crawlStart a full crawl of the project's configured URL. Discovers pages, scans each one, and fires scan events as it progresses.
HTTP/1.1 202 Accepted/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/crawl/pausePause an active crawl. Resume it with /crawl/continue.
HTTP/1.1 202 Accepted/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/crawl/continueResume a paused crawl.
HTTP/1.1 202 Accepted/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/crawl/stopStop an active crawl entirely.
HTTP/1.1 202 Accepted/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/eventsServer-sent event stream for a project. Connect here to receive real-time scan progress, page completion, and finding events without polling.
Content-Type: text/event-stream
event: ScanCompleted
data: {"pageId":"3f7a...","url":"https://example.gov/","findingsCount":3}
event: ScanFailed
data: {"pageId":"9b2c...","url":"https://example.gov/about","error":"timeout"}CI/CD Integration
Accessibility in every pipeline.
Drop a scan step into any pipeline. Block deployments when violations exceed your threshold. Run on pull request, on merge, or pair pipeline runs with project-level weekly, monthly, or yearly crawl schedules for baseline coverage.
name: Accessibility Gate
on:
push:
branches: [main]
pull_request:
env:
API: https://accessibility.api.developingtoday.net
jobs:
a11y:
name: WCAG check
runs-on: ubuntu-latest
steps:
# ── 1. Get a JWT via your identity provider ──────────────────────────────
# This example uses an Azure Entra service principal (client credentials).
# Swap for Google if your org uses that provider.
- name: Authenticate
id: auth
run: |
PROVIDER_TOKEN=$(curl -sf -X POST \
"https://login.microsoftonline.com/${{ secrets.AZURE_TENANT_ID }}/oauth2/v2.0/token" \
-d "client_id=${{ secrets.AZURE_CLIENT_ID }}" \
-d "client_secret=${{ secrets.AZURE_CLIENT_SECRET }}" \
-d "grant_type=client_credentials" \
-d "scope=api://c6ea719f-9fb0-43ef-9e0c-2e84740e7d77/.default" \
| jq -r '.access_token')
JWT=$(curl -sf -X POST "${{ env.API }}/api/auth/exchange-provider-token" \
-H "Content-Type: application/json" \
-d "{\"token\": \"$PROVIDER_TOKEN\"}" \
| jq -r '.token')
echo "::add-mask::$JWT"
echo "jwt=$JWT" >> $GITHUB_OUTPUT
# ── 2. Trigger a scan on the specific page ───────────────────────────────
- name: Trigger scan
run: |
curl -sf -X POST \
"${{ env.API }}/api/orgs/${{ secrets.A11Y_ORG_ID }}/users/${{ secrets.A11Y_USER_ID }}/audit-projects/${{ secrets.A11Y_PROJECT_ID }}/pages/${{ secrets.A11Y_PAGE_ID }}/scan" \
-H "Authorization: Bearer ${{ steps.auth.outputs.jwt }}"
echo "Scan queued for page ${{ secrets.A11Y_PAGE_ID }}"
# ── 3. Stream SSE events and exit when the scan completes ────────────────
- name: Await completion
run: |
timeout 90 bash -c '
curl -sN \
"${{ env.API }}/api/orgs/${{ secrets.A11Y_ORG_ID }}/users/${{ secrets.A11Y_USER_ID }}/audit-projects/${{ secrets.A11Y_PROJECT_ID }}/events" \
-H "Authorization: Bearer ${{ steps.auth.outputs.jwt }}" \
-H "Accept: text/event-stream" | \
while IFS= read -r line; do
echo "$line"
[[ "$line" == *"ScanCompleted"* || "$line" == *"ScanFailed"* ]] && exit 0
done
'
# ── 4. Fetch findings for the page and fail on violations ────────────────
- name: Check for violations
run: |
FINDINGS=$(curl -sf \
"${{ env.API }}/api/orgs/${{ secrets.A11Y_ORG_ID }}/users/${{ secrets.A11Y_USER_ID }}/audit-projects/${{ secrets.A11Y_PROJECT_ID }}/findings?pageId=${{ secrets.A11Y_PAGE_ID }}&status=Open" \
-H "Authorization: Bearer ${{ steps.auth.outputs.jwt }}")
COUNT=$(echo "$FINDINGS" | jq '[.[] | select(.impact == "critical" or .impact == "serious")] | length')
echo "Critical/serious violations: $COUNT"
if [ "$COUNT" -gt "0" ]; then
echo "::error::$COUNT critical or serious WCAG violation(s) found on this page."
echo "Review: https://accessibility.developingtoday.net"
exit 1
fi
echo "No critical or serious violations. Scan passed."Set A11Y_ORG_ID, A11Y_USER_ID, A11Y_PROJECT_ID, A11Y_PAGE_ID, and ENTRA_CLIENT_SECRET as repository secrets. The Entra client ID and tenant ID can live as plain variables.
Event Stream
Real-time scan events over SSE.
Connect to a project's event stream with a standard GET .../audit-projects/{id}/events request and set Accept: text/event-stream. The server keeps the connection open and pushes named events as scans progress — no polling, no round-trips.
ScanCompletedA page scan finished. Data includes the pageId, URL, and finding count.
ScanFailedA page scan failed — timeout, navigation error, or other fault. Includes the pageId and error detail.
CrawlCompletedA full project crawl finished. Fires after all pages in the crawl have been scanned.
CrawlPausedThe crawl was paused, either by API call or automatically on hitting a page limit.
FindingCreatedA new finding was detected on a page.
FindingResolvedA previously open finding was not detected in a subsequent scan.
# Connect with curl — the connection stays open
curl -sN \
"https://accessibility.api.developingtoday.net/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/events" \
-H "Authorization: Bearer $JWT" \
-H "Accept: text/event-stream"
# Events arrive as named SSE frames:
event: ScanCompleted
data: {"pageId":"f1a7b2c3-...","url":"https://example.gov/","findingsCount":3}
event: ScanCompleted
data: {"pageId":"9b2c4d1e-...","url":"https://example.gov/about","findingsCount":0}
event: ScanFailed
data: {"pageId":"e3d5f7a9-...","url":"https://example.gov/login","error":"navigation timeout"}
event: CrawlCompleted
data: {"projectId":"8d2a1c39-...","pagesScanned":87,"pagesFailed":1}Outbound Webhooks
Push events to your systems.
Subscribe to a long-lived HTTPS endpoint and receive signed POSTs when scans finish, findings change, or other lifecycle events occur. Manage subscriptions from Organization → Settings & Billing → Webhooks.
Signing & verification
Every request includes three headers:
X-AccessibilityAuditor-Signature: t={unix_seconds},v1={hex}— HMAC-SHA256 of{timestamp}.{rawBody}using your subscription secret.X-AccessibilityAuditor-Event-Id— stable idempotency token; identical across all retries of the same delivery.X-AccessibilityAuditor-Event-Type— the dotted event name (e.g.scan.completed).
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=').map((s) => s.trim()))
);
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(parts.v1, 'hex'),
Buffer.from(expected, 'hex'),
);
}using System.Security.Cryptography;
using System.Text;
bool Verify(string rawBody, string header, string secret) {
var parts = header.Split(',')
.Select(kv => kv.Split('=', 2))
.ToDictionary(kv => kv[0].Trim(), kv => kv[1].Trim());
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(hmac.ComputeHash(
Encoding.UTF8.GetBytes($"{parts["t"]}.{rawBody}"))).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(parts["v1"]),
Encoding.UTF8.GetBytes(expected));
}import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(kv.strip().split('=', 1) for kv in header.split(','))
expected = hmac.new(
secret.encode('utf-8'),
f"{parts['t']}.{raw_body.decode('utf-8')}".encode('utf-8'),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(parts['v1'], expected)Event catalog
| Event | Status | Core payload fields |
|---|---|---|
scan.completed | Available | projectId, pageId, targetUrl, findingsCount, severityBreakdown, startedAt, finishedAt |
scan.failed | Available | projectId, pageId, targetUrl, errorReason, attemptedAt |
finding.status_changed | Available | findingId, projectId, wcagCriterion, oldStatus, newStatus, changedBy |
webhook.test | Test only | message, subscriptionId |
regression.detected | Reserved | projectId, newFindingsCount, previousScanAt |
finding.created | Reserved | See finding.status_changed shape |
vpat.generated | Reserved | projectId, downloadUrl, generatedAt |
report.generated | Reserved | projectId, reportType, downloadUrl, generatedAt |
engagement.signed_off | Reserved | projectId, signedOffBy, signedOffAt |
Delivery, retries & idempotency
- POSTs use a 10-second request timeout. Any 2xx response is treated as success.
- Failed deliveries are retried on a fixed back-off: 30 s → 2 m → 10 m → 1 h → 6 h → 24 h (six attempts total). After the last attempt the delivery is marked
FailedTerminal. - After five consecutive failed deliveries on a single subscription it is automatically disabled. Re-enabling it from the admin UI resets the failure counter.
X-AccessibilityAuditor-Event-Idis stable across retries. Use it to deduplicate at-least-once delivery on your side.- The signing timestamp is in seconds since the Unix epoch. Reject deliveries where
|now - t|exceeds your tolerance (5 minutes is a good default) to defeat replay attacks.
What the API covers
Full lifecycle, fully automated.
Projects & Pages
- List, create, and configure projects
- Add pages and manage the page list
- Get project and page detail
- Get crawl queue status
Scans & Crawls
- Trigger single-page scans
- Start, pause, resume, and stop crawls
- Set weekly, monthly, or yearly crawl schedules and blackout windows
- Document upload and scan
- Returns 202 for background scan and crawl work
Findings
- List with status, page, criterion filters
- Create manual findings
- Update status and notes
- Delete findings
Event Stream (SSE)
- Real-time scan events, no polling
- ScanCompleted / ScanFailed per page
- CrawlCompleted / CrawlPaused
- FindingCreated / FindingResolved
Authentication
- Same JWT as the web app
- Exchange provider token for JWT
- Entra ID and Google support
- GET /api/auth/me for orgId + userId
Documents
- Upload PDF, Word, PPT, Excel
- Trigger document scans
- Retrieve document findings
- Delete documents
Authentication
Standard JWT — same as the web app.
There are no separate API keys. The API uses the same JWT-based authentication as the web application. In the browser that JWT comes from signing in with your identity provider. In CI/CD, you get one by exchanging a provider access token via POST /api/auth/exchange-provider-token.
- Supported providers: Microsoft Entra ID, Google
- For CI/CD: use a service principal or service account to obtain a provider token, then exchange it
- Pass the JWT as
Authorization: Bearer <token>on every request GET /api/auth/mereturns theorgIdanduserIdvalues needed to construct resource paths- All endpoints require HTTPS
POST /api/auth/exchange-provider-token
Content-Type: application/json
{
"token": "<provider-access-token>"
}
→ { "token": "eyJhbGciOiJSUzI1NiJ9..." }GET /api/orgs/{orgId}/users/{userId}/audit-projects
Host: accessibility.api.developingtoday.net
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Accept: application/jsonAccess
API access on every plan.
Including free. Sign up, sign in with your identity provider, and the API is immediately accessible — no separate provisioning, no separate credentials. The full API reference is on Scalar.
Need custom rate limits, dedicated infrastructure, or volume contracts? Call +1 605 610 2992 or email enterprise sales.
