Official SDKs for the a11yfy PDF accessibility remediation API. Upload a PDF, get back a PDF/UA-compliant, screen-reader-friendly document — with a verifiable compliance certificate.
Quickstart · How it works · Certificates · Webhooks · API docs
The European Accessibility Act (EAA) and US regulations like the ADA and Section 508 require digital documents to be accessible. Most PDFs aren't. a11yfy fixes them automatically:
- 🏷️ Full PDF/UA-1 tagging — headings, lists, tables, reading order, figures with AI alt text
- ✅ Machine-validated — every output is checked with veraPDF before you get it
- 📜 Compliance certificate — Ed25519-signed, publicly verifiable proof for audits
- 🔌 Built for your backend — drop PDF accessibility into your own document workflow with a few lines of code
- 🆓 Already compliant? Free. — compliant PDFs are detected up front and returned without consuming credits
| Package | Registry | Requirements | Source |
|---|---|---|---|
a11yfy |
PyPI | Python ≥ 3.10 | sdks/python |
@a11yfy/sdk |
npm | Node.js ≥ 20 | sdks/typescript |
Both are server-side SDKs — the API key is a secret, keep it out of browsers. Get your key at a11yfy.com → Settings → API keys.
pip install a11yfyfrom a11yfy import A11yfy
client = A11yfy() # reads A11YFY_API_KEY from the environment
result = client.remediate("document.pdf") # upload + process + wait, one call
print(result.before.issues, "→", result.after.issues) # e.g. 47 → 0
print(result.output_url) # the remediated, accessible PDF
print(result.certificate.verify_url) # public proof of compliancenpm install @a11yfy/sdkimport { A11yfyClient } from "@a11yfy/sdk";
const client = new A11yfyClient(); // reads A11YFY_API_KEY from the environment
const result = await client.remediate("document.pdf");
console.log(`${result.before?.issues} → ${result.after?.issues}`); // e.g. 47 → 0
console.log(result.output_url); // the remediated, accessible PDF
console.log(result.certificate?.verify_url); // public proof of complianceremediate() accepts a file path, raw bytes/Buffer, or a stream. Path inputs
are streamed — hashing and upload never load the whole file into memory, so
documents up to the 300 MB limit are fine. It throws a typed JobFailedError /
RemediationTimeoutError — a timed-out job keeps running server-side and can
still be polled.
your PDF ──▶ POST /v1/jobs ──▶ diagnostics ──▶ remediation ──▶ veraPDF check
│
result ◀── GET /v1/jobs/:id/result ◀── certificate issued ◀── PASS ─┘
The high-level remediate() wraps the full flow. The low-level clients expose
every endpoint:
job = client.jobs.create_job(file=("doc.pdf", pdf_bytes))
status = client.jobs.get_job(job.job_id) # pending → processing → done
result = client.jobs.get_job_result(job.job_id)
page = client.jobs.list_jobs(limit=50) # newest first, cursor pagination
certs = client.certificates.find_certificates(job_id=job.job_id)
balance = client.billing.get_balance()const job = await client.jobs.createJob({ file });
const status = await client.jobs.getJob({ id: job.job_id });
const result = await client.jobs.getJobResult({ id: job.job_id });
const page = await client.jobs.listJobs({ limit: 50 });
const certs = await client.certificates.findCertificates({ job_id: job.job_id });
const balance = await client.billing.getBalance();Organizations under a parent (agency / developer program) spend a delegated
credit allowance instead of their own balance — get_balance() then carries
delegated: true plus limit, limit_used and billing_org_name.
When a job can't be funded, the API responds 402 with a typed body. Check
its code before suggesting a top-up — a delegated client cannot buy
credits; only the parent organization can raise the limit:
from a11yfy.errors import PaymentRequiredError
try:
job = client.jobs.create_job(file=("doc.pdf", pdf_bytes))
except PaymentRequiredError as e:
if e.body.code == "delegated_limit_reached":
print(f"Allowance exhausted ({e.body.limit_used}/{e.body.limit}), "
f"managed by {e.body.billing_org_name}")
else: # insufficient_credits_api
print(f"Top up needed: {e.body.available}/{e.body.required} credits")import { A11yfy } from "@a11yfy/sdk";
try {
await client.jobs.createJob({ file });
} catch (err) {
if (err instanceof A11yfy.PaymentRequiredError) {
const body = err.body as A11yfy.InsufficientCreditsError;
if (body.code === "delegated_limit_reached") notifyParent(body.billing_org_name);
else promptTopUp();
}
}Parent organization keys can also pull their clients' consumption in one call — each item is annotated with the submitting organization:
usage = client.billing.get_usage(include="children")
for item in usage.items:
print(item.org_name, item.credits_used)Every remediated PDF that passes machine validation gets an immutable, Ed25519-signed certificate. Retrieve it any time — by job, or by the SHA-256 of the output file itself:
certs = client.certificates.find_certificates(output_sha256=sha256_of_pdf)
pdf = b"".join(client.certificates.download_certificate(certs.certificates[0].certificate_id))Anyone can verify a certificate without an API key at its verify_url
(https://a11yfy.com/en/verify/A11Y-2026-07-...) — hash match proves the
exact file was certified.
Skip polling: pass a webhook_url and verify the HMAC-signed callback with
the built-in helper (constant-time compare, replay protection). The signing
secret is returned once, in the job-creation response that first registers
the webhook_url (signing_secret — it stays visible in the web UI under
Settings → Organization → API keys):
from a11yfy import Webhook, WebhookVerificationError
event = Webhook.construct_event(raw_body, sig_header, secret)
if event.is_success:
download(event.output_url)import { Webhooks } from "@a11yfy/sdk";
const event = Webhooks.constructEvent(rawBody, sigHeader, secret);
if (event.status === "done") download(event.output_url!);Both SDKs are generated from the API's OpenAPI 3.1 spec with
Fern, plus a hand-written, .fernignore-protected
overlay per language (remediate(), webhook verification, typed errors).
npm install -g fern-api
./scripts/sync-openapi.sh # sync spec from the main repo + regenerate
cd sdks/python && uv sync && uv run pytest
cd sdks/typescript && npm install && npm run check && npm test && npm run buildReleases use tag-triggered Trusted Publishing
(OIDC — no registry tokens stored): bump the version, push py-v<x.y.z> or
js-v<x.y.z>.
- 📖 API reference — interactive OpenAPI docs
- 🔍 Certificate verification
- 🌐 a11yfy.com