Developer API

Questions: hello@readinessindex.io.

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 adds next_cursor and total. A change adds changed, 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=, the next_cursor of the page before.
  • Retries are safe. A POST that creates or spends takes an Idempotency-Key of 8 to 128 characters. The first successful answer is kept for 24 hours, and a repeat returns it with Idempotent-Replay: true and does nothing. Reusing a key for a different request is a 422.
  • Reads can be conditional. Send If-None-Match and 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 says comparable: false instead 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

RouteScopeWhat it does
GET /api/v1/openapi.jsonnoneThis document: every route, shape and error, as OpenAPI 3.1.
GET /api/v1/standardnoneThe published standard every scan is scored against, with its Index version. Send `Accept: text/markdown` for the document alone.
GET /api/v1/checksnoneEvery test in the current Index: its id, title, dimension, points, the question it asks and who usually fixes it.
GET /api/v1/checks/{id}noneWhat 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

RouteScopeWhat it does
GET /api/v1/mereadWho this key is, its plan and standing, and what the key may do.
GET /api/v1/usagereadPages in your pool and where they are spent, rescans left, and any daily limit.
GET /api/v1/activityreadWhat was done on your behalf through MCP and the API: each call, what came of it and which key made it. Newest first.

Clients

RouteScopeWhat it does
GET /api/v1/clientsreadAn agency's clients. A personal plan has none: it lists its sites.
GET /api/v1/clients/{id}readOne client with its sites and where each latest full scan landed.
POST /api/v1/clientswriteAdd a client to an agency's roster. Reversible with archive, or delete while it has no sites.
PATCH /api/v1/clients/{id}writeRename a client or change its vertical. Only the fields given change.
DELETE /api/v1/clients/{id}writePermanently delete a client that has no sites at all. A client that has had a site is archived instead.
POST /api/v1/clients/{id}/archivewriteArchive 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}/restorewriteBring an archived client and its sites back. Weekly watching is not switched back on.

Sites and what they scored

RouteScopeWhat it does
GET /api/v1/sitesreadEvery site you can read, with where its latest full scan landed.
GET /api/v1/sites/{id}readEverything 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/siteswriteAdd 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}writeChange 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}/archivewriteArchive a site: it leaves the roster, stops being scanned and stops counting against the plan. Every scan is kept.
POST /api/v1/sites/{id}/restorewriteBring an archived site back, unwatched.
PUT /api/v1/sites/{id}/money-urlswriteReplace the site's key pages. Same host only; the count is the plan's.
PUT /api/v1/sites/{id}/scan-urlswriteReplace the exact pages scanned, an Agency feature. An empty list goes back to the crawler's own choice.
GET /api/v1/sites/{id}/verificationreadWhere proof of ownership stands, with the record to set.
POST /api/v1/sites/{id}/verificationwriteLook for the proof now and record what was found.
GET /api/v1/sites/{id}/summaryreadScore, grade and per-dimension scores, and what changed since the scan before.
GET /api/v1/sites/{id}/fixesreadThe next fixes in the scorer's order, with points and who usually does each.
GET /api/v1/sites/{id}/fixes/{check}readThe 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}/backlogreadThe 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}/benchmarkreadThe site against the published census cohort for its vertical: percentile, quartiles and the largest gaps. Cohort statistics only.

Scans

RouteScopeWhat it does
POST /api/v1/sites/{id}/scanswriteStart 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}/scansreadA site's scans, newest first: full and quick, finished or not.
GET /api/v1/scans/{id}readWhere 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}/resultreadOne scan's stored result, a section at a time.
GET /api/v1/scans/{id}/reportreadA scan's report, rendered from its stored result: Markdown, or JSON holding the Markdown. Never HTML.
GET /api/v1/logsreadYour scan history, newest first: scans, the jobs behind them and what each cost in pages and time.

Webhooks

RouteScopeWhat it does
GET /api/v1/webhooksreadThe webhook endpoints you hold (an agency's, if you act in one), with their health. Never the secret.
POST /api/v1/webhookswriteRegister an endpoint. The signing secret is in this answer and nowhere else.
GET /api/v1/webhooks/{id}readOne endpoint and its health.
PATCH /api/v1/webhooks/{id}writeChange an endpoint's address, events, description, or turn it on or off.
DELETE /api/v1/webhooks/{id}writeRemove an endpoint. Deliveries still waiting for it are dropped.
POST /api/v1/webhooks/{id}/rotate-secretwriteMake a new signing secret. The old one stops verifying at once.
POST /api/v1/webhooks/{id}/testwriteSend a signed `ping` now and return what came of it. Says nothing about the endpoint's health.
GET /api/v1/webhooks/{id}/deliveriesreadThe 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.

formatWhat you get
jsonThe ranked fixes, who usually does each, and how the site changed.
mdThe same as a Markdown table.
csvThe backlog spreadsheet, with whole-number points.
junitJUnit XML: a case per test, and a failure for each test below full marks, so any CI that reads test results can show them.
sarifSARIF 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.

KeyStatusMeaning
unauthenticated401A valid API key is required. Send it as `Authorization: Bearer <key>`.
api_required403API access is part of the Developer and Agency tiers.
insufficient_scope403This key cannot make that kind of call. Use a key with write access.
rate_limited429Too many calls too quickly. Try again in a moment.
internal_error500Something went wrong on our side. Try again in a minute.
not_found404There is nothing here for this account.
not_allowed403This account may not do that here.
bad_arguments400The request was malformed.
bad_cursor400That cursor is not one this server gave you. Start again without one.
bad_format400That format is not one this route serves.
bad_since400`since` must be a date or a date and time.
bad_kind400That kind is not one this route knows.
bad_who400`who` must be `me` or `organization`.
bad_section400That section is not one this route serves.
conflict409That conflicts with something that already exists.
not_empty409That still has things in it. Archive it instead.
in_flight409A scan of this site is already running. Wait for it to finish.
pages_error409The page count is not one this plan allows.
over_allowance409That is more than this plan allows.
unverified409This site's ownership has not been proven yet, so it cannot be watched.
wrong_tier403This plan does not include that.
quota409This account has reached a plan limit.
daily_limit429This key has created as much as it may in a day. Try again tomorrow.
no_result409This site has no finished full scan to read yet.
idempotency_mismatch422That `Idempotency-Key` was used for a different request. Use a new key.
payload_too_large413The 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.