Developers · API v1 · version 2026-11-07

WCAG check API: docs

Base address https://api.tradingcheckup.com. JSON in, JSON out. A scan takes about 20-90 seconds a page, so you start it, then ask for the result (or get a webhook).

Quick start (3 steps)

  1. Create a free key (sign in, one click). It starts with tck_test_ (example results) or tck_live_ (real scans) and is shown once.
  2. Save it as an environment variable: export TC_API_KEY=tck_test_...
  3. Start a scan, then ask for the result until status is done:
curl
# 1. start a scan (returns 202 with an id)
curl -s -X POST https://api.tradingcheckup.com/v1/wcag/scans \
  -H "Authorization: Bearer $TC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.example.com/", "pages": 1}'

# 2. ask for the result every 10 seconds until "status" is "done"
curl -s https://api.tradingcheckup.com/v1/wcag/scans/scan_XXXXXXXXXXXXXXXXXXXX \
  -H "Authorization: Bearer $TC_API_KEY"

A finished result looks like this (real output from our 2-page demo site, a made-up business; addresses changed to example.com):

GET /v1/wcag/scans/{id} - status done (short)
{
  "id": "scan_c1b9a6b19b2680b9ea03",
  "object": "wcag_scan",
  "status": "done",
  "url": "https://www.example.com/",
  "score": 52,
  "counts": {
    "definite": 5,
    "needs_review": 0,
    "fix_verified": 5,
    "confirmed_by_second_check": 5
  },
  "owner_summary": [
    "We checked 2 page(s) on desktop and phone. Accessibility score 52/100.",
    "5 definite problem type(s), every one confirmed by a second, independent check; 5 serious.",
    "Fix first: Button has no name. Some customers cannot use this control at all (blind or keyboard-only users), so enquiries, bookings or sign-ups can be lost here.",
    "5 fix(es) were applied to a copy of the page and re-checked: the problem was gone and nothing new broke (Proven Fix)."
  ],
  "pages": [
    {
      "url": "https://www.example.com/",
      "status": "checked",
      "title": "Home - Fixture Plumbing",
      "note": null
    },
    {
      "url": "https://www.example.com/about.html",
      "status": "checked",
      "title": "About us - Fixture Plumbing",
      "note": null
    }
  ],
  "issues": [
    {
      "id": "iss_1",
      "page": "https://www.example.com/",
      "level": "definite",
      "kind": "button-name",
      "wcag": [
        "4.1.2"
      ],
      "impact": "critical",
      "title": "Button has no name",
      "who_is_affected": "Screen-reader and voice users hear \"button\" and cannot say or know which one.",
      "explanation": "Button has no text, aria-label or title, so it is announced only as \"button\" (WCAG 4.1.2). It shows only an SVG icon (one rectangle).",
      "business_impact": "Some customers cannot use this control at all (blind or keyboard-only users), so enquiries, bookings or sign-ups can be lost here.",
      "who_fixes": "Your web developer (page template change)",
      "effort": "About 30-60 minutes",
      "selector": ".b1",
      "html_snippet": "<button class=\"b1\"><svg width=\"20\" height=\"20\"><rect width=\"20\" height=\"20\"></rect></svg></button>",
      "count": 1,
      "also_at": [],
      "second_check": {
        "confirmed": true,
        "method": "accessibility tree",
        "detail": "Chrome's accessibility tree gives this button no name (visible, 36 x 29 px)"
      },
      "fix": {
        "text": "Proven Fix (verified on a copy of this page): Add aria-label=\"Describe the action\" - (set `aria-label=\"Describe the action\"`) change `<button class=\"b1\">` to `<button class=\"b1\" aria-label=\"Describe the action\">`. We applied exactly this to a copy and re-ran the axe-core rules and our own page checks: the problem is gone on this element and no new problem appeared on the changed element(s). The words are a placeholder: this button shows only an SVG icon (one rectangle) and its code has no text, title or meaningful class/id, so the tool cannot tell its action. Click it (or find its click handler in the script) to see what it does, then write that action in 1-3 words, e.g. aria-label=\"Play video\" or aria-label=\"Close menu\"; better still, show that word on screen next to the icon.",
        "verified_after_fix": true,
        "status": "verified",
        "code_after": [
          "<button class=\"b1\" aria-label=\"Describe the action\"><svg width=\"20\" height=\"20\"><rect width=\"20\" height=\"20\"></rect></svg></button>"
        ],
        "why_not_verified": null
      }
    },
    {
      "id": "iss_2",
      "page": "https://www.example.com/",
      "level": "definite",
      "kind": "button-name",
      "wcag": [
        "4.1.2"
      ],
      "impact": "critical",
      "title": "Button has no name",
      "who_is_affected": "Screen-reader and voice users hear \"button\" and cannot say or know which one.",
      "explanation": "Button has no text, aria-label or title, so it is announced only as \"button\" (WCAG 4.1.2). It shows only an SVG icon (one circle).",
      "business_impact": "Some customers cannot use this control at all (blind or keyboard-only users), so enquiries, bookings or sign-ups can be lost here.",
      "who_fixes": "Your web developer (page template change)",
      "effort": "About 30-60 minutes",
      "selector": ".b2",
      "html_snippet": "<button class=\"b2\"><svg width=\"20\" height=\"20\"><circle cx=\"10\" cy=\"10\" r=\"9\"></circle></svg></button>",
      "count": 1,
      "also_at": [],
      "second_check": {
        "confirmed": true,
        "method": "accessibility tree",
        "detail": "Chrome's accessibility tree gives this button no name (visible, 36 x 29 px)"
      },
      "fix": {
        "text": "Proven Fix (verified on a copy of this page): Add aria-label=\"Describe the action\" - (set `aria-label=\"Describe the action\"`) change `<button class=\"b2\">` to `<button class=\"b2\" aria-label=\"Describe the action\">`. We applied exactly this to a copy and re-ran the axe-core rules and our own page checks: the problem is gone on this element and no new problem appeared on the changed element(s). The words are a placeholder: this button shows only an SVG icon (one circle) and its code has no text, title or meaningful class/id, so the tool cannot tell its action. Click it (or find its click handler in the script) to see what it does, then write that action in 1-3 words, e.g. aria-label=\"Play video\" or aria-label=\"Close menu\"; better still, show that word on screen next to the icon.",
        "verified_after_fix": true,
        "status": "verified",
        "code_after": [
          "<button class=\"b2\" aria-label=\"Describe the action\"><svg width=\"20\" height=\"20\"><circle cx=\"10\" cy=\"10\" r=\"9\"></circle></svg></button>"
        ],
        "why_not_verified": null
      }
    },
    "... more issues ..."
  ],
  "legal_risk": {
    "summary": "We found 5 definite accessibility problem type(s) (5 serious). Problems like these (missing labels, low contrast, keyboard traps, unnamed buttons) are the kind often listed in accessibility complaints and demand letters. Fixing them lowers that risk; it does not remove it.",
    "laws": [
      "... 4 regions with official sources ..."
    ],
    "not_legal_advice": "..."
  }
}
Show the full result (5 issues)
full result
{
  "id": "scan_c1b9a6b19b2680b9ea03",
  "object": "wcag_scan",
  "status": "done",
  "url": "https://www.example.com/",
  "pages_requested": 2,
  "device": "both",
  "created_at": "2026-11-07T07:38:32Z",
  "started_at": "2026-11-07T07:38:32Z",
  "finished_at": "2026-11-07T07:38:59Z",
  "links": {
    "self": "/v1/wcag/scans/scan_c1b9a6b19b2680b9ea03",
    "report_pdf": "/v1/wcag/scans/scan_c1b9a6b19b2680b9ea03/report.pdf"
  },
  "api_version": "2026-11-07",
  "standard": "WCAG 2.2 AA",
  "checked_views": [
    "desktop 1280x900",
    "phone 320x640"
  ],
  "pages": [
    {
      "url": "https://www.example.com/",
      "status": "checked",
      "title": "Home - Fixture Plumbing",
      "note": null
    },
    {
      "url": "https://www.example.com/about.html",
      "status": "checked",
      "title": "About us - Fixture Plumbing",
      "note": null
    }
  ],
  "score": 52,
  "score_note": "0-100 from definite problems only (needs-review items do not lower it). Never a statement that a site is compliant.",
  "counts": {
    "definite": 5,
    "needs_review": 0,
    "fix_verified": 5,
    "confirmed_by_second_check": 5
  },
  "owner_summary": [
    "We checked 2 page(s) on desktop and phone. Accessibility score 52/100.",
    "5 definite problem type(s), every one confirmed by a second, independent check; 5 serious.",
    "Fix first: Button has no name. Some customers cannot use this control at all (blind or keyboard-only users), so enquiries, bookings or sign-ups can be lost here.",
    "5 fix(es) were applied to a copy of the page and re-checked: the problem was gone and nothing new broke (Proven Fix)."
  ],
  "legal_risk": {
    "summary": "We found 5 definite accessibility problem type(s) (5 serious). Problems like these (missing labels, low contrast, keyboard traps, unnamed buttons) are the kind often listed in accessibility complaints and demand letters. Fixing them lowers that risk; it does not remove it.",
    "laws": [
      {
        "region": "Australia",
        "text": "The Disability Discrimination Act 1992 makes it unlawful to discriminate when providing goods, services and facilities (section 24). The Australian Human Rights Commission treats websites as part of a service; its advisory notes (2014, being updated) point to WCAG at level AA as the benchmark. People can complain to the Commission, which tries to settle it by conciliation; some complaints go on to court.",
        "sources": [
          {
            "title": "Disability Discrimination Act 1992 (legislation.gov.au)",
            "url": "https://www.legislation.gov.au/C2004A04426/latest/text"
          },
          {
            "title": "Australian Human Rights Commission: World Wide Web Access advisory notes",
            "url": "https://humanrights.gov.au/our-work/disability-rights/world-wide-web-access-disability-discrimination-act-advisory-notes-ver-41-2014"
          }
        ]
      },
      {
        "region": "United States",
        "text": "The Americans with Disabilities Act covers businesses open to the public (Title III) and state and local governments (Title II). For businesses there is no detailed federal web standard; the Department of Justice names WCAG as helpful guidance. State and local governments have a rule that uses WCAG 2.1 AA, with dates in April 2027 or April 2028 depending on the population they serve. Private website lawsuits are common in the US.",
        "sources": [
          {
            "title": "ADA.gov: Guidance on web accessibility and the ADA",
            "url": "https://www.ada.gov/resources/web-guidance/"
          },
          {
            "title": "ADA.gov: Title II web rule fact sheet",
            "url": "https://www.ada.gov/resources/2024-03-08-web-rule/"
          }
        ]
      },
      {
        "region": "European Union",
        "text": "The European Accessibility Act (Directive (EU) 2019/882) applies from 28 June 2025 to certain products and services sold to consumers, including e-commerce, banking and transport services. Microenterprises that provide services (fewer than 10 staff and no more than EUR 2 million turnover or balance sheet) are exempt. Each EU country applies it through its own law, so details and penalties differ. The technical benchmark is the standard EN 301 549, which is based on WCAG.",
        "sources": [
          {
            "title": "Directive (EU) 2019/882 (EUR-Lex)",
            "url": "https://eur-lex.europa.eu/eli/dir/2019/882/oj"
          },
          {
            "title": "Irish regulator CCPC: EAA guidelines for microenterprises",
            "url": "https://www.ccpc.ie/business/enforcement/accessibility/european-accessibility-act-guidelines-for-microenterprises/"
          }
        ]
      },
      {
        "region": "United Kingdom",
        "text": "Under the Equality Act 2010 (Great Britain; Northern Ireland uses the Disability Discrimination Act 1995), businesses that provide services to the public must make reasonable adjustments for disabled people, and that includes websites. Public sector bodies also have specific website regulations that use WCAG 2.2 and need an accessibility statement.",
        "sources": [
          {
            "title": "Equality Act 2010, section 29 (legislation.gov.uk)",
            "url": "https://www.legislation.gov.uk/ukpga/2010/15/section/29"
          },
          {
            "title": "GOV.UK: Accessibility requirements for public sector websites",
            "url": "https://www.gov.uk/guidance/accessibility-requirements-for-public-sector-websites-and-apps"
          }
        ]
      }
    ],
    "not_legal_advice": "This is general information, not legal advice. Whether a law applies to you depends on your business, your customers and where you trade. This report does not show that your site is or is not legal. If you are worried, or you got a complaint or demand letter, talk to a lawyer."
  },
  "issues": [
    {
      "id": "iss_1",
      "page": "https://www.example.com/",
      "level": "definite",
      "kind": "button-name",
      "wcag": [
        "4.1.2"
      ],
      "impact": "critical",
      "title": "Button has no name",
      "who_is_affected": "Screen-reader and voice users hear \"button\" and cannot say or know which one.",
      "explanation": "Button has no text, aria-label or title, so it is announced only as \"button\" (WCAG 4.1.2). It shows only an SVG icon (one rectangle).",
      "business_impact": "Some customers cannot use this control at all (blind or keyboard-only users), so enquiries, bookings or sign-ups can be lost here.",
      "who_fixes": "Your web developer (page template change)",
      "effort": "About 30-60 minutes",
      "selector": ".b1",
      "html_snippet": "<button class=\"b1\"><svg width=\"20\" height=\"20\"><rect width=\"20\" height=\"20\"></rect></svg></button>",
      "count": 1,
      "also_at": [],
      "second_check": {
        "confirmed": true,
        "method": "accessibility tree",
        "detail": "Chrome's accessibility tree gives this button no name (visible, 36 x 29 px)"
      },
      "fix": {
        "text": "Proven Fix (verified on a copy of this page): Add aria-label=\"Describe the action\" - (set `aria-label=\"Describe the action\"`) change `<button class=\"b1\">` to `<button class=\"b1\" aria-label=\"Describe the action\">`. We applied exactly this to a copy and re-ran the axe-core rules and our own page checks: the problem is gone on this element and no new problem appeared on the changed element(s). The words are a placeholder: this button shows only an SVG icon (one rectangle) and its code has no text, title or meaningful class/id, so the tool cannot tell its action. Click it (or find its click handler in the script) to see what it does, then write that action in 1-3 words, e.g. aria-label=\"Play video\" or aria-label=\"Close menu\"; better still, show that word on screen next to the icon.",
        "verified_after_fix": true,
        "status": "verified",
        "code_after": [
          "<button class=\"b1\" aria-label=\"Describe the action\"><svg width=\"20\" height=\"20\"><rect width=\"20\" height=\"20\"></rect></svg></button>"
        ],
        "why_not_verified": null
      }
    },
    {
      "id": "iss_2",
      "page": "https://www.example.com/",
      "level": "definite",
      "kind": "button-name",
      "wcag": [
        "4.1.2"
      ],
      "impact": "critical",
      "title": "Button has no name",
      "who_is_affected": "Screen-reader and voice users hear \"button\" and cannot say or know which one.",
      "explanation": "Button has no text, aria-label or title, so it is announced only as \"button\" (WCAG 4.1.2). It shows only an SVG icon (one circle).",
      "business_impact": "Some customers cannot use this control at all (blind or keyboard-only users), so enquiries, bookings or sign-ups can be lost here.",
      "who_fixes": "Your web developer (page template change)",
      "effort": "About 30-60 minutes",
      "selector": ".b2",
      "html_snippet": "<button class=\"b2\"><svg width=\"20\" height=\"20\"><circle cx=\"10\" cy=\"10\" r=\"9\"></circle></svg></button>",
      "count": 1,
      "also_at": [],
      "second_check": {
        "confirmed": true,
        "method": "accessibility tree",
        "detail": "Chrome's accessibility tree gives this button no name (visible, 36 x 29 px)"
      },
      "fix": {
        "text": "Proven Fix (verified on a copy of this page): Add aria-label=\"Describe the action\" - (set `aria-label=\"Describe the action\"`) change `<button class=\"b2\">` to `<button class=\"b2\" aria-label=\"Describe the action\">`. We applied exactly this to a copy and re-ran the axe-core rules and our own page checks: the problem is gone on this element and no new problem appeared on the changed element(s). The words are a placeholder: this button shows only an SVG icon (one circle) and its code has no text, title or meaningful class/id, so the tool cannot tell its action. Click it (or find its click handler in the script) to see what it does, then write that action in 1-3 words, e.g. aria-label=\"Play video\" or aria-label=\"Close menu\"; better still, show that word on screen next to the icon.",
        "verified_after_fix": true,
        "status": "verified",
        "code_after": [
          "<button class=\"b2\" aria-label=\"Describe the action\"><svg width=\"20\" height=\"20\"><circle cx=\"10\" cy=\"10\" r=\"9\"></circle></svg></button>"
        ],
        "why_not_verified": null
      }
    },
    {
      "id": "iss_3",
      "page": "https://www.example.com/",
      "level": "definite",
      "kind": "contrast",
      "wcag": [
        "1.4.3"
      ],
      "impact": "serious",
      "title": "Text contrast 2.32:1 is below the required 4.5:1",
      "who_is_affected": "People with low vision, older users, anyone on a phone in sunlight.",
      "explanation": "Measured 2.32:1 (#aaaaaa on #ffffff, 16px normal); WCAG 1.4.3 requires 4.5:1.",
      "business_impact": "Older visitors and people with low vision struggle to read this; on a phone in sunlight almost everyone does.",
      "who_fixes": "Your web developer (style/CSS change)",
      "effort": "About 30-60 minutes (one style rule often fixes every place)",
      "selector": ".lo",
      "html_snippet": "<p class=\"lo\">Open 7 days, 7am to 7pm, including public holidays.</p>",
      "count": 1,
      "also_at": [],
      "second_check": {
        "confirmed": true,
        "method": "colour maths + pixels",
        "detail": "own maths 2.32:1 (16px 400, needs 4.5:1) and the strongest on-screen text pixels reach only 2.32:1"
      },
      "fix": {
        "text": "Proven Fix (verified on a copy of this page): Set the text colour to #707070 - add this CSS: `.lo { color: #707070; }`. We applied exactly this to a copy and re-ran the axe-core rules and our own page checks: the problem is gone on this element and no new problem appeared on the changed element(s). Measured on the copy: #707070 on #ffffff measures 4.95:1. Where the colour comes from: the CSS rule `.lo { color: rgb(170, 170, 170) }` in a <style> block in the page.",
        "verified_after_fix": true,
        "status": "verified",
        "code_after": [
          ""
        ],
        "why_not_verified": null
      }
    },
    {
      "id": "iss_4",
      "page": "https://www.example.com/",
      "level": "definite",
      "kind": "img-alt",
      "wcag": [
        "1.1.1"
      ],
      "impact": "serious",
      "title": "Image has no text alternative",
      "who_is_affected": "Blind screen-reader users hear the file name or nothing.",
      "explanation": "Image has no alt attribute (WCAG 1.1.1); screen readers read the file name or skip it.",
      "business_impact": "Screen-reader users hear nothing useful here, and search engines lose the same information.",
      "who_fixes": "You or whoever edits the website content (no coding)",
      "effort": "Quick: about 2 minutes per place",
      "selector": "img",
      "html_snippet": "<img src=\"/van.png\" width=\"200\" height=\"120\">",
      "count": 1,
      "also_at": [],
      "second_check": {
        "confirmed": true,
        "method": "accessibility tree",
        "detail": "Chrome's accessibility tree gives this image no name (visible, 200 x 120 px)"
      },
      "fix": {
        "text": "Proven Fix (verified on a copy of this page): Add alt=\"Van\" - (set `alt=\"Van\"`) change `<img src=\"/van.png\" width=\"200\" height=\"120\">` to `<img src=\"/van.png\" width=\"200\" height=\"120\" alt=\"Van\">`. We applied exactly this to a copy and re-ran the axe-core rules and our own page checks: the problem is gone on this element and no new problem appeared on the changed element(s). The name was taken from its src \"/van.png\"; change the words if they do not say what it shows.",
        "verified_after_fix": true,
        "status": "verified",
        "code_after": [
          "<img src=\"/van.png\" width=\"200\" height=\"120\" alt=\"Van\">"
        ],
        "why_not_verified": null
      }
    },
    {
      "id": "iss_5",
      "page": "https://www.example.com/about.html",
      "level": "definite",
      "kind": "img-alt",
      "wcag": [
        "1.1.1"
      ],
      "impact": "serious",
      "title": "Image has no text alternative",
      "who_is_affected": "Blind screen-reader users hear the file name or nothing.",
      "explanation": "Image has no alt attribute (WCAG 1.1.1); screen readers read the file name or skip it.",
      "business_impact": "Screen-reader users hear nothing useful here, and search engines lose the same information.",
      "who_fixes": "You or whoever edits the website content (no coding)",
      "effort": "Quick: about 2 minutes per place",
      "selector": "img",
      "html_snippet": "<img src=\"/team.png\" width=\"200\" height=\"120\">",
      "count": 1,
      "also_at": [],
      "second_check": {
        "confirmed": true,
        "method": "accessibility tree",
        "detail": "Chrome's accessibility tree gives this image no name (visible, 200 x 120 px)"
      },
      "fix": {
        "text": "Proven Fix (verified on a copy of this page): Add alt=\"Team\" - (set `alt=\"Team\"`) change `<img src=\"/team.png\" width=\"200\" height=\"120\">` to `<img src=\"/team.png\" width=\"200\" height=\"120\" alt=\"Team\">`. We applied exactly this to a copy and re-ran the axe-core rules and our own page checks: the problem is gone on this element and no new problem appeared on the changed element(s). The name was taken from its src \"/team.png\"; change the words if they do not say what it shows.",
        "verified_after_fix": true,
        "status": "verified",
        "code_after": [
          "<img src=\"/team.png\" width=\"200\" height=\"120\" alt=\"Team\">"
        ],
        "why_not_verified": null
      }
    }
  ],
  "people_must_check": [
    {
      "what": "Whether alt text makes sense",
      "why": "We can see an image has a description, not whether the words describe it well.",
      "how": "Read each image description out loud. Would someone who cannot see the picture get the point?"
    },
    {
      "what": "Screen reader experience",
      "why": "Reading order, announcements and whether things make sense when heard.",
      "how": "Turn on VoiceOver (iPhone: Settings > Accessibility) or NVDA (free on Windows) and try to use your contact form."
    },
    {
      "what": "Keyboard use from start to finish",
      "why": "We test focus and traps on each page, not a whole task like booking or paying.",
      "how": "Put the mouse away. Use Tab, Shift+Tab, Enter and Space to book or send the form. Can you see where you are at every step?"
    },
    {
      "what": "Captions and transcripts",
      "why": "Whether video captions are correct and in sync.",
      "how": "Play each video with sound off. Can you follow it from the captions alone?"
    },
    {
      "what": "Plain language and error messages",
      "why": "Whether instructions and form errors are clear to a real person.",
      "how": "Send your form with a mistake on purpose. Does the message say what went wrong and how to fix it?"
    },
    {
      "what": "PDFs, documents and pages we did not check",
      "why": "This report covers the pages listed only.",
      "how": "List your other important pages and PDFs (menus, price lists, forms) for a later check."
    },
    {
      "what": "Zoom, motion and timing in real use",
      "why": "Some pop-ups, carousels and time limits only appear after use.",
      "how": "Zoom your browser to 200% and use the site. Does anything get cut off, move by itself, or time out?"
    }
  ],
  "limits": "Automated checks find many, not all, accessibility problems. 'definite' = failed our check and was confirmed by a second, independent check in a fresh page load. 'needs_review' = a person must look.",
  "seconds": 26.3,
  "blocked_private_requests": 0
}

API keys

Send Authorization: Bearer tck_live_... on every /v1 call and on /mcp. If your tool prefers it, X-API-Key: tck_live_... works the same way.

Test and live keys. tck_test_... keys are for building your integration: every scan returns at once with a fixed example result (marked "test_mode": true), no real site is checked and no pages are counted. tck_live_... keys run real scans and count against your plan. Older tck_... keys keep working as live keys.

Create, name and revoke keys on your keys page (up to 5 at a time). We keep only a scrambled copy (HMAC-SHA256), so a lost key cannot be shown again: make a new one and revoke the old one. A revoked key stops working at once. Keep keys on your server or in CI secrets, never in a web page (the API does not allow browser calls).

Start a scan: POST /v1/wcag/scans

FieldTypeNotes
urlstring, requiredpublic http(s) page on port 80 or 443 (the normal web ports), up to 2,048 characters. Redirects are followed only within the same site.
pagesinteger, default 11 = this page only; more = this page + links found on it (same site, shortest paths first), up to your plan's pages per scan
deviceboth | desktop | phonev1 always checks both views (1280x900 and 320x640); the field is echoed back
standardstringonly WCAG 2.2 AA in v1
webhook_urlstring, optionalpublic https:// address; we POST when the scan finishes
metadataobject, optionalup to 1,000 characters, kept with the scan for your own use

Answer: 202 Accepted + Location header + the scan with status: "queued", queue_position and retry_after_seconds. Pages are counted when the scan starts; failed scans and pages we could not open are given back.

Get the result: GET /v1/wcag/scans/{id}

While queued or running the answer has retry_after_seconds (also a Retry-After header). Ask again after that. When done, the full result is in the same answer. failed has an error object (e.g. the site blocked our checker or took too long).

Result fields

FieldMeaning
statusqueued, running, done, failed or expired (results are kept 30 days)
queue_position, retry_after_secondswhile queued / running: when to ask again
score0-100 from definite problems only; null if no page could be checked
countsdefinite, needs_review, fix_verified, confirmed_by_second_check
pages[]url, status (checked, partial, blocked, failed, refused), title, note
owner_summary[]3-5 plain sentences for the site owner
issues[]one entry per problem type and element group (below)
legal_risksummary + laws per region with official source links + not_legal_advice
people_must_check[]what automated checks cannot see, and how a person can test it
erroronly when status is failed: code + message

Each issue

FieldMeaning
leveldefinite = failed and confirmed by a second method; needs_review = a person must look (never fails your build by itself)
wcag[]WCAG 2.2 success criteria, e.g. 1.4.3
impactcritical, serious, moderate or minor
kind, title, explanationstable machine name, short title, what is wrong
who_is_affected, business_impactwho is blocked and what it costs the site
who_fixes, effortcontent editor or developer, rough minutes
selector, html_snippet, count, also_at[]CSS selector of the first element, its HTML, how many places, more selectors
second_checkconfirmed true/false, method (e.g. accessibility tree, colour maths + pixels, keyboard walk), detail
fix.textwhat to change, in words and code
fix.verified_after_fixtrue = we applied this exact change to a copy of the page, re-ran the checks: problem gone, nothing new broke
fix.code_after[]the element after the fix (only when verified)
fix.status, fix.why_not_verifiedverified, suggested or needs_person, and why
One issue
{
  "id": "iss_1",
  "page": "https://www.example.com/",
  "level": "definite",
  "kind": "button-name",
  "wcag": [
    "4.1.2"
  ],
  "impact": "critical",
  "title": "Button has no name",
  "who_is_affected": "Screen-reader and voice users hear \"button\" and cannot say or know which one.",
  "explanation": "Button has no text, aria-label or title, so it is announced only as \"button\" (WCAG 4.1.2). It shows only an SVG icon (one rectangle).",
  "business_impact": "Some customers cannot use this control at all (blind or keyboard-only users), so enquiries, bookings or sign-ups can be lost here.",
  "who_fixes": "Your web developer (page template change)",
  "effort": "About 30-60 minutes",
  "selector": ".b1",
  "html_snippet": "<button class=\"b1\"><svg width=\"20\" height=\"20\"><rect width=\"20\" height=\"20\"></rect></svg></button>",
  "count": 1,
  "also_at": [],
  "second_check": {
    "confirmed": true,
    "method": "accessibility tree",
    "detail": "Chrome's accessibility tree gives this button no name (visible, 36 x 29 px)"
  },
  "fix": {
    "text": "Proven Fix (verified on a copy of this page): Add aria-label=\"Describe the action\" - (set `aria-label=\"Describe the action\"`) change `<button class=\"b1\">` to `<button class=\"b1\" aria-label=\"Describe the action\">`. We applied exactly this to a copy and re-ran the axe-core rules and our own page checks: the problem is gone on this element and no new problem appeared on the changed element(s). The words are a placeholder: this button shows only an SVG icon (one rectangle) and its code has no text, title or meaningful class/id, so the tool cannot tell its action. Click it (or find its click handler in the script) to see what it does, then write that action in 1-3 words, e.g. aria-label=\"Play video\" or aria-label=\"Close menu\"; better still, show that word on screen next to the icon.",
    "verified_after_fix": true,
    "status": "verified",
    "code_after": [
      "<button class=\"b1\" aria-label=\"Describe the action\"><svg width=\"20\" height=\"20\"><rect width=\"20\" height=\"20\"></rect></svg></button>"
    ],
    "why_not_verified": null
  }
}
Why 'definite' is safe to fail a build on: every definite item failed our first check and was then confirmed by a second, different method in a new page load (for example Chrome's accessibility tree for names, pixel colours for contrast, a real keyboard walk for focus). Items the second method did not confirm are moved to needs_review with the reason.

PDF report: GET /v1/wcag/scans/{id}/report.pdf

A client-ready PDF of a finished scan (409 not_ready before that).

List and usage

GET /v1/wcag/scans?limit=20 lists your latest scans (newest first, up to 100). GET /v1/usage shows your plan, pages used and left this month, and when the month resets (1st of the month, UTC).

Webhooks

Add webhook_url when you start a scan. When it finishes we POST {"type": "wcag_scan.done", "scan_id", "status", "url", "finished_at", "result_url"} (or wcag_scan.failed), up to 3 tries. The TC-Signature header is t=UNIX_TIME,v1=HMAC_SHA256(webhook_secret, t + "." + raw_body). Your webhook_secret is shown with the key when you create it.

Check the signature (Python)
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str, max_age=300) -> bool:
    """header = request.headers["TC-Signature"]  e.g.  t=1762500000,v1=5f2c..."""
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > max_age:
        return False
    want = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, parts["v1"])

Idempotency

Send Idempotency-Key: any-unique-text (1-80 letters, digits, _ . : -) with a POST. The same key from the same API key within 24 hours returns the first scan (with "idempotent_replay": true) instead of starting and charging a new one. Safe for retries and CI re-runs.

Limits and quota

PlanPages a monthPages per scanRequests a minuteScans waiting at once
Free beta203303
Builder (coming)5001012010
Agency (coming)3,0001030020

Limits depend on your plan; the headers always show yours. Every answer has X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds). Over the limit: 429 with Retry-After. Payments are off during the free beta.

Errors

Every error has the same shape: {"error": {"code": "...", "message": "...", "docs": "..."}}.

HTTPcodeWhen
400bad_requesta field is missing or wrong (message says which)
400url_not_allowednot a public http(s) page on port 80/443, or it points to a private network
400webhook_not_allowedwebhook_url must be a public https:// address
401unauthorizedmissing, wrong or revoked key
402quota_exceedednot enough pages left this month (pages_left, resets_at in the error)
404not_foundno such scan for your account, or no such endpoint
405method_not_allowedwrong HTTP method
409not_readyPDF asked for before the scan is done
413 / 414bad_requestbody over 16 KB / address too long
429rate_limitedtoo many requests a minute; wait Retry-After seconds
429too_many_open_scanstoo many scans waiting at once
500server_errorour fault; safe to retry with the same Idempotency-Key

MCP for AI agents (Claude, ChatGPT and others)

Two tools: check_accessibility(url, pages=1, wait_seconds=150) starts a scan and waits for it, and get_scan_result(scan_id) fetches a scan that was still running. The agent gets the score, owner summary and the issues with fix code and the verified flag.

Claude Desktop (stdio): claude_desktop_config.json
{
  "mcpServers": {
    "tc-wcag": {
      "command": "python3",
      "args": ["/full/path/to/tc_wcag_mcp.py"],
      "env": { "TC_API_KEY": "tck_live_your_key_here" }
    }
  }
}
stdio server: download and test
# stdio (Python 3.9+, no extra packages): download the file
curl -O https://tradingcheckup.com/developers/tc_wcag_mcp.py

# test it by hand: one JSON-RPC message per line; the server answers each line,
# and check_accessibility waits for the scan (up to wait_seconds) before it answers
( echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
  echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"check_accessibility","arguments":{"url":"https://www.example.com/"}}}'
) | TC_API_KEY=tck_live_... python3 tc_wcag_mcp.py

# optional: TC_API_BASE=https://... points it at another API address (e.g. a test server)

What the agent gets back (a short form of the REST result: scan_id instead of id, up to 12 issues worst first, more_issues = how many were left out, full_result = where to read all of them). If the scan is still running after wait_seconds, the tool returns {"scan_id", "status": "running"}: call get_scan_result later.

MCP tool result (text content, JSON)
{
  "scan_id": "scan_...", "status": "done", "url": "https://www.example.com/", "score": 84,
  "counts": {"definite": 1, "needs_review": 0, "fix_verified": 1, "confirmed_by_second_check": 1},
  "owner_summary": ["..."], "pages": [{"url": "...", "status": "checked"}],
  "issues": [{"level": "definite", "wcag": ["2.4.4", "4.1.2"], "impact": "critical", "title": "Link has no name",
              "page": "...", "selector": "a", "count": 1, "confirmed_by_second_check": true,
              "fix": "Proven Fix (verified on a copy of this page): ...", "verified_after_fix": true, "code_after": ["<a ... aria-label=\"Media\">"]}],
  "more_issues": 0, "full_result": "GET /v1/wcag/scans/scan_...", "note": "..."
}
HTTP (remote) MCP
# Claude Code, remote (HTTP) MCP server:
claude mcp add --transport http tc-wcag https://api.tradingcheckup.com/mcp --header "Authorization: Bearer $TC_API_KEY"

# any MCP client over HTTP: POST JSON-RPC 2.0 to https://api.tradingcheckup.com/mcp with the same header, e.g.
curl -s -X POST https://api.tradingcheckup.com/mcp -H "Authorization: Bearer $TC_API_KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"check_accessibility","arguments":{"url":"https://www.example.com/"}}}'

CI example (GitHub Actions)

.github/workflows/accessibility.yml
# .github/workflows/accessibility.yml - fails the build only on CONFIRMED (definite) problems
name: accessibility
on: [pull_request]
jobs:
  wcag:
    runs-on: ubuntu-latest
    steps:
      - name: WCAG 2.2 AA check
        env:
          TC_API_KEY: ${{ secrets.TC_API_KEY }}
          PAGE: https://staging.example.com/
        run: |
          ID=$(curl -s -X POST https://api.tradingcheckup.com/v1/wcag/scans -H "Authorization: Bearer $TC_API_KEY" \
               -H "Content-Type: application/json" -H "Idempotency-Key: $GITHUB_RUN_ID" \
               -d "{\"url\": \"$PAGE\", \"pages\": 3}" | jq -r .id)
          for i in $(seq 1 60); do
            R=$(curl -s https://api.tradingcheckup.com/v1/wcag/scans/$ID -H "Authorization: Bearer $TC_API_KEY")
            S=$(echo "$R" | jq -r .status); [ "$S" = done ] || [ "$S" = failed ] && break; sleep 10
          done
          echo "$R" | jq '.owner_summary, [.issues[] | select(.level=="definite") | {wcag, title, selector, fix: .fix.text}]'
          [ "$(echo "$R" | jq .counts.definite)" = 0 ]

Python

scan.py
# needs the requests package:  pip install requests
import os, time, requests

BASE = "https://api.tradingcheckup.com"
H = {"Authorization": "Bearer " + os.environ["TC_API_KEY"]}

r = requests.post(BASE + "/v1/wcag/scans", headers=H, json={"url": "https://www.example.com/", "pages": 1}, timeout=30)
r.raise_for_status()
scan = r.json()

while scan["status"] in ("queued", "running"):
    time.sleep(scan.get("retry_after_seconds", 10))
    scan = requests.get(BASE + "/v1/wcag/scans/" + scan["id"], headers=H, timeout=30).json()

if scan["status"] != "done":
    raise SystemExit(scan.get("error"))
print("score", scan["score"], scan["counts"])
for i in scan["issues"]:
    if i["level"] == "definite":
        print(i["wcag"], i["impact"], i["title"], "->", i["selector"])
        print("   fix:", i["fix"]["text"][:120], "(verified)" if i["fix"]["verified_after_fix"] else "")

JavaScript (Node 18+)

scan.mjs
// Node 18+ (built-in fetch). Run: TC_API_KEY=tck_live_... node scan.mjs
const BASE = "https://api.tradingcheckup.com";
const H = { Authorization: `Bearer ${process.env.TC_API_KEY}`, "Content-Type": "application/json" };

let scan = await (await fetch(`${BASE}/v1/wcag/scans`, {
  method: "POST", headers: H, body: JSON.stringify({ url: "https://www.example.com/", pages: 1 }),
})).json();
if (scan.error) throw new Error(scan.error.message);

while (scan.status === "queued" || scan.status === "running") {
  await new Promise(r => setTimeout(r, (scan.retry_after_seconds || 10) * 1000));
  scan = await (await fetch(`${BASE}/v1/wcag/scans/${scan.id}`, { headers: H })).json();
}
console.log(scan.score, scan.counts);
for (const i of scan.issues.filter(i => i.level === "definite"))
  console.log(i.wcag.join(","), i.impact, i.title, i.selector, i.fix.verified_after_fix ? "(fix verified)" : "");

Safety and privacy

  • Only check sites you own or have permission to test.
  • We load pages like a normal visitor (one browser, polite pace, our own user agent), never fill in or send forms, never log in.
  • Private networks, loopback, cloud metadata addresses, other ports and non-web addresses are refused - also when a page redirects or loads assets from them.
  • Results are deleted after 30 days. Logs keep the route, status code and time, never your key.
  • Automated checks find many problems, not all. We never say a site is "compliant". The legal-risk text is general information, not legal advice.

Versions and status

The path has the major version (/v1). Every answer has a TC-API-Version header (date). New fields can be added at any time; we never remove or rename a field inside v1. Live status, queue length and average scan time: status page or GET /v1/status (no key needed).

Changelog: 2026-11-07 - v1 free beta: WCAG 2.2 AA scans, second-method confirmation, Proven Fix, PDF, webhooks, MCP.