Encited

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 runId and status: "running".
  • 400 Bad Request — Validation failed: invalid_urls, no_sitemap, or schema error.
  • 401 Unauthorized — Missing or invalid API key.
  • 403 Forbiddendomain_not_owned, no_seo_spider_access, on_demand_limit_reached, or max_pages_per_month_limit_reached.
  • 409 Conflictrun_already_active. An audit is already running for this domain. Response includes the active runId.
  • 422 Unprocessableno_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#

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 runs array 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: limit outside 1–500, offset without limit, search over 500 characters, or an unknown issueId.
  • 401 Unauthorized — Missing or invalid API key (invalid_api_key).
  • 403 Forbiddenapi_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 Unavailablearchive_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>"