# ADA Title II Accessibility Checker: WCAG and PDF (`omargnagy/ada-title-ii-scanner`) Actor

Returns one row per page with its WCAG 2.1 AA violations (axe-core, desktop and mobile), every linked PDF with its tagging status, and a printable ADA Title II readiness report with a score per site. For city, county, school and state websites. Not legal advice.

- **URL**: https://apify.com/omargnagy/ada-title-ii-scanner.md
- **Developed by:** [Omar Nagy](https://apify.com/omargnagy) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 96.6% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.00 / 1,000 scanned pages

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ADA Title II Accessibility Checker: WCAG and PDF

This ADA Title II accessibility checker crawls a public website, tests every page against WCAG 2.1 Level AA with axe-core in desktop and mobile views, checks every linked PDF for accessibility tags, and writes one printable readiness report per site. It is built for the websites of cities, counties, school districts, special districts and states, and for the consultants and agencies who audit them. You pay per page, per PDF and per report, from $0.01 a page, with no start fee.

> This is a readiness scan, not a conformance certificate, and not legal advice. Automated testing finds only part of what WCAG 2.1 Level AA requires; keyboard and screen reader testing by a person is still needed. Accessibility overlays are not treated as conformance under the rule.

### What the ADA Title II accessibility checker returns

- **One dataset row per page**: URL, title, HTTP status, a page score from 0 to 100, every axe-core rule that failed with its impact, the WCAG success criteria it maps to, how many elements failed, up to five CSS selectors, which screen sizes it failed in, pass and needs-review counts, the `lang` attribute and a screenshot link. An address that could not be tested (a 404, a redirect off the site, a page that never loaded) is an `error` row with the reason, and it is free.
- **A PDF inventory**: every linked PDF with its size, last-modified date, page count, whether it is tagged (a MarkInfo flag and a real structure tree), and whether it has a text layer at all. It is in the HTML report and, in full, in `report.json` under `pdfs.documents`.
- **A readiness report per site** in the key-value store: `report.html` (printable) and `report.json`. It carries the readiness score with its formula, the top rules by pages affected, the worst pages first, the PDF table, pages that could not be tested, and the compliance date that applies.
- **Exception hints** for the five categories the rule treats differently (archived content, preexisting documents, third-party content, password-protected documents, preexisting social media posts). They are pattern matches with a reason, never a determination.
- **A run summary** in `OUTPUT`: pages delivered, PDFs inspected, events charged, and whether the run stopped at your maximum cost.

### Who it is for

- **Accessibility consultants and web agencies** who need a first-pass audit of a public entity's site, with a report they can hand over under their own name (`preparedBy`, `contact`).
- **City, county and school district web teams** who want to know where they stand before their deadline, and which pages and PDFs to fix first.
- **Procurement and compliance staff** comparing several sites in one run.
- **AI agents** that need a tool for "how accessible is this government website, and what should be fixed first".

### The ADA Title II web rule deadlines

The US Department of Justice rule on web content and mobile apps of state and local governments sets **WCAG 2.1 Level AA** as the standard. The compliance dates:

| Date | Who |
|---|---|
| 26 April 2027 | public entities with a total population of 50,000 or more |
| 26 April 2028 | public entities with a total population under 50,000, and special district governments |

Source: [ADA.gov, fact sheet on the web and mobile app rule](https://www.ada.gov/resources/2024-03-08-web-rule/). Give the entity's `population` and the report states which date applies. Leave it out and the report lists both.

### How to check a website for ADA Title II

1. Put the site in **Website to scan**, for example `cityofexample.gov`. Add several, separated by commas, for one report each.
2. Set **Maximum pages per site**. Start small, then raise it for a full answer (up to 250).
3. Optionally add the population, the entity name and your own name for the report footer.
4. Run it. Open `report.html` from the Output tab, or read the dataset and `report.json` through the API.

A standard scan of one city site:

```json
{
  "domain": "cityofexample.gov",
  "maxPages": 100,
  "maxPdfs": 50,
  "population": 42500,
  "entityName": "City of Example",
  "populationSource": "US Census Bureau 2024 population estimate",
  "preparedBy": "Example Accessibility Consulting"
}
```

Several sites in one run, pages only:

```json
{
  "domain": "cityofexample.gov, examplecounty.gov, exampleschools.org",
  "maxPages": 25,
  "includePdfInventory": false
}
```

The prefilled example scans the rule's own page on `www.ada.gov` at 3 pages and up to 4 PDFs, so it finishes in about a minute and shows the PDF inventory too. It points at a federal site built for traffic on purpose, so trial runs never load a small town's server.

### Output example

One dataset row from a run of the prefilled input (the screenshot links point at your run's key-value store):

```json
{
  "domain": "ada.gov",
  "url": "https://www.ada.gov/resources/2024-03-08-web-rule/",
  "finalUrl": "https://www.ada.gov/resources/2024-03-08-web-rule/",
  "title": "Fact Sheet: New Rule on the Accessibility of Web Content and Mobile Apps Provided by State and Local Governments | ADA.gov",
  "statusCode": 200,
  "contentType": "text/html; charset=utf-8",
  "pageType": "html",
  "depth": 0,
  "linkedFrom": null,
  "lang": "en",
  "violations": [
    {
      "ruleId": "list",
      "impact": "serious",
      "description": "Ensure that lists are structured correctly",
      "help": "<ul> and <ol> must only directly contain <li>, <script> or <template> elements",
      "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/list?application=playwright",
      "wcagCriteria": [
        "1.3.1"
      ],
      "tags": [
        "wcag2a",
        "wcag131"
      ],
      "nodeCount": 1,
      "selectors": [
        ".usa-sidenav"
      ],
      "viewports": [
        "desktop"
      ]
    }
  ],
  "violationCount": 1,
  "criticalCount": 0,
  "seriousCount": 1,
  "moderateCount": 0,
  "minorCount": 0,
  "passCount": 30,
  "incompleteCount": 1,
  "pageScore": 95,
  "screenshots": {
    "desktop": "https://api.apify.com/v2/key-value-stores/<storeId>/records/screenshot-ada-gov-0001-desktop",
    "mobile": "https://api.apify.com/v2/key-value-stores/<storeId>/records/screenshot-ada-gov-0002-mobile"
  },
  "screenshotUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/screenshot-ada-gov-0001-desktop",
  "exceptionHints": [],
  "axeVersion": "4.13.0",
  "scannedAt": "2026-10-08T20:10:23.239Z",
  "error": null
}
```

The PDF inventory in `report.json` (one entry shown):

```json
{
  "url": "https://www.ada.gov/assets/pdfs/2026-ifr.pdf",
  "linkText": "IFR",
  "linkedFrom": "https://www.ada.gov/resources/2024-03-08-web-rule/",
  "inspected": true,
  "isTagged": true,
  "markedTagged": true,
  "hasStructTree": true,
  "hasTextLayer": true,
  "pageCount": 34,
  "sizeBytes": 396292,
  "lastModified": "Tue, 21 Apr 2026 19:37:04 GMT",
  "language": "en",
  "title": "Extension of Compliance Dates for Nondiscrimination on the Basis of Disability; Accessibility of Web Information and Services of State and Local Government Entities",
  "note": null
}
```

Download the dataset as JSON, CSV or Excel. With several sites in one run, `report.html` and `report.json` become an index that links each site's own `report-<domain>.html`, and `REPORT_INDEX` lists every site with its headline numbers.

### Pricing

Pay per event. Compute, proxy and storage are included in the price.

| Event | Price | When it is charged |
|---|---|---|
| `page` | from $0.01 per page, lower on Bronze, Silver and Gold plans | One page loaded and tested in every screen size you chose, with screenshots. Charged once per page. |
| `pdf` | from $0.005 per PDF, lower on Bronze, Silver and Gold plans | One linked PDF downloaded and opened for its tag, structure tree and text layer checks. |
| `report` | from $0.20 per site, lower on Bronze, Silver and Gold plans | The HTML report and JSON summary for one site. |

- No start fee. Error rows, PDFs that were too large, refused, blocked by robots.txt or unreadable, and the report of a site where no page could be tested are all free.
- The prefilled run (3 pages, 2 PDFs, 1 report) costs $0.24. A 100-page site with 20 PDFs costs $1.30. One site at the full 250 pages and the default 200 PDFs costs at most $3.70.
- If you set a maximum cost per run, the report price is set aside first. The scan stops when the next page would not fit, writes the report for everything it charged, and says "Stopped at your maximum cost" in the status message, the report and `OUTPUT`.

### Use it from Claude, Cursor or any MCP client

Load this Actor as a tool through Apify's MCP server:

```
https://mcp.apify.com?tools=omargnagy/ada-title-ii-scanner
```

Add it to your client's MCP config:

```json
{
  "mcpServers": {
    "ada-title-ii-checker": {
      "url": "https://mcp.apify.com?tools=omargnagy/ada-title-ii-scanner"
    }
  }
}
```

Your client signs in to Apify on first use, or you can send your Apify API token as an `Authorization: Bearer` header. Then ask in plain language, for example: "Scan 20 pages of cityofexample.gov for ADA Title II, then list the five rules that affect the most pages and every untagged PDF."

### Re-scan on a schedule

1. Save your input as a task.
2. Add an Apify schedule, for example monthly.
3. Compare `readinessScore` and `untaggedPdfCount` in `REPORT_INDEX` between runs to track progress toward the deadline.
4. Send each report to Slack, Google Drive, Make, Zapier or n8n with Apify integrations or webhooks.

### How the score is made

```
Per page:   penalty  = sum over failed rules of impactWeight x min(elements, 3)
            impactWeight: critical 8, serious 5, moderate 2, minor 1
            pageScore = max(0, 100 - penalty)

Per site:   htmlScore = mean pageScore over tested pages
            pdfScore  = 100 x (tagged PDFs / PDFs inspected)
            readinessScore = round(0.8 x htmlScore + 0.2 x pdfScore)
                             or round(htmlScore) when no PDF was inspected
```

The formula is printed in every report next to the number, and every row records the axe-core version, so a score can be recomputed. Elements are capped at three per rule, so twenty low-contrast links in one menu count as one fix, not twenty.

A PDF counts as tagged only when it has both the MarkInfo flag and a real structure tree. A file that claims to be marked but has no structure tree does nothing for a screen reader, and the report names that case.

### How it crawls

- Scope is the site's host and, by default, its subdomains. It follows links only, submits no forms and enters no logged-in areas.
- One page at a time by default, at least one second between requests. If the site's robots.txt asks for a longer Crawl-Delay, the site's number wins (up to 120 seconds) and the report says so.
- robots.txt Disallow rules are obeyed for pages and PDF downloads.
- Desktop and mobile are tested on one page load, so the site serves each page once.
- The scanner identifies itself with a user agent that links to this page.
- Pages load through Apify's datacenter proxy. Residential proxies are never used.

### Limits

- Automated rules cover part of WCAG 2.1 AA. Alternative text quality, keyboard flow, captions and error recovery need a person.
- Up to 250 pages per site per run, and up to 2,000 PDFs. A site that asks for a long Crawl-Delay scans slowly. If the run's time limit comes first, the scan stops on time, writes the report for what it tested, and says "This scan stopped early" on the report's first screen.
- The PDF check reads tags, structure tree, text layer, language and title. It is not a full PDF/UA validation.
- Mobile apps are not tested.
- Some public-sector sites block traffic from cloud servers and answer HTTP 403. Those sites cannot be scanned, and the run fails at no charge.
- Exception hints match URL, host and date patterns. Whether content is actually exempt is for the entity and its counsel to decide.
- Two runs of the same site from one account at the same time are not allowed: the second is refused with a message naming the first. If the first run was aborted or crashed, the site is released automatically within about 3 minutes.

### FAQ

**Is this a compliance certificate?**
No. It is evidence of where a site stands against WCAG 2.1 AA as far as automated rules can tell, with a reproducible score. Use it to find and prioritize fixes, then test by hand.

**Which deadline applies to my entity?**
Set `population`. At 50,000 or more the report states 26 April 2027, below that 26 April 2028. Special district governments have the 2028 date whatever their population, so leave the field empty for them.

**Why did my run fail or test no pages?**
The domain may be mistyped, offline, or refusing automated visitors, or the start page may redirect to another site. The status message and the report say which. A run that tests no page charges nothing, not even the report.

**How long does a scan take?**
Roughly 5 to 15 seconds per page on a site with no crawl delay, plus a few seconds per PDF. The prefilled run takes about a minute. A 100-page site takes roughly 10 to 30 minutes, longer if the site asks for a crawl delay.

**Can I put my own name on the report?**
Yes. Fill **Prepared by** and **Contact line**, and they print at the foot of every report. Nothing else is branded.

**Can I check WCAG 2.2 too?**
Yes. Add `wcag22aa` to the axe-core rule tags. The rule itself names WCAG 2.1 AA.

**Why did a run with a small maximum cost return nothing?**
One page plus the report must fit in it. Set at least $0.21 per site.

**Can I scan pages behind a login?**
No. It scans public pages only.

# Actor input Schema

## `domain` (type: `string`):

A domain such as cityofexample.gov, or a page to start from. Separate several sites with commas to get one report per site.

## `maxPages` (type: `integer`):

How many pages to load and test per site, up to 250. Pages that fail to load or return an HTTP error are listed and not charged.

## `includePdfInventory` (type: `boolean`):

Download every PDF linked from a scanned page and check its tags, structure tree, text layer and page count. Turn off for a pages-only scan.

## `maxPdfs` (type: `integer`):

How many PDFs to check per site. The report still counts every PDF link it found and says when the list is a sample.

## `viewports` (type: `array`):

Desktop is 1366 x 900, mobile is 390 x 844. A page is charged once, however many sizes are tested.

## `population` (type: `integer`):

Total population of the city, county or district. The report then states which ADA Title II date applies: 26 April 2027 at 50,000 or more, 26 April 2028 below that.

## `entityName` (type: `string`):

The name printed on the report, for example City of Example. Defaults to the domain.

## `populationSource` (type: `string`):

Where the population figure came from, printed next to it so the number is traceable.

## `preparedBy` (type: `string`):

Your organization's name, printed at the foot of every report. Empty by default.

## `contact` (type: `string`):

A contact line printed at the foot of every report, for example an email for questions about it. Empty by default.

## `startUrls` (type: `array`):

Pages to start from, one per line, when navigation hides a section. Pages on the same host share one report. Replaces Website to scan when filled.

## `includeSubdomains` (type: `boolean`):

Also crawl subdomains of the site, where agenda portals and payment pages often live. Off scans the exact host only.

## `wcagTags` (type: `array`):

Which axe-core rule sets to run. The default is WCAG 2.0 and 2.1 at levels A and AA, the standard the rule names. Add wcag22aa for WCAG 2.2 or best-practice for advisory checks.

## `maxPdfMegabytes` (type: `integer`):

PDFs larger than this are listed with their size and date but not downloaded or charged.

## `takeScreenshots` (type: `boolean`):

Save one JPEG per page per screen size, linked from the report as evidence.

## `screenshotFullPage` (type: `boolean`):

Capture the whole scrollable page instead of the visible area. Several times larger.

## `maxConcurrency` (type: `integer`):

How many pages load at the same time. The default of 1 protects small public servers. Raise it, up to 8, only for a site that can take it.

## `crawlDelaySecs` (type: `integer`):

Gap between requests to one host, and between PDF downloads. If robots.txt asks for a longer Crawl-Delay, the site's number wins (up to 120 seconds).

## `respectRobotsTxt` (type: `boolean`):

Leave on. Disallow rules are then obeyed for pages and PDFs. A Crawl-Delay is always honoured.

## `userAgent` (type: `string`):

How the scanner identifies itself to each site. The default links to this Actor's page so a site operator can see who crawled them.

## `useApifyProxy` (type: `boolean`):

Load pages through Apify's datacenter proxy. robots.txt and PDF downloads go direct from the run. Residential proxies are never used.

## Actor input object example

```json
{
  "domain": "cityofexample.gov",
  "maxPages": 3,
  "includePdfInventory": true,
  "maxPdfs": 4,
  "viewports": [
    "desktop",
    "mobile"
  ],
  "entityName": "City of Example",
  "populationSource": "US Census Bureau 2024 population estimate",
  "preparedBy": "Example Accessibility Consulting",
  "contact": "accessibility@example.com",
  "includeSubdomains": true,
  "wcagTags": [
    "wcag2a",
    "wcag2aa",
    "wcag21a",
    "wcag21aa"
  ],
  "maxPdfMegabytes": 25,
  "takeScreenshots": true,
  "screenshotFullPage": false,
  "maxConcurrency": 1,
  "crawlDelaySecs": 1,
  "respectRobotsTxt": true,
  "userAgent": "Mozilla/5.0 (compatible; ADATitleIIScanner/0.2; +https://apify.com/omargnagy/ada-title-ii-scanner)",
  "useApifyProxy": true
}
```

# Actor output Schema

## `pages` (type: `string`):

No description

## `report` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `index` (type: `string`):

No description

## `output` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domain": "https://www.ada.gov/resources/2024-03-08-web-rule/",
    "maxPages": 3,
    "includePdfInventory": true,
    "maxPdfs": 4,
    "viewports": [
        "desktop",
        "mobile"
    ],
    "includeSubdomains": true,
    "wcagTags": [
        "wcag2a",
        "wcag2aa",
        "wcag21a",
        "wcag21aa"
    ],
    "takeScreenshots": true,
    "respectRobotsTxt": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("omargnagy/ada-title-ii-scanner").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "domain": "https://www.ada.gov/resources/2024-03-08-web-rule/",
    "maxPages": 3,
    "includePdfInventory": True,
    "maxPdfs": 4,
    "viewports": [
        "desktop",
        "mobile",
    ],
    "includeSubdomains": True,
    "wcagTags": [
        "wcag2a",
        "wcag2aa",
        "wcag21a",
        "wcag21aa",
    ],
    "takeScreenshots": True,
    "respectRobotsTxt": True,
}

# Run the Actor and wait for it to finish
run = client.actor("omargnagy/ada-title-ii-scanner").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domain": "https://www.ada.gov/resources/2024-03-08-web-rule/",
  "maxPages": 3,
  "includePdfInventory": true,
  "maxPdfs": 4,
  "viewports": [
    "desktop",
    "mobile"
  ],
  "includeSubdomains": true,
  "wcagTags": [
    "wcag2a",
    "wcag2aa",
    "wcag21a",
    "wcag21aa"
  ],
  "takeScreenshots": true,
  "respectRobotsTxt": true
}' |
apify call omargnagy/ada-title-ii-scanner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,omargnagy/ada-title-ii-scanner"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/H4uoDCbPIpPYPBstQ/builds/qhqvwJ1OaK3v3Fwuh/openapi.json
