The adasafe API

Trigger and read accessibility scans from your own tools, CI, or an AI agent. Server-to-server, REST, JSON. Base URL https://api.adasafe.ai — every path below already includes the /v1 prefix.

There is no score

Every integrator asks for one number. We don’t return it — because there isn’t an honest one to give. WCAG conformance is pass-or-fail per criterion, so a “92% accessible” score would be invented, not measured.

Instead, responses carry counts per severity, a categorical risk_level (High / Medium / Low), and a coverage block that says how many in-scope pages were not audited. You always know what was checked and what wasn’t. No score, percent, or grade field exists — by design.

Authentication

Create a key on your API keys page (paid plans only). Send it as a bearer token. Keys are server-side only — an API key in browser JavaScript is a leaked key, and the API rejects browser requests.

curl https://api.adasafe.ai/v1/me \
  -H "Authorization: Bearer ada_live_your_key_here"

Triggering a scan

POST /v1/scans needs two things: ownership_confirmed: true (you attest you own or may scan the site) and an Idempotency-Key header. The key makes retries safe — the same key returns the same scan and never double-charges your quota.

curl -X POST https://api.adasafe.ai/v1/scans \
  -H "Authorization: Bearer ada_live_..." \
  -H "Idempotency-Key: 3f9c1e7a-..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "ownership_confirmed": true}'

# → 202 { "scan_id": "…", "status": "pending" }

Poll GET /v1/scans/{id} for status (cheap), then fetch GET /v1/scans/{id}/results once it’s complete. Add ?format=md for a Markdown report ready to paste into an issue or hand to an AI assistant.

Endpoints

  • GET/v1/meYour plan, remaining scan quota, scopes, and rate limits.
  • POST/v1/scansTrigger a scan. Requires ownership_confirmed and an Idempotency-Key.
  • GET/v1/scansList your scans, newest first (cursor pagination).
  • GET/v1/scans/{id}Scan status, counts, coverage, and risk level — cheap to poll.
  • GET/v1/scans/{id}/resultsFull violations with fixes. Add ?format=md for Markdown.
  • POST/v1/scans/batchScan several pages as one job (one unit of quota).
  • POST/v1/discoverList in-scope pages for a site before scanning.
  • GET/v1/sitesList your monitored sites.
  • POST/v1/sitesAdd a monitored site. Requires ownership_confirmed.
  • PATCH/v1/sites/{id}Update a monitored site (label, interval, active).
  • DELETE/v1/sites/{id}Stop monitoring a site.
  • POST/v1/sites/{id}/scanScan a monitored site now.

Quotas & limits

API scans draw from the same monthly pool as the dashboard — one shared allowance, not a separate bucket. Out of scans returns 402 (upgrade to continue); going too fast returns 429 with a Retry-After header (slow down). Different situations, different fixes.

About the fixes

Each violation may carry a suggested fix. It always states its source and whether it was verified by a re-scan. An AI-suggested fix that hasn’t been validated is labelled as such — it never reads as confirmed. Apply anything with judgement; the scan is a tool, not a certification.

Webhooks

Prefer push over polling? Add a webhook on your Notifications page and adasafe POSTs a signed JSON payload the moment a scan or batch finishes — the same honest shape as the API: counts, coverage, and a categorical risk level, never a score and never page content.

{
  "event": "scan.completed",
  "occurred_at": "2026-08-26T10:15:00+00:00",
  "data": {
    "type": "scan",
    "id": "sc_…",
    "url": "https://example.com/",
    "status": "complete",
    "risk_level": "High",
    "counts": { "critical": 1, "serious": 3, "moderate": 5, "minor": 2, "total": 11 },
    "coverage": { "pages_scanned": 1, "failed_pages": 0 },
    "delta": { "new_issue_types": 2, "resolved": 1, "unchanged": 8, "first_scan": false },
    "dashboard_url": "https://adasafe.ai/dashboard?scan_id=sc_…"
  }
}

Every delivery carries these headers:

X-Adasafe-Event:     scan.completed
X-Adasafe-Delivery:  d3f1…             # stable across retries — dedupe on this
X-Adasafe-Signature: ts=1756202100;h1=<hmac-sha256 hex>
Content-Type:        application/json
User-Agent:          adasafe-webhooks/1

Verifying the signature

Each webhook has its own signing secret, shown once when you create it. The X-Adasafe-Signature header is ts=<unix>;h1=<hmac>. Recompute the HMAC-SHA256 of the raw request body prefixed with the timestamp, compare in constant time, and reject anything whose timestamp is more than five minutes old.

import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes) -> bool:
    ts, _, h1 = header.partition(";")
    ts, h1 = ts.removeprefix("ts="), h1.removeprefix("h1=")
    if abs(time.time() - int(ts)) > 300:            # 5-minute replay window
        return False
    expected = hmac.new(secret.encode(), f"{ts}:".encode() + body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, h1)

Events

Choose which events each webhook subscribes to. The X-Adasafe-Event header and the payload’s event field carry the name:

scan.completed        a single-page scan finished
batch.completed       a whole-site / batch scan finished
monitoring.completed  a scheduled monitoring run finished
scan.failed           a scan errored or was blocked (counts = null)

A non-2xx response or a timeout is retried with backoff for up to 24 hours. After 20 consecutive failures the webhook auto-disables and we email the owner. Retries re-send the identical body, so treat X-Adasafe-Delivery as the idempotency key.