SEO Audit API
Trigger on-demand SEO audits for your domains, list past runs, and fetch detailed results. The crawler discovers pages, checks for SEO issues, and reports back. Only one audit can run per domain at a time.
An API key scoped to a specific domain is restricted to that domain's runs.
API key access requires the Pro plan or above. A key on a lower plan receives 403 { "error": "plan_required", "requiredPlan": "agency" }. The agency string is the stored ID for the Pro plan.
Start an Audit#
POST /api/seo-spider/runs#
Start a new SEO audit run for a domain. The crawler will discover and analyze pages based on the selected mode.
Request Body#
| Parameter | Type | Description |
|---|---|---|
domain (required) |
string | Domain to audit (e.g., "your-app.com"). |
urls (required) |
string[] | Seed URLs to crawl (min 1, must be valid URLs). All URLs must belong to the target domain. In follow mode, typically just the homepage. |
mode (required) |
"follow" | "selected" | "sitemap" | follow — spider from seed URLs following links. selected — only audit the specified URLs. sitemap — discover URLs from the domain's sitemap. |
allowSampling |
boolean | Enable URL pattern sampling on the crawler. Default: true. |
newPagesOnly |
boolean | Only audit new or updated pages since the last completed run (sitemap mode). Default: false. |
sitemapUrl |
string (URL) | Custom sitemap URL override. Falls back to the domain's configured sitemap if omitted. |
Response Body#
{
"ok": true,
"runId": "a1b2c3d4-e5f6-...",
"status": "running"
}
Response Codes#
- 200 Success — Audit started. Returns
runIdandstatus: "running". - 400 Bad Request — Validation failed:
invalid_urls,no_sitemap, or schema error. - 401 Unauthorized — Missing or invalid API key.
- 403 Forbidden —
domain_not_owned,no_seo_spider_access,on_demand_limit_reached, ormax_pages_per_month_limit_reached. - 409 Conflict —
run_already_active. An audit is already running for this domain. Response includes the activerunId. - 422 Unprocessable —
no_new_pages. No new or updated pages found in sitemap since last audit (whennewPagesOnly: true).
Example#
const response = await fetch("https://encited.com/api/seo-spider/runs", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-lovablehtml-api-key": "<API_KEY>",
},
body: JSON.stringify({
domain: "your-app.com",
urls: ["https://your-app.com"],
mode: "follow",
allowSampling: true,
}),
});
const result = await response.json();
// { "ok": true, "runId": "a1b2c3d4-e5f6-...", "status": "running" }
curl -X POST \
"https://encited.com/api/seo-spider/runs" \
-H "Content-Type: application/json" \
-H "x-lovablehtml-api-key: <API_KEY>" \
-d '{
"domain": "your-app.com",
"urls": ["https://your-app.com"],
"mode": "follow",
"allowSampling": true
}'
Limits: Audit access and page-per-run limits depend on your plan. Only one audit can run per domain at a time.
List Runs#
GET /api/seo-spider/runs#
List the 50 most recent SEO audit runs across your domains, optionally filtered by domain.
Query Parameters#
| Parameter | Type | Description |
|---|---|---|
domain |
string | Filter to runs for a single domain. If the API key is scoped to a domain, only that domain's runs are returned regardless of this parameter. |
Response Body#
{
"runs": [
{
"id": "a1b2c3d4-e5f6-...",
"domain": "your-app.com",
"startedAt": 1762400000000,
"completedAt": 1762400180000,
"status": "success",
"trigger": "manual",
"frequency": "weekly",
"pagesCrawled": 42,
"healthScore": 87.5,
"issueCount": 12,
"deltaIssues": -3,
"deltaHealth": 2.1,
"robotsAnalysis": { "...": "..." },
"crawlStats": { "...": "..." },
"runCount": 1,
"closeReason": "finished",
"needsContinuation": false,
"hitUrlLimit": false,
"errorMessage": null
}
]
}
status is one of queued, running, success, limit_reached, partial, or error. Timestamps are Unix epoch milliseconds. Results are ordered by startedAt descending and capped at 50.
Response Codes#
- 200 Success — Returns up to 50 runs. Empty
runsarray if the caller has no access to any matching domain. - 401 Unauthorized — Missing or invalid API key (
invalid_api_key).
Example#
const response = await fetch(
"https://encited.com/api/seo-spider/runs?domain=your-app.com",
{
headers: {
"x-lovablehtml-api-key": "<API_KEY>",
},
},
);
const { runs } = await response.json();
// runs[0] = { id, domain, status, healthScore, issueCount, ... }
# All recent runs across your domains
curl "https://encited.com/api/seo-spider/runs" \
-H "x-lovablehtml-api-key: <API_KEY>"
# Filter to a single domain
curl "https://encited.com/api/seo-spider/runs?domain=your-app.com" \
-H "x-lovablehtml-api-key: <API_KEY>"
Get Run Details#
GET /api/seo-spider/runs/:runId#
Fetch a single audit run with its per-page results.
Path Parameters#
| Parameter | Type | Description |
|---|---|---|
runId (required) |
string | The run ID returned by POST /api/seo-spider/runs. |
Query Parameters#
| Parameter | Type | Description |
|---|---|---|
limit |
number | Max page rows to return (1–500). Omit to receive every page in one response — always paginate for large runs. |
offset |
number | Page rows to skip, for pagination. Defaults to 0. |
search |
string | Case-insensitive substring filter on the page path. |
issueId |
string | Only pages carrying this issue (e.g. META_DESC_MISSING). pageTotal reflects the filtered count. |
Response Body#
{
"run": {
"id": "a1b2c3d4-e5f6-...",
"domain": "your-app.com",
"startedAt": 1762400000000,
"completedAt": 1762400180000,
"status": "success",
"trigger": "manual",
"frequency": "weekly",
"pagesCrawled": 42,
"healthScore": 87.5,
"issueCount": 12,
"deltaIssues": -3,
"deltaHealth": 2.1,
"robotsAnalysis": { "...": "..." },
"crawlStats": { "...": "..." },
"runCount": 1,
"closeReason": "finished",
"needsContinuation": false,
"hitUrlLimit": false,
"errorMessage": null,
"sitemapIssues": []
},
"pages": [
{
"id": "page-uuid",
"path": "/",
"statusCode": 200,
"loadTimeMs": 420,
"healthScore": 92,
"previewScore": 88,
"indexable": true,
"issueCount": 2,
"issues": [{ "issueId": "META_DESC_MISSING", "severity": "warning" }],
"trendPoints": [85, 88, 92],
"metaDesc": "Turn your SPA into crawlable HTML.",
"canonical": "https://example.com/",
"metaRobots": "index, follow",
"h1": "Example home",
"contentType": "text/html; charset=utf-8",
"lastModified": "Tue, 21 Jul 2026 08:00:00 GMT",
"htmlBytes": 48213,
"redirectCount": 0,
"socialTags": { "og:title": "Example", "twitter:card": "summary" },
"viewport": "width=device-width, initial-scale=1",
"charset": "utf-8",
"crawledAt": "2026-07-21 08:00:12",
"wordCount": 412,
"textRatio": 0.18,
"readability": 64.2,
"internalOutlinks": 34,
"externalOutlinks": 6,
"nofollowOutlinks": 2,
"inlinkCount": 12
}
],
"pageTotal": 42,
"issueBreakdown": { "META_DESC_MISSING": 12, "THIN_CONTENT": 3 }
}
pages is sourced from the live store while a run is recent and from cold storage once archived; the response shape is identical either way. Runs audited before the per-page detail fields existed return null for them. pageTotal is the number of pages matching the current filters. issueBreakdown counts issue occurrences across the whole run regardless of filters; it is computed only when offset is 0 and no search is set, and is null otherwise.
Response Codes#
- 200 Success — Returns the run and its pages.
- 400 Bad Request — Invalid query parameters:
limitoutside 1–500,offsetwithoutlimit,searchover 500 characters, or an unknownissueId. - 401 Unauthorized — Missing or invalid API key (
invalid_api_key). - 403 Forbidden —
api_key_domain_scope_mismatch— the API key is scoped to a different domain. - 404 Not Found — Run does not exist or does not belong to the authenticated account.
- 503 Service Unavailable —
archive_unavailable— the run's pages are in cold storage and could not be read. Retry later.
Example#
const response = await fetch(
"https://encited.com/api/seo-spider/runs/a1b2c3d4-e5f6-...",
{
headers: {
"x-lovablehtml-api-key": "<API_KEY>",
},
},
);
const { run, pages } = await response.json();
// run.healthScore, run.issueCount, pages[i].path, pages[i].issues, ...
curl "https://encited.com/api/seo-spider/runs/a1b2c3d4-e5f6-..." \
-H "x-lovablehtml-api-key: <API_KEY>"
