AccessLedger

API reference

The JSON API

Ordinary HTTP and JSON. Five endpoints, one bearer token, no client library to install. Everything the web interface shows you about scans and issues is available here.

https://theaccessledger.com/api/v1

Authentication

Sending your token

Every endpoint except /health needs a token from Account, then API tokens, sent as a bearer token.

Authorization: Bearer al_your_token_here

Tokens have scopes. read lists sites and reads results. scan additionally starts scans. A token without the scope an endpoint needs gets insufficient_scope rather than a vague refusal, and the message names the scope it was missing.

Requests are served over HTTPS only. A token sent over plain HTTP should be treated as disclosed and revoked.

Limits

Rate limits and quota

Two separate things, and they refuse you for different reasons. Rate limits are per token and per minute, and exist so one runaway loop cannot affect anyone else. The monthly scan quota comes from your plan and is what actually rations scanning.

A rate limit returns 429 with retry_after_seconds. Wait that long and retry. A spent quota returns 402 and waiting will not help until it resets, so the response says when that is.

Every response that touches your quota carries the current figures, so a client never has to track them itself.

Endpoints

All five, in full

Every endpoint, the scope it needs and its rate limit.
Endpoint Scope Limit What it does
GET /health none 60 per minute Check that the API process is answering.
POST /scans scan 10 per minute Start a scan of one page, or of a verified site.
GET /scans/{id} read 240 per minute Read a scan, and its findings once it has finished.
GET /sites read 120 per minute List the sites on your account.
GET /sites/{id}/issues read 120 per minute The open issues on a site, as the ledger currently holds them.

GET /api/v1/health

Check that the API process is answering.

Touches no database and needs no token, so it answers even while something else is broken. That is the point of it: it separates "the API is down" from "the API is up and your request was refused".

For whether scanning itself is working, read /status.json instead. This endpoint deliberately cannot tell you that.

Response

{
  "ok": true,
  "service": "accessledger",
  "api_version": "v1",
  "time": "2026-07-31T14:02:11.000Z"
}

POST /api/v1/scans

Start a scan of one page, or of a verified site.

Send either url or site_id, never both. A url scans that single page and needs no prior setup. A site_id scans every page configured for a site you have already verified you control.

Returns immediately with a scan id and a status of queued. Scanning is a real browser visiting real pages, so poll GET /scans/{id} rather than waiting on this call.

Counts against your monthly quota, which is returned on every response so you never have to guess how much is left.

Request body

{
  "url": "https://example.com/pricing"
}

Response

{
  "id": "0d0b1f3c-8a24-4e2f-9b77-1c6f8f2a5e10",
  "status": "queued",
  "url": "https://example.com/pricing",
  "quota": { "plan": "business", "limit": 250, "used": 31, "remaining": 219 }
}

GET /api/v1/scans/{id}

Read a scan, and its findings once it has finished.

status moves through queued, running, and then either complete or failed. Poll this until it settles.

A status of failed means the scan could not be completed, which is a problem at our end. It is not a finding about the page. Treat the two differently in a build pipeline: one should alert you, the other should fail the build.

issues is capped at 200 entries. issues_truncated tells you when there were more, in which case read the full record at report_url.

Response

{
  "id": "0d0b1f3c-8a24-4e2f-9b77-1c6f8f2a5e10",
  "status": "complete",
  "pages_scanned": 12,
  "counts": { "critical": 0, "serious": 3, "moderate": 7, "minor": 2 },
  "issues": [
    {
      "rule_id": "color-contrast",
      "impact": "serious",
      "help": "Elements must meet minimum color contrast ratio thresholds",
      "wcag": ["wcag2aa", "wcag143"],
      "page_url": "https://example.com/pricing"
    }
  ],
  "issues_truncated": false,
  "report_url": "https://theaccessledger.com/sites/2f1c..."
}

GET /api/v1/sites

List the sites on your account.

Includes whether each site is verified, and how. Only a verified site can be scanned by id, so this is the endpoint that tells you why a scan was refused.

Response

{
  "sites": [
    {
      "id": "2f1c9d80-3b6e-4a11-9d5c-77e2a1b40cc9",
      "base_url": "https://example.com",
      "host": "example.com",
      "verified_at": "2026-06-14T09:41:00.000Z",
      "verification_method": "dns_txt"
    }
  ]
}

GET /api/v1/sites/{id}/issues

The open issues on a site, as the ledger currently holds them.

This is the running record rather than one scan: what is open right now, when each issue was first seen, and when it was fixed if it has been.

Filter with impact to narrow to critical, serious, moderate or minor.

An issue is only closed when a later scan actually reached the page and did not find it. A page that could not be loaded leaves its issues open, because a stale open issue is a nuisance and a false "fixed" is a liability.

Response

{
  "issues": [
    {
      "id": "9c1e...",
      "rule_id": "image-alt",
      "impact": "critical",
      "page_url": "https://example.com/about",
      "first_seen_at": "2026-05-02T11:20:00.000Z",
      "fixed_at": null,
      "occurrences": 3
    }
  ]
}

Example

Scan a page and wait for the result

Start the scan:

curl -X POST https://theaccessledger.com/api/v1/scans \
  -H "Authorization: Bearer $ACCESSLEDGER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/pricing"}'

Then poll the id it returns until the status settles:

curl https://theaccessledger.com/api/v1/scans/$SCAN_ID \
  -H "Authorization: Bearer $ACCESSLEDGER_TOKEN"

A scan of a single page usually finishes in a few seconds. A whole site takes as long as it takes to visit every page politely, one at a time, which for 250 pages is several minutes. Poll every few seconds rather than in a tight loop.

Errors

Every error, and what to do about it

Errors are JSON with an error code and a message written for a person. Branch on the code, show the message.

{ "error": "quota_exceeded", "message": "..." }
Error codes, their HTTP status and the fix.
Code Status What it means
unauthorized 401 No token, or a token that does not exist. Check the Authorization header is "Bearer" followed by your al_ token.
token_revoked 401 The token was revoked or has expired. Create a new one in your account.
insufficient_scope 403 The token is valid but was not given this permission. A read-only token cannot start scans.
invalid_request 400 The body was not valid JSON, or the fields do not make sense together, such as sending both url and site_id.
invalid_url 400 The URL is not a public http or https address. Private and internal addresses are refused on purpose.
site_not_verified 403 Scheduled crawling needs proof you control the domain. Verify the site, then retry.
not_found 404 No such scan or site on this account. Ids are scoped to your account, so another account's id reads as missing.
scan_in_progress 409 That site already has a scan running. Wait for it rather than starting a second.
quota_exceeded 402 The monthly scan allowance is used up. The response body says when it resets.
rate_limited 429 Too many requests for this token. retry_after_seconds says how long to wait.
internal_error 500 Our fault. Retry, and if it persists check the status page before assuming your request was wrong.

One thing worth knowing

What a finding is and is not

Automated testing reliably detects somewhere around 30 to 40 percent of WCAG success criteria. It finds the missing alt attribute. It cannot tell you whether the alt text you wrote is useful to anyone.

So a clean API response means the automated checks found nothing on the pages they reached on that date. Build that wording into whatever you put in front of your own users, because the alternative is a dashboard quietly implying something it has not established.

GitHub Action documentation