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.

POST .../audit-projects/{projectId}/pages/{pageId}/scan
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.

GET .../audit-projects/{projectId}/events — text/event-stream
event: ScanCompleted
data: {"pageId":"...","findingsCount":3,"impact":"critical"}

Reference

Endpoints.

All requests require the Authorization: Bearer <token> header. All responses are JSON.

POST/api/orgs/{orgId}/users/{userId}/audit-projects/{projectId}/pages/{pageId}/scan

Queue a scan for a single page. Returns 202 Accepted immediately — the scan runs asynchronously. Listen on the project event stream for completion.

Response
HTTP/1.1 202 Accepted
POST/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/crawl

Start a full crawl of the project's configured URL. Discovers pages, scans each one, and fires scan events as it progresses.

Response
HTTP/1.1 202 Accepted
POST/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/crawl/pause

Pause an active crawl. Resume it with /crawl/continue.

Response
HTTP/1.1 202 Accepted
POST/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/crawl/continue

Resume a paused crawl.

Response
HTTP/1.1 202 Accepted
POST/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/crawl/stop

Stop an active crawl entirely.

Response
HTTP/1.1 202 Accepted
GET/api/orgs/{orgId}/users/{userId}/audit-projects/{id}/events

Server-sent event stream for a project. Connect here to receive real-time scan progress, page completion, and finding events without polling.

Response
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.

.github/workflows/accessibility.yml
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.

ScanCompleted

A page scan finished. Data includes the pageId, URL, and finding count.

ScanFailed

A page scan failed — timeout, navigation error, or other fault. Includes the pageId and error detail.

CrawlCompleted

A full project crawl finished. Fires after all pages in the crawl have been scanned.

CrawlPaused

The crawl was paused, either by API call or automatically on hitting a page limit.

FindingCreated

A new finding was detected on a page.

FindingResolved

A previously open finding was not detected in a subsequent scan.

Live event stream
# 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).
Node.js verification
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'),
  );
}
.NET verification
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));
}
Python verification
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

EventStatusCore payload fields
scan.completedAvailableprojectId, pageId, targetUrl, findingsCount, severityBreakdown, startedAt, finishedAt
scan.failedAvailableprojectId, pageId, targetUrl, errorReason, attemptedAt
finding.status_changedAvailablefindingId, projectId, wcagCriterion, oldStatus, newStatus, changedBy
webhook.testTest onlymessage, subscriptionId
regression.detectedReservedprojectId, newFindingsCount, previousScanAt
finding.createdReservedSee finding.status_changed shape
vpat.generatedReservedprojectId, downloadUrl, generatedAt
report.generatedReservedprojectId, reportType, downloadUrl, generatedAt
engagement.signed_offReservedprojectId, 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-Id is 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/me returns the orgId and userId values needed to construct resource paths
  • All endpoints require HTTPS
Exchange a provider token for a JWT
POST /api/auth/exchange-provider-token
Content-Type: application/json

{
  "token": "<provider-access-token>"
}

→ { "token": "eyJhbGciOiJSUzI1NiJ9..." }
Every subsequent request
GET /api/orgs/{orgId}/users/{userId}/audit-projects
Host: accessibility.api.developingtoday.net
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Accept: application/json

Access

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.