curl -X POST https://seonio.com/api/v1/site-audit/result \ -H "Content-Type: application/json" \ -H "X-API-Key: $SEONIO_API_KEY" \ -d '{"audit_id": "019a4f2e-7c31-7d02-a6b5-3e8f1c2d4a90"}'
{
"audit_id": "019a4f2e-7c31-7d02-a6b5-3e8f1c2d4a90",
"domain": "example.com",
"country": "us",
"status": "running",
"final": false,
"stage": "Checking your pages (43 of 100) and testing your top pages on phones",
"progress": {
"percent": 37,
"pages_fetched": 43,
"pages_picked": 100,
"areas_done": 0,
"areas": {
"technical": "running",
"onpage": "running",
"content": "running",
"speed": "running",
"backlinks": "done",
"ai": "running"
}
},
"eta_seconds": 190,
"poll_after_seconds": 30,
"error": null,
"created_at": "2026-11-04T14:02:11",
"finished_at": null,
"url": "https://seonio.com/site-audit/example.com?country=us",
"free_audit": true,
"report": null
}What to send and what comes back
Step 2: where the audit is and, once done or partial, its standard report: the date, the pages checked, Site Health, each area's score and counts, every issue in order of importance (impact, then the pages it affects) with up to 10 example URLs, what changed since the site's previous audit, the speed tests, the backlink numbers and the site's other numbers as the audit found them, the AI crawlers robots.txt allows, and the site's AI Visibility Report headline. Send audit_id, or domain and country instead (not both) for this account's newest audit of them: the running one, else the last one. Only the account that started an audit can read it: an audit_id that is not one of this account's audits, or a domain and country it has no audit of, is a 422. The audit runs in the background, in two calls: 1. POST /api/v1/site-audit/start checks the price, starts the audit of a domain in a country and returns an audit_id. 2. POST /api/v1/site-audit/result (free) with that audit_id, or with the same domain and country: poll every 30 seconds. An audit usually takes a few minutes; eta_seconds estimates the rest. It ends done, partial (an area could not be checked) or failed (refunded in full, or the free audit given back). The audit's page on seonio.com (url) shows the same audit.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
audit_id |
string | null | optional | The audit_id the start call returned. |
domain |
string | null | optional | Instead of audit_id: the domain the audit was started for. |
country |
string | optional | With domain: the audit's country (default "us"; "uk" is accepted as "gb").
50 supported countriesdzAlgeriaarArgentinaauAustraliaatAustriabdBangladeshbeBelgiumbrBrazilbgBulgariacaCanadaczCzechiadkDenmarkegEgyptfiFinlandfrFrancedeGermanyghGhanagrGreecehuHungaryinIndiaidIndonesiaieIrelanditItalyjpJapankeKenyamyMalaysiamxMexicomaMorocconlNetherlandsnzNew ZealandngNigerianoNorwaypkPakistanphPhilippinesplPolandptPortugalroRomaniasaSaudi ArabiarsSerbiasgSingaporezaSouth AfricaesSpainlkSri LankaseSwedenchSwitzerlandthThailandtrTurkiyeaeUnited Arab EmiratesgbUnited Kingdom (also uk, returned as gb)usUnited StatesvnVietnam |
Response
| Field | Type | Description |
|---|---|---|
audit_id |
string | The audit's id. |
domain |
string | The site's domain. |
country |
string | The country's ISO code. |
status |
string | "queued", "running", "finishing", "done", "partial" or "failed". |
final |
boolean | True once the audit has ended (done, partial or failed). |
stage |
string | null | What the audit is doing while it runs. |
progress |
object | While it runs: percent, pages_fetched, pages_picked, areas_done and each area's state (queued, running, done). |
eta_seconds |
integer | null | Estimated seconds left while it runs (an estimate, not a promise); 0 once ended. |
poll_after_seconds |
integer | null | When to call again; null once the audit has ended. |
error |
string | null | Why a failed audit failed; a failed audit is not charged. |
created_at |
string | null | When it started (UTC). |
finished_at |
string | null | When it ended (UTC). |
url |
string | The audit's page on seonio.com (signed in to this account). |
free_audit |
boolean | True when the audit used one of your plan's free site audits this month (nothing charged). |
report |
object | null | Once done or partial: date; pages {checked, ok, limit, left_out_hosts}; site_health (0-100); areas [{key, name, score, issues, passed, could_not_run}]; issues [{rule, area, impact, title, why, pages, examples}]; what_changed {since, points, fixed, new, came_back, still_open, not_rechecked} or null for a site's first audit; speed [{url, score, lcp_ms, cls}]; backlinks {authority, linking_sites, date}; numbers {organic_visits, organic_visits_month, search_demand, ranking_keywords, keywords_date}; crawlers [{bot, for, allowed}]; ai_visibility {report_id, status, recommended, answers, date} or null. Null before. |
Copy, paste, add your key
The same request in cURL, Python and Node.js.
curl -X POST https://seonio.com/api/v1/site-audit/result \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"audit_id": "019a4f2e-7c31-7d02-a6b5-3e8f1c2d4a90"}'
import requests
resp = requests.post(
"https://seonio.com/api/v1/site-audit/result",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"audit_id": "019a4f2e-7c31-7d02-a6b5-3e8f1c2d4a90"},
)
print(resp.json())
const resp = await fetch("https://seonio.com/api/v1/site-audit/result", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "YOUR_API_KEY",
},
body: JSON.stringify({"audit_id": "019a4f2e-7c31-7d02-a6b5-3e8f1c2d4a90"}),
});
console.log(await resp.json());
Run a real request
Send this request from your browser with your API key, or leave the key empty to see the sample response.
A successful run costs Free. With no key, you see the sample response and nothing is charged.
{
"audit_id": "019a4f2e-7c31-7d02-a6b5-3e8f1c2d4a90",
"country": "us",
"created_at": "2026-11-04T14:02:11",
"domain": "example.com",
"error": null,
"eta_seconds": 190,
"final": false,
"finished_at": null,
"free_audit": true,
"poll_after_seconds": 30,
"progress": {
"areas": {
"ai": "running",
"backlinks": "done",
"content": "running",
"onpage": "running",
"speed": "running",
"technical": "running"
},
"areas_done": 0,
"pages_fetched": 43,
"pages_picked": 100,
"percent": 37
},
"report": null,
"stage": "Checking your pages (43 of 100) and testing your top pages on phones",
"status": "running",
"url": "https://seonio.com/site-audit/example.com?country=us"
}
What developers build with it
- Poll a running audit.
- Read the finished audit as JSON.
The same contract on every endpoint
Error responses are JSON with a "detail" field. It is a message string unless the row above says otherwise.
| Code | When |
|---|---|
200 | Success. The lookup completed and was charged, even if the response is empty. /keywords/global charges only the keywords it looked up. |
401 | Missing or unknown API key. |
402 | Not enough balance. "detail" is an object with "error", "message", "balance_required" and "balance_available". Requests still running hold their price until they finish, so a retry can succeed once they do. |
403 | The account is not verified yet, or it is locked. |
409 | Another AI Visibility Report of this account is running, a start sent with free_only found no free report or site audit left, or the site was audited less than 30 days ago ("detail" is then an object with "error": "too_soon", "message", "audit_id" and "next_audit_at"); the detail says which. Not charged. |
422 | Invalid request (wrong shape or types, a keyword over 200 characters, a URL the page checker cannot audit, a domain that does not exist, or a bare public suffix such as co.uk that groups many websites) — never charged. "detail" is usually a list of validation errors; an answer that retrying cannot change (such as a domain without a DNS record) is a message string with an X-Outcome header naming it: missing, unsupported, too_large or refused_input. |
429 | Rate limit exceeded, never charged. Requests per second per account by plan: 5 on a free account, 10 on Starter, 15 on Pro and 20 on Business; retry after the Retry-After header. Also the page checker's provider limit and the AI Visibility Report's hourly limit on new reports. |
503 | A data provider failed or timed out (X-Outcome: temporary). Nothing was charged; retry after the seconds in the Retry-After header. |
Questions, answered
Simple answers for developers using the API.
Every endpoint has a published USD price or formula. Requests draw from the same testing, included and Extra Usage balance as website and MCP actions: testing first, then included usage, then Extra Usage expiring soonest.
API actions require a verified account with a free $1 balance or an active subscription balance. Create and manage API keys from your account, then send the key in the X-API-Key header.
Yes. Your account shows API usage and remaining balance, so you can track costs while building and running automated workflows.
REST API and MCP requests per second depend on your plan: 5 on a free account, 10 on Starter, 15 on Pro and 20 on Business. Website data requests from the same account count toward the same second. Requests above the limit return HTTP 429 with a Retry-After header and are not charged; wait a second and retry.
Turn search insights into measurable growth
Start your SEO research in seconds.