Web accessibility has a uniquely bad failure mode: nothing breaks visibly. A developer ships a redesign that drops alt text from product images, removes a form label, or kills color contrast on the new button — and the site looks fine to everyone on the team. The first signal that something went wrong is often a demand letter citing ADA Title III or Section 1557, months later.
The fix is the same one we apply to every other invisible regression: monitor it continuously and alert on change. This post builds an automated WCAG accessibility monitor in Python on top of the SEO Score API, whose every audit includes an accessibility category scored against common WCAG checks — alt text, form labels, heading structure, language attributes, contrast signals, and ARIA usage. For a full WCAG 2.1 AA scan beyond that subset, point the same monitor at the dedicated accessibility audit API.
What the audit gives you for accessibility
Every /audit response includes an accessibility category alongside SEO and performance. The Python SDK returns it directly:
from seoscoreapi import audit
result = audit("https://example.com", API_KEY)
acc = result["audit"]["accessibility"]
print(acc["score"]) # 0–100 accessibility score
for check in acc["checks"]:
if check["status"] != "pass":
print(f" ✗ {check['name']}: {check.get('recommendation', '')}")
That's the raw material. Each check carries a status (pass, warn, fail), the offending value, and a recommendation for fixing it. The monitor below watches the category score and flags the specific checks that regressed.
The monitor
pip install seoscoreapi. This script audits a list of pages, compares each accessibility score to the last run, and prints (or emails) anything that dropped:
import json
import os
from pathlib import Path
from seoscoreapi import audit
API_KEY = os.environ["SEO_KEY"]
PAGES = [
"https://example.com/",
"https://example.com/contact",
"https://example.com/appointments",
"https://example.com/apply",
]
STATE = Path("a11y_state.json")
THRESHOLD = 90 # WCAG-serious sites should hold a high accessibility floor
def load_state() -> dict:
return json.loads(STATE.read_text()) if STATE.exists() else {}
def check_page(url: str, last: float | None) -> dict | None:
result = audit(url, API_KEY)
acc = result["audit"]["accessibility"]
score = acc["score"]
failures = [
{"name": c["name"], "fix": c.get("recommendation", "")}
for c in acc["checks"]
if c["status"] == "fail"
]
alert = None
if score < THRESHOLD:
alert = f"{url} accessibility score {score} is below floor {THRESHOLD}"
elif last is not None and score < last - 3:
alert = f"{url} accessibility DROPPED {last} → {score}"
return {"url": url, "score": score, "failures": failures, "alert": alert}
def main():
state = load_state()
alerts = []
for url in PAGES:
report = check_page(url, state.get(url))
state[url] = report["score"]
if report["alert"]:
alerts.append(report)
print(f"⚠️ {report['alert']}")
for f in report["failures"]:
print(f" ✗ {f['name']}: {f['fix']}")
else:
print(f"✅ {report['url']}: {report['score']}")
STATE.write_text(json.dumps(state, indent=2))
if alerts:
# send_alert_email(alerts) # wire to your mailer / Slack / PagerDuty
raise SystemExit(1) # non-zero exit fails a CI job
if __name__ == "__main__":
main()
The a11y_state.json file is the memory — it's how the monitor knows a score dropped versus was always low. Commit it to the repo or persist it wherever your job runs.
Why monitor instead of one-time audit?
Because accessibility isn't a project you finish — it's a state you maintain. You can pass a full WCAG audit on Monday and fail it on Friday because a marketing tweak removed a label. A one-time audit tells you where you stand today; a monitor tells you the moment you slip. For sites under legal obligation — healthcare under Section 1557, financial services, government contractors — that continuous proof of diligence matters if a complaint ever lands. The multi-location healthcare accessibility guide covers why this is acute when you're maintaining dozens of near-identical pages.
Two ways to run it on a schedule
As a cron job. The simplest setup: a crontab line that runs the script every morning and emails on a non-zero exit. Fifteen minutes of setup, no infrastructure.
As a CI gate. Drop the script into your CI/CD pipeline and the raise SystemExit(1) on a regression fails the build — so an accessibility drop blocks the merge, exactly like a failing test. This is the strongest version: accessibility regressions never reach production because the pipeline won't let them. It's the accessibility sibling of pre-deploy SEO gates.
Should the threshold be a hard floor or a delta?
Both, which is why the script checks both. A hard floor (score < THRESHOLD) catches pages that were never compliant. A delta check (score < last - 3) catches regressions on pages that were fine — the more dangerous case, because those are the ones nobody's watching. A small tolerance band (the - 3) keeps you from alerting on audit-to-audit noise while still catching real drops. Tune the floor to your obligation: a practice website under Section 1557 should hold a higher floor than a marketing microsite.
How many audits does monitoring cost?
Four pages checked daily is ~120 audits/month — inside the Starter plan ($5/mo, 200 audits). A 50-page healthcare site checked daily is ~1,500/month, which lands on Pro ($39/mo, 5,000 audits) with headroom to spare. The audit-50-location healthcare walkthrough works through exactly this kind of multi-page volume math.
Is an automated score the same as a full WCAG audit?
No, and it's important to be honest about this. Automated tooling — ours included — reliably catches the machine-detectable WCAG failures: missing alt text, absent form labels, missing language attributes, structural and contrast issues. It does not replace manual testing with a screen reader or the judgment of an accessibility expert, which catch the things only a human can (is this alt text meaningful? does the focus order make sense?). What automated monitoring does is hold the line between expert audits — it ensures the issues you paid to fix don't silently come back. Use it as the continuous floor, not the ceiling.
The bottom line
Accessibility regressions are invisible, expensive, and entirely preventable with the same monitoring discipline you already apply to uptime and errors. A 60-line script and a daily cron turn "we hope the site is still accessible" into "we'll know within 24 hours if it isn't." For organizations with a legal exposure, that's not a nice-to-have — it's the cheapest insurance you'll buy this year.