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
| 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": "..." }
| 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.