Quick start
The whole workflow in a terminal on macOS or Linux: set your key, start a scan, keep its ID, then read the status and results. It uses jq to format JSON; there’s a Python alternative below.
- Create a key. In the Pallyfi app, open Account, Connections and create a key under Developer API. Copy it straight away: it’s shown once.
- Load the key without showing it.
read -rstakes the key without echoing it or saving it in your shell history. Paste the key, then press Enter: nothing appears, which is expected. - Start a scan and keep its ID. The request waits for the scan, usually a few seconds, and returns it.
jq -r .idtakes theidfield from the response and stores it inSCAN_ID. - Check the status and read the results using
$SCAN_ID.
Run each block on its own. They work in zsh, the macOS default, and bash.
read -rs PALLYFI_API_KEY && export PALLYFI_API_KEYSCAN_ID=$(curl -s -X POST https://app.pallyfi.com/api/v1/scans \
-H "Authorization: Bearer $PALLYFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}' | jq -r .id)
echo "Scan ID: $SCAN_ID"curl -s https://app.pallyfi.com/api/v1/scans/$SCAN_ID \
-H "Authorization: Bearer $PALLYFI_API_KEY" \
| jq '{status, score, failure_reason, summary}'curl -s https://app.pallyfi.com/api/v1/scans/$SCAN_ID/results \
-H "Authorization: Bearer $PALLYFI_API_KEY" \
| jq '.data[] | {rule_id, impact, level, wcag, element_count, help, help_url}'About SCAN_ID: it’s the id of the scan, a value like bad8f181-6e9c-42e1-9feb-90356b555d96 returned by POST /scans. Every example on this page uses $SCAN_ID, so after step 2 they work as written. In a new terminal, set it again with SCAN_ID="the-id-from-the-response".
Keep keys out of commands and screenshots. Don’t type a key into a command (it’s saved in your shell history), commit it, or share a screenshot or recording that shows it. The examples use $PALLYFI_API_KEY, so the key itself never appears. If a key was exposed, revoke it in Account, Connections and create a new one.
Readable output
The API returns compact JSON, which is the standard for APIs and what your code should read. To read it in a terminal, pipe it through a formatter.
| System | Install jq |
|---|---|
| macOS (Homebrew) | brew install jq |
| Ubuntu or Debian | sudo apt-get install jq |
| Windows | winget install jqlang.jq |
| GitHub Actions runners | Already installed |
curl -s https://app.pallyfi.com/api/v1/scans/$SCAN_ID \
-H "Authorization: Bearer $PALLYFI_API_KEY" | jq .No jq? Python 3, which macOS and most Linux systems include, can format and extract too.
curl -s https://app.pallyfi.com/api/v1/scans/$SCAN_ID \
-H "Authorization: Bearer $PALLYFI_API_KEY" | python3 -m json.toolSCAN_ID=$(curl -s -X POST https://app.pallyfi.com/api/v1/scans \
-H "Authorization: Bearer $PALLYFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}' \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])')The -s flag hides curl’s progress meter so only the JSON reaches the formatter. The Pallyfi CLI is in preview and not yet published.
Three ways to use results
Every scan has one set of results. Read them the way that suits the job.
| Way | Best for | Where |
|---|---|---|
| REST API | Integrations and automation: machine-readable JSON. | The endpoints on this page. |
| Pallyfi CLI | Terminals and CI logs: a readable summary, or JSON with --json. | In preview, not yet published. |
| Scan Explorer | Investigating visually: score, rule and element counts, each failed rule with its elements, and the raw JSON. | API scans in the Pallyfi app, also linked from Account, Connections. |
The Scan Explorer signs you in with your Pallyfi account, never an API key, and shows only your own account’s scans. It needs the same plans as the scan endpoints.
Authentication
Send your key as a bearer token on every request. Keys start with pf_live_ and act as the account that created them.
Authorization: Bearer pf_live_...- Create, name and revoke keys in Account, Connections. Up to 10 active keys per account.
- Pallyfi stores only a hash of each key. If you lose one, revoke it and create another.
- Revoking a key takes effect on the next request.
Scopes
Every key created today carries both scopes. A request without the scope it needs gets 403 insufficient_scope.
| Scope | Allows |
|---|---|
scans:write | Starting scans (POST /scans) |
scans:read | Reading scans and results (GET /scans/:id, GET /scans/:id/results) |
Start a scan
POST/api/v1/scans
Body: { "url": "https://..." }. Public http(s) pages on standard ports only. The request returns when the scan finishes, with 201 for a completed scan. If the page can’t be scanned the scan is returned with status: "failed" and a failure_reason.
curl -s -X POST https://app.pallyfi.com/api/v1/scans \
-H "Authorization: Bearer $PALLYFI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: release-2026-10-19-home" \
-d '{"url":"https://example.com"}' | jq .{
"id": "bad8f181-6e9c-42e1-9feb-90356b555d96",
"object": "scan",
"url": "https://example.com/",
"status": "completed",
"score": 88,
"summary": {
"passed": 13,
"failed": 4,
"needs_review": 1,
"by_level": [
{ "level": "A", "passed": 6, "failed": 0 },
{ "level": "AA", "passed": 4, "failed": 0 },
{ "level": "AAA", "passed": 2, "failed": 1 },
{ "level": "Best Practice", "passed": 1, "failed": 3 }
],
"note": "Automated testing checks only part of WCAG. A clean scan is not a conformance claim."
},
"failure_reason": null,
"created_at": "2026-10-19T05:24:09.842Z",
"completed_at": "2026-10-19T05:24:16.737Z",
"results_url": "/api/v1/scans/bad8f181-6e9c-42e1-9feb-90356b555d96/results"
}| Field | Meaning |
|---|---|
status | running, completed or failed. |
score | 0 to 100, the same score as Quick Scan. null unless completed. |
summary.passed, summary.failed | Counts of automated rules that passed and failed on the page (rules, not elements). |
summary.needs_review | Rules the engine couldn’t decide; a person needs to check them. |
summary.by_level | The same rule counts per WCAG level (A, AA, AAA) and Best Practice. |
failure_reason | When failed: timeout, access_blocked, not_found, unavailable, network, invalid or scanner_error. |
results_url | Path to this scan’s results, relative to https://app.pallyfi.com. |
How scans run
- The request waits.
POST /scansresponds when the scan has finished, usually within seconds and at most about 90 seconds. Set your client timeout to at least 120 seconds. - If your connection drops, the scan still finishes and is saved. Send the same request with the same
Idempotency-Keyto get it back; if it’s stillrunning, pollGET /scans/:idevery few seconds. - A scan never stays running. One that hasn’t finished after 5 minutes is reported as
failedwithfailure_reason: "timeout".
while true; do
STATUS=$(curl -s https://app.pallyfi.com/api/v1/scans/$SCAN_ID \
-H "Authorization: Bearer $PALLYFI_API_KEY" | jq -r .status)
[ "$STATUS" != "running" ] && break
sleep 5
done
echo "$STATUS"Scan status
GET/api/v1/scans/:id
Returns the same scan object. status is running, completed or failed. Only scans started by keys on your own account are visible.
curl -s https://app.pallyfi.com/api/v1/scans/$SCAN_ID \
-H "Authorization: Bearer $PALLYFI_API_KEY" | jq .Results
GET/api/v1/scans/:id/results
Every failed rule, one entry each: its WCAG level and tags, impact and help link. element_count is the full number of failing elements; elements holds up to 25 of them, returned_element_count says how many, and truncated is true when there were more. Rules aren’t paginated or cut off. Available once the scan has completed.
curl -s https://app.pallyfi.com/api/v1/scans/$SCAN_ID/results \
-H "Authorization: Bearer $PALLYFI_API_KEY" | jq .curl -s https://app.pallyfi.com/api/v1/scans/$SCAN_ID/results \
-H "Authorization: Bearer $PALLYFI_API_KEY" \
| jq '[.data[] | select(.impact == "critical" or .impact == "serious") | {rule_id, impact, element_count, help}]'{
"object": "list",
"scan_id": "bad8f181-6e9c-42e1-9feb-90356b555d96",
"data": [
{
"rule_id": "color-contrast-enhanced",
"level": "AAA",
"impact": "serious",
"help": "Elements must meet enhanced color contrast ratio thresholds",
"help_url": "https://dequeuniversity.com/rules/axe/4.13/color-contrast-enhanced",
"wcag": ["wcag2aaa", "wcag146"],
"element_count": 3,
"returned_element_count": 1,
"truncated": false,
"elements": [
{ "html": "<a href=\"https://iana.org/domains/example\">", "target": ["a"], "failure_summary": "Fix any of the following: ..." }
]
}
],
"has_truncated_elements": false,
"max_elements_per_rule": 25,
"note": "Automated testing checks only part of WCAG. A clean scan is not a conformance claim."
}Element html is the page’s own markup, returned as text. Escape it before showing it in a browser.
Automated testing checks only part of WCAG. A scan with no failures is not a conformance claim; combine it with manual testing.
Errors
Every error has the same shape, with a stable code to branch on and a message to show people. Codes don’t change within an API version; messages may be reworded.
{ "error": { "code": "rate_limited", "message": "This key can start 30 scans an hour. Try again in 600 seconds." } }| Status | Code | When | Retry? |
|---|---|---|---|
| 400 | invalid_request | The body isn't JSON, or the url field is missing. | No: fix the request. |
| 400 | invalid_url | The url isn't a public http(s) address on port 80 or 443, or it contains a username or password. | No: use a public address. |
| 401 | unauthorized | No key, a malformed key, or a key that was revoked. | No: check the key. |
| 403 | insufficient_scope | The key doesn't have the scope this endpoint needs. | No: use a key with the scope. |
| 403 | plan_required | The account's plan doesn't include scanning (Business Pro and Enterprise Pro do). Any plan can create keys. | No: change plan. |
| 404 | not_found | No scan with that id for this account. | No. |
| 409 | idempotency_conflict | This Idempotency-Key was already used for a different url. | No: use a new key. |
| 409 | scan_running | Results asked for before the scan finished. | Yes: poll the scan, then retry. |
| 409 | scan_failed | Results asked for on a scan that failed. See failure_reason on the scan. | No: start a new scan. |
| 429 | rate_limited | The key started 30 scans in the last hour. | Yes: after Retry-After seconds. |
| 429 | account_rate_limited | The account started 100 scans in the last hour, across all its keys. | Yes: after Retry-After seconds. |
| 429 | concurrency_limited | The account already has 5 scans running. | Yes: after Retry-After seconds. |
| 500 | internal_error | Something failed on Pallyfi's side. | Yes: with the same Idempotency-Key. |
| 503 | not_enabled | The Developer API isn't available for this workspace yet. | Later. |
Rate limits
- Each key can start 30 scans per rolling hour.
- Each account can start 100 scans per rolling hour, shared by all its keys. More keys don’t add allowance: four keys starting 30, 30, 30 and 10 scans use the account’s whole hour. Creating, revoking or replacing keys doesn’t reset it.
- An account can have 5 scans running at once, across all its keys.
- Every scan started counts, including ones that fail. Replays of an
Idempotency-Key, rejected requests and reading scans or results don’t. - “Rolling” means each start stops counting exactly an hour after it began, so allowance frees up gradually rather than all at once on the hour.
- Over any limit you get
429with aRetry-Afterheader: the seconds until a scan can start again. - Limits hold for simultaneous requests too: each start is checked and recorded together, so requests arriving at the same moment can’t go past them.
Scan-start responses, including 429s, tell you what’s left:
HTTP/1.1 429 Too Many Requests
Retry-After: 751
X-RateLimit-Limit-Key: 30
X-RateLimit-Remaining-Key: 12
X-RateLimit-Limit-Account: 100
X-RateLimit-Remaining-Account: 0
{ "error": { "code": "account_rate_limited", "message": "Your account can start 100 scans an hour across all its API keys. Try again in 751 seconds." } }Branch on the code: rate_limited (this key), account_rate_limited (the whole account) or concurrency_limited (too many running). Wait for Retry-After before trying again; sending more requests sooner doesn’t start scans sooner. You can see the account’s current usage in Pallyfi under Account, Connections, Developer API.
Idempotency
Send an Idempotency-Key header (up to 200 characters) when starting a scan. Use it so a retried CI step never scans twice.
- The same key and url returns the original scan, with
Idempotent-Replayed: true, whatever its outcome. A failed scan replays as failed: use a new key to try again. - The same key with a different url is refused with
409 idempotency_conflict; it never returns an unrelated scan. - Simultaneous requests with one key start one scan.
- An Idempotency-Key belongs to the API key that sent it and is kept as long as the scan is.
Versioning
Every response has a Pallyfi-Api-Version header with the date of the API version that served it, currently 2026-10-19. There is one version today; you can’t request another.
- Without notice: new endpoints, new optional request fields, new response fields, new error codes and reworded messages. Ignore fields you don’t know.
- Only in a new dated version: removing or renaming a field or error code, or changing what one means. Breaking changes will be announced on this page before they ship.
Plans
Scan endpoints come with Business Pro and Enterprise Pro, the same plans that include Quick Scan. The plan is checked on every request, so a change takes effect immediately. Any account can create keys.
Security
- Keep keys in a secret store (CI secrets, a password manager), never in source code, front-end bundles, commands, screenshots or screen recordings.
- Load a key into your terminal with
read -rs PALLYFI_API_KEYrather than typing it afterexport, so it isn’t saved in your shell history. - Use one key per integration so you can revoke one without breaking the others.
- Pallyfi only scans public pages. Private, loopback, link-local and cloud metadata addresses, internal host names and non-standard ports are refused, and every address the page itself loads, including redirects, is checked again when the scan runs.
- Keys are 40 random characters after
pf_live_, shown once and stored only as a hash. Create keys for a purpose and revoke ones you no longer use. - Scans and results are visible only to the account that owns the key.
Integration ideas
- Release checks. Scan key pages after each deploy and post the summary to your team.
- Content workflows. Scan a page when it’s published from your CMS.
- Dashboards. Pull scores and failure counts into your own reporting.
A Pallyfi CLI and GitHub Action built on this API are in preview and not yet published. This page will link to them when they are.
Troubleshooting
- 401 unauthorized
- Check the header is exactly
Authorization: Bearer pf_live_...and the key hasn’t been revoked. - 403 plan_required
- The account’s plan doesn’t include scanning. Business Pro and Enterprise Pro do. Creating a key doesn’t grant scanning by itself.
- 409 scan_running
- Wait for the scan to finish: poll
GET /scans/:id, then fetch results. - The request timed out on my side
- Raise your client timeout to 120 seconds, and retry with the same
Idempotency-Keyto pick up the scan instead of starting another. - A scan came back failed
- The page blocked automated checks, timed out or couldn’t be reached.
failure_reasonsays which. - 400 invalid_url on a valid-looking URL
- Internal addresses, private networks and ports other than 80 and 443 aren’t scanned.
Still stuck? Contact us.
