HubSEOhubseoDocs
REST API & Webhooks

Site Health API

Start a bounded crawl of one of your sites, then read page health, dead links and a score.

A Site Health run crawls one of your projects (sites). It starts at the site's root, follows internal links, and records each page's health. Runs are asynchronous: you start one, then poll it.

Scopes: site-health:write to start runs, site-health:read to read them, and projects:read to look up project ids with GET /api/v1/projects. Keys created with every scope (*) can read runs but can't start them; grant site-health:write explicitly.

Start a run

POST /api/v1/projects/{project_id}/site-health/runs
Authorization: Bearer hub_live_YOUR_API_KEY
Idempotency-Key: 3f6c1d2e-9a7b-4c8d-b1e2-0f9a8b7c6d5e
Content-Type: application/json

{ "max_pages": 25, "check_dead_links": true }

All body fields are optional:

FieldTypeDefaultNotes
urlstringthe site's rootMust be on the project's host (www. is ignored).
max_pagesinteger 1–50plan maximumCapped at 25 pages on the free plan and 50 on paid plans.
check_dead_linksbooleantrueReports internal pages that returned 4xx/5xx.

Responses:

  • 202 Accepted: the run was created. Location points to /api/v1/runs/{id}.
  • 200 OK: the same Idempotency-Key was sent again (meta.idempotent_replay: true), or an identical run is already in progress and you joined it (meta.joined_in_flight: true). Location is set as for 202.

Idempotency-Key is optional but recommended (8–200 printable characters). Keys are kept per account with no expiry. Reusing one with a different request returns 409 idempotency_conflict.

{
  "data": {
    "id": "6b0e2c1f-4d3a-4b8e-9f1c-2a7d5e6f8a90",
    "type": "site_health",
    "project_id": "8f3b2c1a-5d4e-4f3a-9b8c-7d6e5f4a3b2c",
    "status": "queued",
    "phase": "queued",
    "created_at": "2026-10-12T12:00:00.000000+00:00",
    "started_at": null,
    "finished_at": null,
    "counters": null,
    "error": null,
    "request": { "url": "https://example.com/", "max_pages": 25, "check_dead_links": true },
    "result_available": false
  }
}

Poll a run

GET /api/v1/runs/{run_id}
GET /api/v1/runs/{run_id}?include=result

status is queued, running, completed, partial, failed or cancelled. The last four are final and never change. A crawl usually finishes within about 90 seconds; polling every few seconds is fine.

  • partial: the crawl stopped at its page or time limit. The results are real but cover only part of the site.
  • failed with error.code = worker_lost: the run stopped before finishing and recorded no results. Start a new run.
  • counters: stays null until something has been measured. It is never a placeholder zero.

With ?include=result, a finished run includes result:

  • summary:
    • complete and termination (complete, max-pages or max-duration).
    • Page counts.
    • inventory: eligible versus excluded pages, with reasons and an example URL each.
    • dead_links, covered in the next section.
  • pages: one entry per crawled page, with http_status, fetch_state, indexability_state, health_state (eligible, hidden, redirect, failed or unchecked) and health_reason.
  • findings: the site-quality evidence results.
  • score: { "has_data", "aggregate_score", "status" }. When nothing could be measured, has_data is false and the other fields are null.

dead_links.state is one of:

  • disabled: the run had check_dead_links: false.
  • unavailable: no crawl result is available.
  • complete: every page in the crawl was checked.
  • partial: the crawl stopped early, so more pages may exist.

Only confirmed 4xx/5xx responses count as found and appear in items. Pages that couldn't be fetched (timeouts, resets) are counted as unverified. They are never reported as broken, and they are left out of checked. Only internal pages the crawl reached are checked.

List runs

GET /api/v1/projects/{project_id}/site-health/runs?limit=20&cursor=…

Runs are listed newest first. limit defaults to 20 (maximum 100). When another page exists, meta.has_next is true and links.next holds the URL of the next page. A malformed cursor or limit returns 422.

Latest result

GET /api/v1/projects/{project_id}/site-health/latest

Returns { "run", "result", "has_data": true } for the newest finished run with results. If there isn't one, it returns { "run": null, "result": null, "has_data": false } with 200.

Limits and errors

LimitFreePaid
Runs started per hour520
Runs in progress at once12
Pages per run2550
Statuserror.codeMeaning
401missing_key, invalid_key, revoked_key, auth_unavailableNo usable key, or the key isn't linked to an account
403insufficient_scopeThe key lacks the scope (a * key used to start a run)
404not_foundThe project or run doesn't exist or isn't yours
409idempotency_conflictThe Idempotency-Key was used with a different request
422validation_error / invalid_cursorInvalid body, URL, limit or cursor
429quota_exceeded / rate_limit_exceededHourly runs, runs in progress, or the per-minute key limit. See Retry-After.
503service_unavailableA temporary problem; nothing was started. Retry later.

Error bodies use the standard envelope and include request_id. Every response also carries an X-Request-Id header.

On this page