DIRECTIVE (EU) 2019/882 · IN FORCE SINCE 28 JUNE 2025 EU-HOSTED · GDPR-CLEAN
EAA Compliance

DOCUMENTATION · API v1

Pull your evidence into your own systems.

Everything the portal shows is available over HTTP: coverage state, open root causes, the append-only ledger and the generated documents. Read-only by default, JSON everywhere, and stable — v1 will not change shape under you.

https://eaacompliance.org/api/v1 Bearer auth JSON only Included from Scale

Authentication

Keys are organization-scoped, hashed at rest, shown once, and revocable. Send yours as a bearer token on every request. Sign in, then request a key from support.

curl
curl https://eaacompliance.org/api/v1/domains \
  -H "Authorization: Bearer eaa_live_9f3c41a8b2e04d7f"
Keys read; they don’t sign.An API key can read your evidence and trigger scans within your plan’s capacity. It cannot sign off a criterion, edit the ledger, or change your plan — human judgement and money both stay in the portal, behind a person.

Conventions

Identifiers

Public ids are prefixed and stable: scn_ scans, jrn_ journeys, fg_ finding groups. Never parse meaning out of them.

Time

All timestamps are UTC, ISO 8601, second precision. Ranges are inclusive of from, exclusive of to.

Pagination

Keyset, not offset: pass before with the last id you saw. Stable while new entries arrive.

Versioning

Breaking changes ship as /api/v2. Additive fields can appear in v1 at any time — ignore what you don’t know.

Errors & limits

Errors are JSON with a stable error code and a human message. The code is for your switch statement; the message is for your logs.

401 Unauthorized
{
  "ok": false,
  "error": "invalid_key",
  "message": "That API key is not valid or has been revoked."
}
StatusCodeMeaning
400bad_requestA parameter is missing or malformed.
401invalid_keyMissing, unknown or revoked key.
403plan_requiredThe API is included from the Scale plan.
404not_foundNo such object in your organization.
409capacity_reachedA scan was requested beyond this period’s capacity.
429rate_limited60 requests/minute. Retry after the header says so.
Capacity is honest here too.A 409 tells you the period is spent and names the reset date. Monitoring itself keeps running at reduced frequency — the API never pretends a scan happened.

Domains

GET/api/v1/domainsread

Every domain in your organization with its verification state, schedule and coverage figure.

200 OK
{
  "ok": true,
  "domains": [
    {
      "id": 12,
      "host": "beispiel-commerce.de",
      "status": "verified",
      "schedule": "daily",
      "coverage": { "covered": 46, "total": 50, "failing": 4 },
      "last_scan": "scn_k21mmd0q7x",
      "verified_at": "2026-08-16T23:00:11Z"
    }
  ]
}

Scans

POST/api/v1/scanswrite · capacity

Trigger a scan — the per-deploy hook. Counts against this period’s capacity like any other scan.

ParameterTypeNotes
domain_idinteger REQUIREDA verified domain in your organization.
reasonstring OPTIONALFree text stored on the ledger entry, e.g. a release tag.
curl
curl -X POST https://eaacompliance.org/api/v1/scans \
  -H "Authorization: Bearer $EAA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain_id": 12, "reason": "release v4.18.2"}'
GET/api/v1/scans/{scan_id}read

Status and summary for one scan. Poll this, or subscribe to scan.completed and stop polling.

Findings

GET/api/v1/findingsread

Root causes, not raw instances — ranked by severity and legal weight, the same order the portal shows.

ParameterTypeNotes
domain_idinteger OPTIONALRestrict to one domain.
statusstring OPTIONALopen · verifying · resolved
200 OK
{
  "ok": true,
  "summary": { "raw_instances": 371, "root_causes": 4 },
  "findings": [
    {
      "id": "fg_8241",
      "rule": "image-alt",
      "title": "Images without a text alternative",
      "criteria": ["1.1.1"],
      "impact": "critical",
      "legal_weight": "high",
      "instances": 312,
      "status": "open",
      "sources": ["crawler", "journey"],
      "reference": "https://eaacompliance.org/rules/image-alt"
    }
  ]
}
POST/api/v1/findings/{id}/fixedwrite

Mark a root cause fixed from your pipeline. It moves to verifying — and only a scan can move it to resolved.

The API cannot declare compliance.There is deliberately no endpoint that resolves a finding, signs off a criterion, or sets a statement to “fully compliant”. Those states are earned by evidence, not asserted by a client.

Coverage

GET/api/v1/coverage/{domain_id}read

All 50 criteria with state, method and evidence — the matrix as data. Ideal for a compliance dashboard of your own.

200 OK (excerpt)
{
  "covered": 46, "total": 50,
  "criteria": [
    { "ref": "1.4.3", "level": "AA", "method": "automated",
      "state": "fail", "root_causes": 1, "instances": 41 },
    { "ref": "3.2.3", "level": "AA", "method": "human",
      "state": "verified", "by": "[email protected]",
      "at": "2026-08-14T09:12:00Z" }
  ]
}

Ledger

GET/api/v1/ledgerread

The append-only record, newest first, keyset-paginated. Filter by domain_id and scope (verification, scan, coverage, system). Entries are never edited or deleted, so mirroring it into your own store is safe.

Exports

GET/api/v1/exports/{domain_id}/statementread

The accessibility statement as structured data — computed status, barriers with remediation state, method and enforcement contact — so you can render it inside your own site template and keep it current automatically.

Status is computed, not supplied.compliance_status is one of full, partial, none, derived from the matrix. There is no parameter to override it.

Webhooks

Point a URL at your systems and stop polling. Deliveries retry with backoff for 24 hours; each carries an event id you can use to de-duplicate.

EventFires when
scan.completedA scan finishes — includes the summary and the new failing-criteria count.
finding.openedA new root cause appears, or a resolved one reappears.
finding.resolvedA scan verifies zero remaining instances.
coverage.changedA criterion changes state, including human sign-offs.
verification.warningA domain’s TXT record went missing — the 72-hour grace window opened.
capacity.reachedThe period’s capacity is spent and frequency has stepped down.

Verifying signatures

Every delivery carries EAA-Signature: t=<unix>,v1=<hex> — an HMAC-SHA256 over "{t}.{raw body}" with your endpoint secret. Compare in constant time and reject anything older than five minutes.

PHP
$expected = hash_hmac('sha256', $t . '.' . $payload, $secret);
if (!hash_equals($expected, $v1) || abs(time() - $t) > 300) {
    http_response_code(400); exit;   // reject silently, log loudly
}

Recipe · block a deploy on a regression

Scan on release, wait for the event, fail the pipeline if a critical root cause appeared. Capacity-aware: a 409 means the period is spent, not that the site is clean.

deploy.sh
# trigger, then let the webhook do the waiting
SCAN=$(curl -sf -X POST "$EAA/api/v1/scans" \
  -H "Authorization: Bearer $EAA_KEY" \
  -d "{\"domain_id\": 12, \"reason\": \"$GIT_TAG\"}" | jq -r .scan.id)

# or poll, if your pipeline prefers it
until [ "$(curl -sf -H "Authorization: Bearer $EAA_KEY" \
  "$EAA/api/v1/scans/$SCAN" | jq -r .scan.status)" = "done" ]; do sleep 20; done

Recipe · publish your statement

Fetch the statement as data on a schedule and render it into your own footer template. It updates itself as remediation is verified — no one has to remember to edit a page after a fix lands.

Changelog

DateChange
2026-08-17v1 published: domains, scans, findings, coverage, ledger, exports, webhooks.

Additive fields are announced here and never break existing clients. Removals only ever happen in a new major version.