> ## Documentation index
> Fetch the complete documentation index at: https://rankfield.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Follow an audit

> Read an audit's status and live progress: pages fetched, the current speed, the site's response time and the issues found so far.

`GET https://rankfield.dev/v1/site-audit/{id}`

```bash cURL
curl 'https://rankfield.dev/v1/site-audit/169c615ff6c5057c8ca5384a6e2df47a' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

```python Python
import requests

response = requests.get(
    "https://rankfield.dev/v1/site-audit/169c615ff6c5057c8ca5384a6e2df47a",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(response.json())
```

```javascript JavaScript
const response = await fetch("https://rankfield.dev/v1/site-audit/169c615ff6c5057c8ca5384a6e2df47a", {
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
  },
});
console.log(await response.json());
```

```json 200
{
  "audit": {
    "id": "169c615ff6c5057c8ca5384a6e2df47a",
    "domain": "https://rankfield.dev",
    "status": "done",
    "createdAt": "2026-10-28T21:47:08Z",
    "startedAt": "2026-10-28T21:47:08Z",
    "finishedAt": "2026-10-28T21:48:53Z",
    "expiresAt": "2026-11-27T21:48:53Z",
    "stoppedBy": [],
    "error": null,
    "options": {
      "domain": "https://rankfield.dev",
      "maxPages": 200,
      "subdomains": false,
      "sources": [
        "links",
        "sitemaps"
      ],
      "urls": [],
      "mode": "full",
      "sample": false,
      "samplePerTemplate": 500,
      "checkExternal": false,
      "checkResources": true,
      "previous": null,
      "speed": 2.0
    },
    "progress": {
      "phase": "done",
      "fetched": 199,
      "queued": 0,
      "known": 361,
      "pagesPerSecond": 1.9,
      "rate": 2.0,
      "ttfbP50Ms": 63.0,
      "baselineMs": 54.0,
      "errors": 0,
      "halvings": 0,
      "pauses": 0,
      "etaSeconds": 0,
      "siteHold": null,
      "rateLimited": 0,
      "stoppedBy": [],
      "issues": {
        "error": 0,
        "warning": 10,
        "notice": 464
      }
    },
    "counts": {
      "inventory": {
        "internal": 202,
        "htmlPages": 103,
        "indexable": 103,
        "documents": 96,
        "redirects": 0,
        "errors": 0,
        "resources": 3,
        "disallowed": 0,
        "notFetched": 0,
        "known": 199,
        "sitemapUrls": 99,
        "externalChecked": 0,
        "externalBroken": 0,
        "externalUnchecked": {
          "total": 0,
          "excluded": 0,
          "robotsDisallowed": 0,
          "botBlocked": 0
        },
        "byStatus": {
          "2xx": 202,
          "3xx": 0,
          "4xx": 0,
          "5xx": 0,
          "failed": 0
        }
      },
      "health": {
        "score": 100,
        "urlsWithErrors": 0,
        "urlsWithoutErrors": 202,
        "internalUrls": 202,
        "note": "The share of crawled internal URLs with no error-level issue; site-wide findings are not counted per URL."
      },
      "issues": {
        "error": {
          "issueTypes": 0,
          "affectedUrls": 0
        },
        "warning": {
          "issueTypes": 4,
          "affectedUrls": 10
        },
        "notice": {
          "issueTypes": 18,
          "affectedUrls": 464
        }
      },
      "byRule": {
        "canonical-missing": 7,
        "not-in-sitemap": 4,
        "empty-anchor": 3,
        "low-link-score": 7,
        "title-too-short": 33,
        "title-duplicate": 3,
        "title-same-as-h1": 1,
        "description-too-long": 1,
        "description-too-short": 1,
        "description-duplicate": 3,
        "h1-duplicate": 3,
        "heading-empty": 2,
        "img-too-large": 1,
        "thin-content": 1,
        "low-text-ratio": 54,
        "duplicate-content": 3,
        "schema-none": 103,
        "url-uppercase": 1,
        "no-hsts": 103,
        "og-missing": 9,
        "twitter-card-missing": 28,
        "no-preferred-image": 103
      },
      "byRuleIndexable": {
        "heading-empty": 2,
        "not-in-sitemap": 4,
        "title-same-as-h1": 1,
        "low-text-ratio": 54,
        "no-hsts": 103,
        "og-missing": 9,
        "url-uppercase": 0,
        "description-too-short": 1,
        "description-too-long": 1,
        "title-too-short": 33,
        "img-too-large": 1,
        "title-duplicate": 3,
        "schema-none": 103,
        "duplicate-content": 3,
        "description-duplicate": 3,
        "thin-content": 1,
        "twitter-card-missing": 28,
        "no-preferred-image": 103,
        "h1-duplicate": 3,
        "low-link-score": 7,
        "empty-anchor": 3,
        "canonical-missing": 7
      },
      "links": 7267,
      "sitemaps": 1,
      "fetched": 199
    }
  }
}
```

```json 404
{
  "error": {
    "code": "not_found",
    "message": "Not found"
  }
}
```

Poll every few seconds while `status` is `queued`, `running` or `postPass`. Issue counts are live, and every [result](/docs/site-audit/read-the-results) can be read during the crawl.

## Parameters

### Headers

| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| `Authorization` | string | Yes | – | `Bearer` followed by your API key. See [Authentication](/docs/get-started/authentication). |

### Path parameters

| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| `id` | string | Yes | – | The audit's `id`, from [Audit a website](/docs/site-audit/audit-a-website). |

## Response

| Field | Type | Description |
|---|---|---|
| `audit` | object | As [Audit a website](/docs/site-audit/audit-a-website) answers. |
| `audit.progress.phase` | string | `queued`, `probes` (robots.txt, the homepage, how the site answers a page that does not exist, its http and www variants, llms.txt), `crawl` (sitemaps, pages and resources), `postPass` (the site-wide checks) or `done`. |
| `audit.progress.fetched`, `audit.progress.queued`, `audit.progress.known` | integer | Pages fetched, waiting, and known so far. |
| `audit.progress.pagesPerSecond`, `audit.progress.rate` | number | Pages fetched a second, and the speed the brake allows now (fetches a second). |
| `audit.progress.ttfbP50Ms`, `audit.progress.baselineMs` | number \| null | The site's median time to first byte now, and at the start: a slower site slows the audit. |
| `audit.progress.errors`, `audit.progress.halvings`, `audit.progress.pauses` | integer | Failed fetches, and how often the speed was halved or the crawl paused for the site's sake. |
| `audit.progress.etaSeconds` | integer \| null | An estimate of the time left: the pages still to fetch (up to `maxPages` of those known so far) at the current speed. `null` while the site makes us wait. |
| `audit.progress.siteHold` | object \| null | While the site makes us wait: `{until, seconds, reason, status}`. `reason` is `rateLimited` (it answered `429` or `503`, and `status` says which; we wait for its `Retry-After`, 30 seconds when it gives none, 5 minutes at most) or `siteErrors` (we paused after errors). `until` is when we go on, in Unix milliseconds, and `seconds` how long that is from now. |
| `audit.progress.rateLimited` | integer | How many times the site answered `429` or `503`. |
| `audit.progress.issues`, `audit.counts.issues` | object | Issues found so far, per severity (`error`, `warning`, `notice`): `issueTypes`, the kinds of issue, and `affectedUrls`, URL–issue pairs (a URL with three issues counts three times). |
| `audit.counts.health` | object | `{score, urlsWithErrors, urlsWithoutErrors, internalUrls}`: the health score, the share of crawled internal URLs with no error-level issue, 0 to 100, with its parts. Site-wide findings don't count per URL. |
| `audit.counts.inventory` | object | What was crawled, apart from issues: `internal` (every crawled URL of the site), `htmlPages` (HTML pages that answered 200), `indexable`, `documents` (Markdown, PDF and other files, which are not HTML pages), `redirects`, `errors`, `resources` (the site's images, scripts and stylesheets), `disallowed` (by robots.txt), `notFetched`, `known`, `sitemapUrls`, `externalChecked` (external URLs given a status check), `externalBroken` (the broken ones), `externalUnchecked` (`{total, excluded, robotsDisallowed, botBlocked}`: external URLs we did not check, on sites we never fetch, disallowed by robots.txt, or refused by bot protection, never counted as broken) and `byStatus`. |

## Your audits

`GET /v1/site-audit` lists the audits your key started, newest first, each as above.

## Stop an audit

`POST /v1/site-audit/{id}/cancel` stops the crawl. What was found stays readable, and the site-wide checks run on it (`stoppedBy: ["canceled"]`).

## Errors

| Status | Code | When |
|---|---|---|
| 401 | `unauthorized` | No valid API key. |
| 404 | `not_found` | No audit with this id for this key (only the key that started an audit can read it), an unknown issue, or a page that is not in the audit. |
| 500 | `internal` | Our bug. Retry. |
| 503 | `unavailable` | New audits are paused while the service is short of storage, or it is restarting. Retry after `Retry-After`. |

## MCP

On the [MCP server](/docs/agents/mcp-server), the tools for this are `site_audit_list`, `site_audit_status` and `site_audit_cancel`.
