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.
All endpoints accept both session auth (dashboard) and API key auth (programmatic access). API keys scoped to a specific domain are restricted to that domain's runs.
API key access requires a Pro plan or above. Lower-tier API keys receive 403 { "error": "plan_required", "requiredPlan": "pro" }.
Start an Audit
/api/seo-spider/runsStart 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 |
|---|---|---|
domainrequired |
string | Domain to audit (e.g., "your-app.com"). |
urlsrequired |
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. |
moderequired |
"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
Audit started. Returns runId and status: "running".
Validation failed: invalid_urls, no_sitemap, or schema error.
Missing or invalid API key / session.
domain_not_owned, no_seo_spider_access, on_demand_limit_reached, or
max_pages_per_month_limit_reached.
run_already_active. An audit is already running for this domain. Response
includes the active runId.
no_new_pages. No new or updated pages found in sitemap since last audit
(when newPagesOnly: 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
/api/seo-spider/runsList 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
Returns up to 50 runs. Empty runs array if the caller has no access to any
matching domain.
Missing or invalid API key / session (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 domainscurl "https://encited.com/api/seo-spider/runs" \-H "x-lovablehtml-api-key: <API_KEY>"# Filter to a single domaincurl "https://encited.com/api/seo-spider/runs?domain=your-app.com" \-H "x-lovablehtml-api-key: <API_KEY>"
Get Run Details
/api/seo-spider/runs/:runIdFetch a single audit run with its per-page results.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
runIdrequired |
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
Returns the run and its pages.
Invalid query parameters: limit outside 1–500, offset without limit,
search over 500 characters, or an unknown issueId.
Missing or invalid API key / session (invalid_api_key).
api_key_domain_scope_mismatch — the API key is scoped to a different domain.
domain_access_forbidden — session user lacks access to the run's domain.
Run does not exist or does not belong to the authenticated account.
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>"
