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.
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
| Method | Path | Description |
|---|---|---|
| POST | /v1/apikeys | Create a new API key (JWT auth). Body: { name }. |
| GET | /v1/apikeys | List your API keys, masked - a short prefix only, never the full key. |
| DELETE | /v1/apikeys/{id} | Revoke an API key immediately. |
| POST | /v1/public/scans | Start a scan (API key auth). Body: { url }. |
| GET | /v1/public/scans/{id}/report | Get a scan's status, scores, and findings. Optional ?fail_below=&dimension= - see "Pass/fail gating" below. |
| GET | /v1/public/scans/{id}/report.md | The 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.csv | The 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.json | The 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
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.
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"}.
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.
| Plan | Requests / hour |
|---|---|
| Free | 10 |
| Pro | 100 |
| Business | 500 |
| Agency | 2,000 |
Errors
Exact response bodies, taken straight from the API - nothing here is illustrative.
| Status | Body | When |
|---|---|---|
| 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.
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