Legible
Scan APIChrome extension

Scan API

The scan behind every Legible result is a public API. Send a domain, get back every check with the URL we fetched, its HTTP status and what we found. No account and no API key. The OpenAPI spec describes the same endpoint, and How we check explains each check and how the score works.

Quickstart

  1. In the browser: type your domain on the home page.
  2. From a terminal: npx legible-cli <domain>, with your domain.
  3. In CI: add --min-score and a number, and the command exits 1 below it. Or use the GitHub Actions job below.
  4. From your editor: add Legible's MCP server, then ask your agent to scan your site.

Request

Send a POST to /api/scan with a JSON body: either a domain, or the id of a company on the email-API board, which scans it the way the board does.

curl -X POST https://leaderfile.vercel.app/api/scan \
  -H "content-type: application/json" \
  -d '{"domain": "resend.com"}'

To check a fix you just shipped, add "rescan": true: it reads the site again instead of returning a cached result.

GET /api/checks lists every check, with a real example from whoever does it best on our boards. For a coding agent, there's a skill that scans a live site and writes its own version of each file.

From your terminal

The same scan as a command, with nothing to install: npx runs it on Node 18.3 or newer. It sends only the domains you name.

Exit codes: 0 fine, 1 below --min-score, 2 we couldn't run the scan. npx legible-cli --help prints all of this. If Windows PowerShell blocks npx, type npx.cmd instead.

A real run on 9 Nov 2026, with version 1.1.0, as it printed:

$ npx legible-cli resend.com

resend.com: 96 out of 100 (scoring v2). 29 of 31 checks pass.

Fails our check. It doesn't prove the thing isn't there, only that our check didn't pass on what we read:
  Errors have a schema
    Real example, Mailtrap: https://raw.githubusercontent.com/mailtrap/mailtrap-openapi/main/specs/email-sending-transactional.openapi.yml
  Tools for browser agents
    Don't do it for the score. It's still a proposed standard, and almost nobody on our boards passes our check, though Bird, Postmark and CrewAI load it from a script our check doesn't read. Build it only if agents need to operate forms on your site.

This reads public files. It isn't a test of what an agent can do with your product.
Full result: https://leaderfile.vercel.app/scan?domain=resend.com&via=cli

Legible's MCP server

Legible's MCP server lets the agent in your editor scan a site, list our checks and read our boards, with the same results as this site. It's at https://leaderfile.vercel.app/mcp, over Streamable HTTP, with no account and no API key. Its tools are scan_site, get_checks and get_board.

In Claude Code:

claude mcp add --transport http legible https://leaderfile.vercel.app/mcp

In Cursor: Add to Cursor, or add it to .cursor/mcp.json:

{
  "mcpServers": {
    "legible": {
      "url": "https://leaderfile.vercel.app/mcp"
    }
  }
}

A scan from your editor counts towards the same limit as one here: 50 live scans per 10 minutes from one network.

Use it in CI

A GitHub Actions job that fails when your score drops below the number you pick. A score we had to withhold or a refused rescan also fails it, because the score comes back as null. For a ready-made job that names the check that changed and watches your rivals, and only warns on anything we couldn't read, open "Track it weekly" at the foot of your result.

name: Agent readiness
on:
  schedule:
    - cron: "0 9 * * 1"
  workflow_dispatch:
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: Fail if the score drops below 80
        run: |
          r=$(curl -sS -X POST https://leaderfile.vercel.app/api/scan \
            -H "content-type: application/json" \
            -d '{"domain": "your-domain.com", "rescan": true}')
          version=$(echo "$r" | jq -r '.scoringVersion // "1"')
          if [ "$version" = "2" ]; then score=$(echo "$r" | jq -r '.v2.summary.score'); else score=$(echo "$r" | jq -r '.summary.score'); fi
          v1=$(echo "$r" | jq -r '.summary.score')
          until=$(echo "$r" | jq -r '.v2.legacyUntil // empty')
          echo "Score: $score (scoring v$version)"
          if [ "$score" != "null" ] && [ "$score" -lt 80 ] && [ "$v1" != "null" ] && [ "$v1" -ge 80 ] && [ -n "$until" ] && [ ! "$(date -u +%F)" \> "$until" ]; then
            echo "::warning::Only our scoring change took this below 80. It warns, not fails, until $until."
            exit 0
          fi
          [ "$score" != "null" ] && [ "$score" -ge 80 ]

This job is stricter than the CLI's --min-score when we can't tell:

To hear about changes in Slack, use the weekly job from "Track it weekly" at the foot of your result instead. It watches the checks your site passes today, and your rivals' if you compared them:

  1. In Slack, create an incoming webhook for the channel you want.
  2. In your GitHub repo, open Settings, then Secrets and variables, then Actions, and add a repository secret named SLACK_WEBHOOK_URL with the webhook's address.
  3. Copy the job from your result into .github/workflows/legible.yml.

It posts one message a run, only when something changed, with a link back to each result. Nothing else is sent to us.

Response

A 200 with the result. reachable is false when the domain couldn't be reached, with the reason in detail. Otherwise scoringVersion is "2": v2.results holds one entry per check, and v2.summary.score is the score the site shows, null when we could check too little to score. The top-level results and summary are the first scoring's, kept for older CI jobs; from v2.legacyUntil, summary.score carries the new score too.

{
  "reachable": true,
  "host": "resend.com",
  "scannedAt": "2026-11-09T19:03:28.810Z",
  "scoringVersion": "2",
  "summary": { "score": 96, "withheld": false },
  "v2": {
    "summary": {
      "score": 96,
      "withheld": false,
      "steps": [{ "name": "Find", "score": 100 }, { "name": "Read", "score": 100 }, { "name": "Act", "score": 90 }]
    },
    "legacyUntil": "2026-12-09",
    "results": [
      {
        "id": "AR-READ-07",
        "status": "pass",
        "evidence": [{ "url": "https://resend.com/llms.txt", "httpStatus": 200, "note": "text file with markdown links" }]
      }
    ]
  },
  "cached": false
}

Trimmed: a real result has every check and more summary fields.

Limits

Live scans are limited to 50 per 10 minutes from one network, and a result is cached for up to 10 minutes (1 minute when a file was slow or busy), so asking again soon returns the same scan with cached set to true, which doesn't count towards the limit. A scan stops after 27 seconds; a check still waiting by then shows as couldn't check.

A rescan counts as a live scan, and each domain gets one rescan a minute, whoever asks.

Errors

Errors come back as JSON with an error message: 400 for a body that isn't JSON, an invalid domain or an unknown board company; 429 when your network has run its scans or a domain its rescans, with a Retry-After header; 503 when scanning is switched off or the rate limit couldn't be checked, so no scan ran.

What changed