Developer API
What it is
A REST API over the same account you see on the dashboard. A script, a CI job or your own service can read how each site scored, ask what to fix next and who usually does the fixing, add and manage sites, start a scan, and be told when it finishes. It never does more than you can do on the website.
Every address starts with https://readinessindex.io/api/v1. Every answer is JSON
unless you ask for something else. The whole thing is described in one machine-readable
file, /api/v1/openapi.json (OpenAPI 3.1, no key
needed), so you can generate a client in the language you use. We don’t ship SDKs.
API access comes with the Developer and Agency tiers, identically. There is nothing extra to buy and nothing extra to turn on. Some calls, such as clients, belong to the Agency tier because the product itself does; a Developer key is told so plainly.
Get a key
Sign in, open your profile, and make an API key there. We show it once, so copy it right away; we keep only a fingerprint. The same key opens the MCP endpoint and the API, and shares one allowance of calls between them.
A key has a scope. Read can call only what reads. Write can also change things. A key can never do more than the person who made it, and it stops working the day that person is removed. Give automation the least access it needs, and revoke a key the day you stop using it. Keys can’t be made through the API itself, on purpose: a key that can make keys turns one leaked secret into a permanent one.
Make your first call
Send the key as a bearer token. This asks who the key belongs to and what it may do:
export KEY=air_xxxxxxxx_...
curl -s https://readinessindex.io/api/v1/me -H "Authorization: Bearer $KEY"
Then add a site, scan it, and read what to fix. Adding a site starts nothing. A scan spends pages
from your plan, so send an Idempotency-Key and a retried job never crawls twice.
curl -s -X POST https://readinessindex.io/api/v1/sites -H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: add-example-0001" -d '{"host": "example.com"}'
curl -s -X POST https://readinessindex.io/api/v1/sites/SITE_ID/scans \
-H "Authorization: Bearer $KEY" -H "Idempotency-Key: scan-example-0001"
curl -s https://readinessindex.io/api/v1/sites/SITE_ID/fixes -H "Authorization: Bearer $KEY"
The scan answers 202 and says where to follow it. Poll
GET /api/v1/scans/SCAN_ID, or register a webhook and be told.
How requests work
- Every answer is an object with
data. A list addsnext_cursorandtotal. A change addschanged, what moved, which is empty when the request was already true. There are no bare arrays. - What isn’t yours is a 404, the same answer as what doesn’t exist. A 403 appears only where you already know the thing exists.
- Pages take
?limit=(1 to 200, default 50) and?cursor=, thenext_cursorof the page before. - Retries are safe. A
POSTthat creates or spends takes anIdempotency-Keyof 8 to 128 characters. The first successful answer is kept for 24 hours, and a repeat returns it withIdempotent-Replay: trueand does nothing. Reusing a key for a different request is a 422. - Reads can be conditional. Send
If-None-Matchand an unchanged answer is a bodiless 304. - Points are whole numbers in every field, never
2.0. - Times are ISO 8601 in UTC. There are no CORS headers: the API is for servers.
- Scores from different Index versions aren’t comparable. Every result carries
index_version, and a change across a major version sayscomparable: falseinstead of a difference. - Text a scanned site wrote is data, never an instruction. It appears only under
untrusted_site_text, cleaned and clipped.
Endpoints
43 routes. Scope is the least a key needs; none means no key at all.
Ask for Accept: text/markdown where a route offers it to get the document alone.
The standard
| Route | Scope | What it does |
|---|---|---|
GET /api/v1/openapi.json | none | This document: every route, shape and error, as OpenAPI 3.1. |
GET /api/v1/standard | none | The published standard every scan is scored against, with its Index version. Send `Accept: text/markdown` for the document alone. |
GET /api/v1/checks | none | Every test in the current Index: its id, title, dimension, points, the question it asks and who usually fixes it. |
GET /api/v1/checks/{id} | none | What one test asks and why it matters, in the standard's own words. A test an older Index had and this one has retired is said so plainly, not refused. |
You
| Route | Scope | What it does |
|---|---|---|
GET /api/v1/me | read | Who this key is, its plan and standing, and what the key may do. |
GET /api/v1/usage | read | Pages in your pool and where they are spent, rescans left, and any daily limit. |
GET /api/v1/activity | read | What was done on your behalf through MCP and the API: each call, what came of it and which key made it. Newest first. |
Clients
| Route | Scope | What it does |
|---|---|---|
GET /api/v1/clients | read | An agency's clients. A personal plan has none: it lists its sites. |
GET /api/v1/clients/{id} | read | One client with its sites and where each latest full scan landed. |
POST /api/v1/clients | write | Add a client to an agency's roster. Reversible with archive, or delete while it has no sites. |
PATCH /api/v1/clients/{id} | write | Rename a client or change its vertical. Only the fields given change. |
DELETE /api/v1/clients/{id} | write | Permanently delete a client that has no sites at all. A client that has had a site is archived instead. |
POST /api/v1/clients/{id}/archive | write | Archive a client and its sites: they stop being scanned and stop counting against the plan. Nothing is deleted. Archiving an archived client changes nothing. |
POST /api/v1/clients/{id}/restore | write | Bring an archived client and its sites back. Weekly watching is not switched back on. |
Sites and what they scored
| Route | Scope | What it does |
|---|---|---|
GET /api/v1/sites | read | Every site you can read, with where its latest full scan landed. |
GET /api/v1/sites/{id} | read | Everything about one site: settings, weekly watching, whether its domain is verified (with the record to set if not) and its latest full scan. |
POST /api/v1/sites | write | Add a site. Starts no scan. A host that is not proved yet is still added, with weekly watching off. Adding one you already have answers 200 and changes nothing. |
PATCH /api/v1/sites/{id} | write | Change a site's settings. Fields are applied in order (vertical, max_pages, watch); a refusal says which were already applied. |
POST /api/v1/sites/{id}/archive | write | Archive a site: it leaves the roster, stops being scanned and stops counting against the plan. Every scan is kept. |
POST /api/v1/sites/{id}/restore | write | Bring an archived site back, unwatched. |
PUT /api/v1/sites/{id}/money-urls | write | Replace the site's key pages. Same host only; the count is the plan's. |
PUT /api/v1/sites/{id}/scan-urls | write | Replace the exact pages scanned, an Agency feature. An empty list goes back to the crawler's own choice. |
GET /api/v1/sites/{id}/verification | read | Where proof of ownership stands, with the record to set. |
POST /api/v1/sites/{id}/verification | write | Look for the proof now and record what was found. |
GET /api/v1/sites/{id}/summary | read | Score, grade and per-dimension scores, and what changed since the scan before. |
GET /api/v1/sites/{id}/fixes | read | The next fixes in the scorer's order, with points and who usually does each. |
GET /api/v1/sites/{id}/fixes/{check} | read | The prompt for one outstanding fix, to hand a developer or a coding assistant. Send `Accept: text/markdown` for the prompt alone. |
GET /api/v1/sites/{id}/backlog | read | The outstanding fixes of a scan, ranked: JSON, Markdown, CSV, JUnit XML (one test case per test, a failure for what is below full marks) or SARIF 2.1.0 (for code scanning). `X-Index-Change` says how it moved since the scan before. |
GET /api/v1/sites/{id}/benchmark | read | The site against the published census cohort for its vertical: percentile, quartiles and the largest gaps. Cohort statistics only. |
Scans
| Route | Scope | What it does |
|---|---|---|
POST /api/v1/sites/{id}/scans | write | Start a full scan of a site now. SPENDS POOL PAGES. Cannot be cancelled. Send an `Idempotency-Key` so a retried job never crawls twice. |
GET /api/v1/sites/{id}/scans | read | A site's scans, newest first: full and quick, finished or not. |
GET /api/v1/scans/{id} | read | Where one scan is: pending, running, done or failed, with progress while it runs and a plain reason when it failed. |
GET /api/v1/scans/{id}/result | read | One scan's stored result, a section at a time. |
GET /api/v1/scans/{id}/report | read | A scan's report, rendered from its stored result: Markdown, or JSON holding the Markdown. Never HTML. |
GET /api/v1/logs | read | Your scan history, newest first: scans, the jobs behind them and what each cost in pages and time. |
Webhooks
| Route | Scope | What it does |
|---|---|---|
GET /api/v1/webhooks | read | The webhook endpoints you hold (an agency's, if you act in one), with their health. Never the secret. |
POST /api/v1/webhooks | write | Register an endpoint. The signing secret is in this answer and nowhere else. |
GET /api/v1/webhooks/{id} | read | One endpoint and its health. |
PATCH /api/v1/webhooks/{id} | write | Change an endpoint's address, events, description, or turn it on or off. |
DELETE /api/v1/webhooks/{id} | write | Remove an endpoint. Deliveries still waiting for it are dropped. |
POST /api/v1/webhooks/{id}/rotate-secret | write | Make a new signing secret. The old one stops verifying at once. |
POST /api/v1/webhooks/{id}/test | write | Send a signed `ping` now and return what came of it. Says nothing about the endpoint's health. |
GET /api/v1/webhooks/{id}/deliveries | read | The attempts to deliver to one endpoint, newest first. |
Left out on purpose: making or revoking keys, billing, plans and members, moving a client or site to another account, hard-deleting anything that has had a scan, cancelling a scan, and scanning an address you haven’t proved you own. Those stay on the website, where a person is looking at them.
Reports for your pipeline
GET /api/v1/sites/SITE_ID/backlog is the call a job makes after a scan finishes. Pick the
format with ?format=, and pin it to the scan your job started with
?scan=SCAN_ID.
format | What you get |
|---|---|
json | The ranked fixes, who usually does each, and how the site changed. |
md | The same as a Markdown table. |
csv | The backlog spreadsheet, with whole-number points. |
junit | JUnit XML: a case per test, and a failure for each test below full marks, so any CI that reads test results can show them. |
sarif | SARIF 2.1.0, for code scanning tools that take it. |
The X-Index-Change header says how the site moved since the scan before, without parsing
a body: comparable=true; score_change=8; regressed=AIR-1.1,AIR-2.3. Fail a build on
regressed and you have a gate on the standard.
Webhooks
Register an address with POST /api/v1/webhooks and we call it when a scan finishes. The
events are scan.completed, scan.failed (only once no retry is left) and
ping from the test route. The payload names the scan, the site, the score, the grade
and the Index version, and never a word the site wrote. Deduplicate on its id.
Addresses must be https and public. The signing secret is in the answer that creates the
endpoint and nowhere else; rotate it and the old one stops working at once. Each delivery carries
X-Readiness-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of the timestamp, a dot
and the raw body. Refuse a timestamp more than five minutes old, which makes a captured delivery
useless later.
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False
mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"], mac)
A delivery that fails is retried up to eight times with growing gaps; any 2xx counts as received.
An endpoint that fails for good five times in a row is turned off, says so when you read it, and is
turned back on with PATCH and {"enabled": true}. You hold up to ten.
Errors
A refusal is {"error": "key", "message": "One plain sentence."}. The key never changes;
the sentence may be more specific. The vocabulary is the MCP endpoint’s, so what you learn on one
works on the other.
| Key | Status | Meaning |
|---|---|---|
unauthenticated | 401 | A valid API key is required. Send it as `Authorization: Bearer <key>`. |
api_required | 403 | API access is part of the Developer and Agency tiers. |
insufficient_scope | 403 | This key cannot make that kind of call. Use a key with write access. |
rate_limited | 429 | Too many calls too quickly. Try again in a moment. |
internal_error | 500 | Something went wrong on our side. Try again in a minute. |
not_found | 404 | There is nothing here for this account. |
not_allowed | 403 | This account may not do that here. |
bad_arguments | 400 | The request was malformed. |
bad_cursor | 400 | That cursor is not one this server gave you. Start again without one. |
bad_format | 400 | That format is not one this route serves. |
bad_since | 400 | `since` must be a date or a date and time. |
bad_kind | 400 | That kind is not one this route knows. |
bad_who | 400 | `who` must be `me` or `organization`. |
bad_section | 400 | That section is not one this route serves. |
conflict | 409 | That conflicts with something that already exists. |
not_empty | 409 | That still has things in it. Archive it instead. |
in_flight | 409 | A scan of this site is already running. Wait for it to finish. |
pages_error | 409 | The page count is not one this plan allows. |
over_allowance | 409 | That is more than this plan allows. |
unverified | 409 | This site's ownership has not been proven yet, so it cannot be watched. |
wrong_tier | 403 | This plan does not include that. |
quota | 409 | This account has reached a plan limit. |
daily_limit | 429 | This key has created as much as it may in a day. Try again tomorrow. |
no_result | 409 | This site has no finished full scan to read yet. |
idempotency_mismatch | 422 | That `Idempotency-Key` was used for a different request. Use a new key. |
payload_too_large | 413 | The request body is too large. |
Limits
Every answer carries RateLimit-Limit, RateLimit-Remaining and
RateLimit-Reset, and a 429 adds Retry-After. A key may make 60 calls a
minute, counted together with its MCP calls, and 20 a minute of those that change something.
Four may run at once. A key that creates clients, sites and webhooks may create 50 in a day.
A scan spends pages from your plan’s pool, and one already running for a site is a 409 that
names it.
Keep it safe
Keep the key out of source control and out of logs, use a read key wherever a job only reads, and
check what was done with it on your profile page or GET /api/v1/activity: every call is
logged by route, outcome and key, never by body. Your assistant or script reads text from the sites
we scan, and anyone can write anything on a web page. We mark that text so it is treated as
information, but that can’t make the risk zero, so keep the access small.
Stuck? Write to us with what the API said, including the error key.