Public API

Sitejar API

Run compliance scans from a script, a cron job, or your CI pipeline - and fail the build before a violation ships. Every endpoint below runs the exact same scan pipeline as the dashboard. This is the same API we run regularly against sitejar.app itself - see /transparency.

Authentication

Generate an API key from your account (POST /v1/apikeys, using the JWT you get from logging in), then send it as a bearer token on every request: Authorization: Bearer sj_live_.... Keys are shown in full exactly once, at creation - Sitejar only ever stores a one-way hash afterward.

Create a key
curl -X POST https://api.sitejar.app/v1/apikeys \
  -H "Authorization: Bearer <your JWT access token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"CI pipeline"}'

# {"id":"...", "name":"CI pipeline", "key":"sj_live_...", "createdAt":"..."}
# The "key" field is shown exactly once - store it as a secret now.

Endpoints

MethodPathDescription
POST/v1/apikeysCreate a new API key (JWT auth). Body: { name }.
GET/v1/apikeysList your API keys, masked - a short prefix only, never the full key.
DELETE/v1/apikeys/{id}Revoke an API key immediately.
POST/v1/public/scansStart a scan (API key auth). Body: { url }.
GET/v1/public/scans/{id}/reportGet a scan's status, scores, and findings. Optional ?fail_below=&dimension= - see "Pass/fail gating" below.
GET/v1/public/scans/{id}/report.mdThe report as a GitHub/Notion-ready Markdown task list - one checkbox per deduped element group. Paid plans only; see "Exporting reports" below.
GET/v1/public/scans/{id}/report.csvThe report as CSV (one row per deduped element group; RFC 4180, UTF-8 with BOM so Excel opens it correctly). Paid plans only.
GET/v1/public/scans/{id}/report.jsonThe report as a JSON export envelope: {schemaVersion, exportedAt, scanId, requestedHost, scores, findings}, where findings reuse the report endpoint's exact finding shape (including elementContext). Paid plans only.

Starting a scan

Request
curl -X POST https://api.sitejar.app/v1/public/scans \
  -H "Authorization: Bearer sj_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-site.com"}'

# {"scanId":"5f2b1e3a-..."}

Scans run asynchronously - poll the report endpoint until status is done (or failed).

Pass/fail gating

Add ?fail_below=<0-100>&dimension=<name|overall> to the report endpoint and the response gains a server-computed "pass" boolean - true when that dimension's score (or the mean of all six, for overall) is at or above the threshold. Both params are required together; dimension must be overall or one of accessibility, security, privacy, darkPattern, quality, seo. This computes the same pass/fail rule server-side so every caller applies it identically, instead of each CI script re-implementing its own score comparison.

Poll for the report
curl https://api.sitejar.app/v1/public/scans/5f2b1e3a-.../report?fail_below=90&dimension=accessibility \
  -H "Authorization: Bearer sj_live_..."

# {"scanId":"5f2b1e3a-...", "status":"done", "scores":{...}, "pass": true}
# "pass" only appears when both params are given - it is omitted otherwise.

Exporting reports

Once a scan is done, the same report is available as Markdown (a task list you can paste into a GitHub issue or Notion page), CSV (for Sheets/Airtable), and JSON - swap the extension on the report path. Counts always agree across formats, and with the dashboard and PDF. Available on every paid plan; a free key gets 403 {"error":"export_requires_upgrade"}.

Export as Markdown
curl https://api.sitejar.app/v1/public/scans/5f2b1e3a-.../report.md \
  -H "Authorization: Bearer sj_live_..."

# A GitHub/Notion-ready Markdown task list (text/markdown; Content-Disposition names the file).
# Swap the extension for the other formats: report.csv (RFC 4180, UTF-8 BOM) or report.json
# ({schemaVersion, exportedAt, scanId, requestedHost, scores, findings}). Paid plans only -
# a free key gets 403 {"error":"export_requires_upgrade"}.

Rate limits

Fixed per-hour limits, keyed to your plan, and shared across every /v1/public/* call - polling the report endpoint spends quota the same way starting a scan does. Every response includes X-RateLimit-Limit/X-RateLimit-Remaining headers; going over returns 429 with a Retry-After header telling you how many seconds until the window resets.

PlanRequests / hour
Free10
Pro100
Business500
Agency2,000

Errors

Exact response bodies, taken straight from the API - nothing here is illustrative.

StatusBodyWhen
400{"error":"'http://localhost:9999' resolves to 'localhost', which cannot be scanned (localhost/private/loopback addresses are not allowed)."}The scan target's URL is malformed, uses an unsupported scheme, or resolves to a localhost/private/loopback address (the SSRF guard).
401(empty body)The bearer token is missing, malformed, doesn't start with sj_live_, or doesn't match any active key.
403{"error":"public_api_disabled","message":"This feature is not currently enabled."}The public API isn't enabled for your account/environment yet.
403{"error":"plan_lacks_api_access","message":"Your plan does not include API access. Upgrade to Pro or higher."}Your plan's hourly rate limit is 0 (Free-tier accounts that haven't upgraded).
403{"error":"export_requires_upgrade","message":"Report export (Markdown/CSV/JSON) requires a Pro plan or higher - upgrade to export this scan's report."}A report.md/.csv/.json export was requested without a paid plan.
429{"error":"rate_limit_exceeded","message":"Rate limit exceeded - 10 requests/hour for your plan."}Over your plan's hourly limit. Also sets Retry-After (seconds until the window resets) and X-RateLimit-Limit/X-RateLimit-Remaining.
400{"error":"fail_below and dimension must be provided together."}Only one of the two pass/fail query params was given.
400{"error":"fail_below must be between 0 and 100."}fail_below is outside 0-100.
404(empty body)The scan id doesn't exist, or belongs to a different account - both look identical, on purpose.

Gate your CI pipeline

The pattern most teams start with: scan a staging deploy on every pull request, and fail the build if the accessibility score drops below 90 - using the pass field from "Pass/fail gating" above so the workflow doesn't need to parse scores itself.

.github/workflows/compliance.yml
name: Compliance gate
on: [pull_request]

jobs:
  sitejar-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Run Sitejar compliance scan
        env:
          SITEJAR_API_KEY: ${{ secrets.SITEJAR_API_KEY }}
          TARGET_URL: https://staging.your-site.com
        run: |
          scan_id=$(curl -s -X POST https://api.sitejar.app/v1/public/scans \
            -H "Authorization: Bearer $SITEJAR_API_KEY" \
            -H "Content-Type: application/json" \
            -d "{\"url\": \"$TARGET_URL\"}" | jq -r '.scanId')

          echo "Scan $scan_id queued - polling for completion..."

          status=""
          for _ in $(seq 1 30); do
            report=$(curl -s "https://api.sitejar.app/v1/public/scans/$scan_id/report?fail_below=90&dimension=accessibility" \
              -H "Authorization: Bearer $SITEJAR_API_KEY")
            status=$(echo "$report" | jq -r '.status')
            [ "$status" = "done" ] && break
            [ "$status" = "failed" ] && { echo "::error::Scan failed"; exit 1; }
            sleep 10
          done

          # Without this check, a scan that's still "queued" or "running"
          # after all 30 polls falls through silently - "$report" is stale
          # and "pass" isn't present yet, so the step would pass the build
          # by accident instead of failing it.
          if [ "$status" != "done" ]; then
            echo "::error::Scan timed out after 5 minutes - it never reached status=done"
            exit 1
          fi

          pass=$(echo "$report" | jq -r '.pass')
          echo "Accessibility gate (fail_below=90): pass=$pass"

          if [ "$pass" != "true" ]; then
            echo "::error::Accessibility score is below the required 90 - failing the build."
            exit 1
          fi