Documentation

How Altpass works

Everything the checker looks at, how the score is calculated, what we send to the AI model, and how to use the API.

The checker

Altpass fetches the page you enter as AltpassBot (after checking robots.txt), follows up to three redirects, reads up to 3 MB of HTML, and parses it in a single streaming pass. It doesn’t run JavaScript, so it sees the HTML your server sends.

It records every:

  • <img> — src (or data-src/lazy-load attributes, or the largest srcset candidate), alt presence and value, role, aria-hidden, aria-label/aria-labelledby, title, width/height, loading, and whether it sits inside a link or button (with that control’s address and text);
  • <input type="image"> image buttons;
  • <svg role="img"> without a <title> or label, and links/buttons whose only content is an unlabeled SVG icon;
  • <area> links in image maps.

Images inside <template> or <noscript>, or hidden with hidden/display:none, are skipped because assistive technology doesn’t encounter them. Relative URLs are resolved against the page (and any <base>).

Verdicts

VerdictWhen
Link or button with no nameThe image (or SVG icon) is the only content of a link, button, image button or image-map area and nothing gives it a name.
Missing altNo alt attribute and no ARIA label. Includes <svg role="img"> without a title.
Empty alt — looks meaningfulalt="" on an image that is large (about 250×150 or bigger), inside main content or a figure, or named like a photo — and not a small icon or a decorative-looking file. A prompt to double-check, not an error.
Weak altA file name (“IMG_2041.jpg”, “hero_banner_v2”), a URL, a generic word (“image”, “photo”, “banner”, a lone “logo”), placeholder text, “image of…”, over 150 characters, identical to the title attribute or page title, or a duplicate of the adjacent link text, caption or surrounding text.
Has alt textPasses all of the above. Accuracy still needs a human.
Decorative (correct)alt="" on a small or decorative-looking image, aria-hidden="true", role="presentation"/"none", or an empty-alt image inside a link that already has text.

How the score works

Each image earns points: 1 for “Has alt text” or “Decorative”, 0.5 for “Weak” or “Empty — looks meaningful”, and 0 for “Missing” or “Link/button with no name”. Link/button failures count twice in the total because they block navigation, not just understanding.

score = 100 × points ÷ (images + extra weight for link failures), rounded. 90+ is “Excellent”, 75+ “Good”, 50+ “Needs work”, below 50 “Failing”. A page with no images has no score.

The score measures what automated checks can see. A perfect score doesn’t prove your alt text is accurate, or that your site is accessible.

AI alt text

When you press Write alt text, the Worker fetches the image (up to 5 MB; the file type is verified from its first bytes, not its extension) and sends it with this context to mistral-small-3.1-24b-instruct on Cloudflare Workers AI, falling back to llama-4-scout-17b:

  • page title and URL, nearest heading, about 200 characters of text before and after the image, and any caption;
  • if the image is inside a link or button: the destination and whether the control has other text;
  • the current alt, the image’s pixel size and file name;
  • your preferences: language, maximum length (default 125 characters), tone, and brand/product names to use when they match.

The model first classifies the image and decides whether it is decorative, then writes the alt. The response includes alt, decorative (with a reason), long_description for charts, diagrams and infographics, image_type, confidence and notes — for example “Contains text — verify the wording and spelling.” Outputs never start with “Image of” or “Picture of”; over-long answers are shortened.

Formats: JPEG, PNG, WebP and GIF (first frame). SVG, AVIF and HEIC return “unsupported format — describe manually” in this version; no credit is used.

Quality: how we tested it

We ran 49 real images through the live API — product photos, people, charts and infographics, logos, icons, screenshots, decorative flourishes, linked banners, plus French and German output — and reviewed every result against the image. We changed the prompt twice based on what we saw.

  • 37 good as written, 12 acceptable with a light edit, 0 wrong on the final prompt.
  • Decorative images returned as alt="": 5 of 6 (the first prompt managed 0 of 6).
  • Every image that was the only content of a link came back as a destination: “Tartine Bakery home”, “Shopping cart”, “Follow Acme Bakery on Twitter”.
  • Weak spots: descriptions of people can be generic (“Person frosting a cake”), and the model occasionally mentions apparent age despite instructions not to. Review before approving.

Site audits (paid plans)

  1. Enter a domain, a page, or a sitemap URL in the dashboard.
  2. Altpass reads robots.txt, then the sitemaps it lists, /sitemap.xml and /sitemap_index.xml (including sitemap indexes and .xml.gz). Without a sitemap it crawls same-site links breadth-first from your start page. Discovery stops at your plan’s page limit and skips anything robots.txt disallows.
  3. Your browser drives the audit: each step checks about five pages and saves the results, so you can watch progress, pause, close the tab and resume later.
  4. Results are grouped by unique image. Image URLs are normalised — protocol, www., size suffixes like -300x200 or _600x, and resizing parameters such as ?w= or ?v= are ignored — so the same logo across 200 pages is one row.
  5. Select images and generate suggestions in bulk (one credit each), edit them, mark them decorative or approved, and save.
  6. Export CSV or JSON: page_url, image_url, current_alt, issue, suggested_alt, decorative, approved — one row per image on each page.

The runtime fix script

Each site in your dashboard has a script tag:

<script src="https://altpass-api.hrishikesh.workers.dev/v1/fix.js?site=SITE_ID" defer></script>

It contains your approved alt texts (decorative images map to alt=""). On page load, and whenever images are added or change source (it uses a MutationObserver, so lazy-loaded and infinite-scroll images are covered), it sets the alt on matching images whose alt is missing, empty or weak. It never replaces alt text that passes the checks. It only runs on your site’s domain (and localhost for testing), is cached for an hour, and switches itself off if your license lapses.

Be clear-eyed about it. Runtime injection helps screen-reader users immediately, but it isn’t the same as fixing the source: some crawlers and tools read the original HTML, and the fix depends on our script loading. Use the CSV export to put the same alt text into your CMS, then remove the script. It doesn’t make a site compliant and we don’t claim SEO gains.

Changes you approve reach the script within about an hour (edge cache). Open the browser console and run AltpassFix.applied() to see how many images it fixed on the current page.

REST API (Pro and Agency)

Base URL https://altpass-api.hrishikesh.workers.dev. Authenticate with your license key: Authorization: Bearer YOUR_LICENSE_KEY. Every JSON response includes a quota object, and POST /v1/alt also sets an X-Credits-Remaining header. Requests are rate-limited to 120 per minute.

Generate alt text

POST /v1/alt — uses one credit on success.

curl -X POST https://altpass-api.hrishikesh.workers.dev/v1/alt \
  -H "Authorization: Bearer $ALTPASS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://example.com/img/summer-sale.jpg",
    "page_url": "https://example.com/",
    "lang": "en",
    "max_len": 125
  }'

With only page_url, Altpass fetches that page, finds the image and uses its context automatically. To supply context yourself (no page fetch), pass context:

curl -X POST https://altpass-api.hrishikesh.workers.dev/v1/alt \
  -H "Authorization: Bearer $ALTPASS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://example.com/img/q3.png",
    "context": {
      "page_title": "Q3 results",
      "heading": "Revenue by region",
      "text_before": "EMEA grew fastest this quarter.",
      "link_href": "", "link_text": "", "in_button": false,
      "current_alt": "chart"
    },
    "lang": "de", "max_len": 150, "tone": "formal", "keywords": "Acme"
  }'

Response:

{
  "image_url": "https://example.com/img/q3.png",
  "alt": "…",
  "decorative": false,
  "decorative_reason": "",
  "long_description": "…",          // charts, diagrams, infographics; otherwise null
  "image_type": "chart",
  "confidence": "high",               // high | medium | low
  "notes": ["Contains text — verify the wording and spelling."],
  "chars": 74,
  "model": "mistral-small-3.1-24b-instruct",
  "lang": "de",
  "image": { "width": 1200, "height": 800, "format": "png" },
  "context_used": { "page_title": true, "heading": true, "nearby_text": true, "link": false, "auto_context": false },
  "quota": { "plan": "pro", "credits_limit": 3000, "credits_used": 41, "credits_remaining": 2959, "resets_on": "2026-11-01" }
}

Options: lang — one of en es fr de it pt nl sv da nb fi pl cs ro el tr uk ja ko zh hi ar; max_len 40–250 (default 125); tone — neutral, friendly, formal or marketing; keywords — brand or product names (used only when they match the image).

Check a page

POST /v1/check with {"url": "https://example.com/page"} — free, no key needed, returns score, grade, counts and items (each with status, reasons, fix and context).

curl -X POST https://altpass-api.hrishikesh.workers.dev/v1/check -H "Content-Type: application/json" -d '{"url":"https://example.com/"}'

Account and audits

  • GET /v1/me — plan, limits, quota, sites.
  • POST /v1/audit {"url", "max_pages"} → audit; then call POST /v1/audit/{id}/next until status is done.
  • GET /v1/audit/{id}/images?view=unique&issues=1, GET /v1/audit/{id}/export.csv, …/export.json.
  • POST /v1/sites/{site_id}/alts {"items":[{"image_url","alt","decorative","approved"}]} — save approved alt texts for the fix script.

Errors

Errors return JSON {"error": "code", "message": "…"}. Common codes: bad_url (400), blocked (private or local address), robots (disallowed by robots.txt), blocked_by_site (the site refused us), invalid_license (401), api_plan_required, no_credits (402), unsupported_format, rate_limited (429), ai_capacity (503 — try again later; no credit used).

Limits

  • Pages: 3 MB of HTML, 10-second timeout, 3 redirects, 600 images per page. Images: 5 MB.
  • Private, local and reserved network addresses are refused.
  • The free checker is rate-limited per network to keep it fair; free generation is 25 alt texts a month per browser, with a per-network ceiling.