For developers
API
The API does two things: it starts a scan of a site in your account, and it reads what your account can read in the app — sites, scan status and reports. It is part of the Gold and Platinum plans.
Authentication
- Create a key in the app under Settings → API. It is shown once; we keep a fingerprint of it, not the key.
- Send it with every request as a bearer token. Cookies are ignored.
- A key has one or both scopes: reports:read (sites, scan status, reports) and scans:create (start scans).
- A key is not a login. It cannot change your account, your sites, your billing or your keys. If one leaks, revoke it — the others keep working.
All paths are under https://app.geoready.org/api. A machine-readable description is at https://app.geoready.org/api/v1/openapi.json.
Endpoints
| Request | Scope | What it does |
|---|---|---|
| GET /v1/me | — | Your plan, this week's quota, and the key making the request. |
| GET /v1/sites | reports:read | The sites in your account. |
| POST /v1/scans | scans:create | Start a scan of one of those sites. |
| GET /v1/scans/{jobId} | reports:read | Status of a scan your account started. |
| GET /v1/reports | reports:read | The latest finished scans of each site — up to 8 per site, not the full history. |
| GET /v1/reports/{jobId} | reports:read | One report, with the same content and the same level of detail you see in the app. Add ?locale=zh for Chinese. |
GET /v1/me
curl https://app.geoready.org/api/v1/me -H "Authorization: Bearer geoready_…"
# 200
{
"plan": "gold",
"quota": { "limit": 260, "remaining": 259, "windowStart": "2026-10-08T00:00:00.000Z" },
"key": { "name": "Nightly report job", "scopes": ["reports:read", "scans:create"] }
}GET /v1/sites
curl https://app.geoready.org/api/v1/sites -H "Authorization: Bearer geoready_…"
# 200
{ "sites": [ { "siteKey": "example.com", "role": "OWNED" }, { "siteKey": "example.org", "role": "COMPETITOR" } ] }POST /v1/scans
curl -X POST https://app.geoready.org/api/v1/scans \
-H "Authorization: Bearer geoready_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: nightly-2026-10-13" \
-d '{"domain": "example.com"}'
# 202 — a new scan; 200 with "replayed": true when this Idempotency-Key was used before
{
"jobId": "0199d2c4-7c1e-7a55-9c7d-3f0f1c2b4a10",
"siteKey": "example.com",
"replayed": false,
"quota": { "limit": 260, "remaining": 259, "windowStart": "2026-10-08T00:00:00.000Z" }
}GET /v1/scans/{jobId}
curl https://app.geoready.org/api/v1/scans/0199d2c4-7c1e-7a55-9c7d-3f0f1c2b4a10 -H "Authorization: Bearer geoready_…"
# 200 — status is PENDING, RUNNING, SUCCEEDED, INCOMPLETE or FAILED
{
"jobId": "0199d2c4-7c1e-7a55-9c7d-3f0f1c2b4a10",
"siteKey": "example.com",
"status": "SUCCEEDED",
"createdAt": "2026-10-13T02:00:00.000Z",
"finishedAt": "2026-10-13T02:00:41.000Z"
}GET /v1/reports
curl https://app.geoready.org/api/v1/reports -H "Authorization: Bearer geoready_…"
# 200 — baseScore is null when a scan produced no score
{
"sites": [
{
"siteKey": "example.com",
"role": "OWNED",
"reports": [
{ "jobId": "0199d2c4-…", "status": "SUCCEEDED", "checkedAt": "2026-10-13T02:00:41.000Z", "baseScore": 72, "pagesLimit": 10 }
]
}
]
}GET /v1/reports/{jobId}
curl "https://app.geoready.org/api/v1/reports/0199d2c4-7c1e-7a55-9c7d-3f0f1c2b4a10?locale=en" -H "Authorization: Bearer geoready_…"
# 200 — abridged: every check row has id, displayClass, goal, outcome, evidence, fix, prompt
{
"reportId": "0199d2c4-…",
"siteKey": "example.com",
"checkedAt": "2026-10-13T02:00:41.000Z",
"complete": true,
"baseScore": 72,
"coverage": 1,
"fullAccess": true,
"baseModules": [ { "id": "F08", "displayClass": "scored-fail", "verdict": "fail", "kind": "scoring", "goal": "…", "outcome": "…", "evidence": [], "fix": "…", "prompt": "…" } ],
"agentModules": [ … ],
"extraModules": [ … ],
"paid": { "paidScore": 90, "pagesSampled": 10, "modules": [ … ] },
"ai": []
}
# 409 while the scan has no finished report
{ "code": "report-not-ready", "message": "…", "details": { "status": "RUNNING" } }Starting a scan
- The site must already be in your account's list. The API never adds a site.
- Each scan uses one unit of your weekly quota — the same quota the app uses. There is no separate API quota, and the API cannot start free checks.
- The response has the scan's id and your quota as it is now. Poll the status endpoint until it is SUCCEEDED, INCOMPLETE or FAILED, then read the report.
Repeating a request safely
- Send an Idempotency-Key header (8–128 printable ASCII characters) to make a request safe to retry.
- Repeating a request with the same key and the same domain returns the first scan with "replayed": true and status 200. Nothing is debited — also when your quota has run out since, the site was removed from your list, or that scan failed. To scan again, use a new key.
- The same key with a different domain is refused (409).
- A key belongs to the API key that sent it: two API keys can use the same value independently.
- There is no fixed lifetime: a key is remembered as long as its scan exists.
- After your plan ends, a replay is refused like any other request (403).
Errors
Every error has the same shape: a stable code, a message for people, and sometimes details.
{ "code": "quota-exhausted", "message": "…", "details": { "quota": { "limit": 260, "remaining": 0, "windowStart": "…" } } }| Status | Code | When |
|---|---|---|
| 400 | invalid-request | The body, a parameter or the Idempotency-Key header is malformed. |
| 400 | invalid-domain | The domain cannot be understood. |
| 401 | unauthorized | No key, or a key that is unknown, revoked or expired. The answer is the same in each case. |
| 403 | api-not-in-plan | Your plan does not include the API (or has ended). |
| 403 | insufficient-scope | The key lacks the scope this request needs. |
| 403 | quota-exhausted | This week's quota is used up. |
| 404 | not-found | No such path, scan or report — or not yours to read. |
| 404 | site-not-tracked | The domain is not in your account's site list. |
| 409 | idempotency-conflict | The Idempotency-Key was used for another domain. |
| 409 | report-not-ready | The scan has no finished report yet; details.status says where it is. |
| 429 | rate-limited | Too many requests. Retry-After says when to try again. |
| 500 | internal-error | Something went wrong on our side. |
Webhooks
Webhooks are not switched on for this service yet. Poll the scan status endpoint for now.
Limits
- 60 requests a minute per key, 120 a minute per account, 300 a minute per address.
- 10 scan starts a minute per key.
- 30 requests a minute with a missing or wrong key per address.
- Over a limit you get 429 with a Retry-After header.
What the API does not do
- Free checks, and scans of sites that are not in your list.
- Adding or removing sites, billing, account settings, key management.
- Starting AI analyses or changing whether a report is shared.