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

# Broken Link Checker

> Scan a single page, or crawl an entire site, for dead links with exact HTTP status codes and reasons.

Three endpoints, sharing one link-checking engine: scan one page synchronously, or start a site-wide crawl (via `sitemap.xml` and internal links) and poll it to completion. Every discovered link is resolved, checked over the network, and classified by severity.

<Info>
  **2 credits** per call (single-page scan or crawl start) · Typical response time \~2–10s for a single page
</Info>

## Scan a single page

`POST /v1/broken-links` checks every link found on one page and returns immediately.

### Body parameters

<ParamField body="url" type="string" required>
  The page to scan. `https://` is assumed if the scheme is omitted.
</ParamField>

### Response

<ResponseField name="data" type="object" required>
  <Expandable title="properties" defaultOpen>
    <ResponseField name="url" type="string" required>
      Echoes the scanned URL.
    </ResponseField>

    <ResponseField name="scannedAt" type="string" required>
      ISO 8601 timestamp.
    </ResponseField>

    <ResponseField name="totalLinks" type="integer" required>
      Total links discovered on the page.
    </ResponseField>

    <ResponseField name="checkedLinks" type="integer" required>
      How many were actually checked (capped for performance).
    </ResponseField>

    <ResponseField name="truncated" type="boolean" required>
      `true` if the page had more links than the scan cap.
    </ResponseField>

    <ResponseField name="healthScore" type="integer" required>
      0–100.
    </ResponseField>

    <ResponseField name="grade" type="string" required>
      Letter grade derived from `healthScore`.
    </ResponseField>

    <ResponseField name="summary" type="object" required>
      <Expandable title="properties">
        <ResponseField name="ok" type="integer" required>
          Links that returned a healthy status.
        </ResponseField>

        <ResponseField name="warnings" type="integer" required>
          Links with a non-fatal issue (e.g. a redirect chain).
        </ResponseField>

        <ResponseField name="broken" type="integer" required>
          Links that are dead.
        </ResponseField>

        <ResponseField name="info" type="integer" required>
          Links that were noted but not scored (e.g. a non-HTTP scheme).
        </ResponseField>

        <ResponseField name="internal" type="integer" required>
          Links to the same site.
        </ResponseField>

        <ResponseField name="external" type="integer" required>
          Links to a different site.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="statusBreakdown" type="array" required>
      One row per distinct status/error code seen.

      <Expandable title="item shape">
        <ResponseField name="code" type="string" required>
          Status code as a string (`"404"`) or a network error code (`"ENOTFOUND"`).
        </ResponseField>

        <ResponseField name="label" type="string" required>
          Short human label, e.g. `"Not Found"`.
        </ResponseField>

        <ResponseField name="severity" type="string" required>
          `"ok"`, `"warning"`, `"broken"`, or `"info"`.
        </ResponseField>

        <ResponseField name="count" type="integer" required>
          How many links had this code.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="links" type="array" required>
      <Expandable title="item shape">
        <ResponseField name="url" type="string" required>
          Resolved absolute URL.
        </ResponseField>

        <ResponseField name="originalHref" type="string" required>
          href exactly as authored.
        </ResponseField>

        <ResponseField name="text" type="string" required>
          Anchor text.
        </ResponseField>

        <ResponseField name="type" type="string" required>
          `"internal"`, `"external"`, `"mailto"`, `"tel"`, `"anchor"`, or `"other"`.
        </ResponseField>

        <ResponseField name="rel" type="string">
          The `rel` attribute, if any.
        </ResponseField>

        <ResponseField name="statusCode" type="integer | null" required>
          HTTP status, or `null` when there was no response.
        </ResponseField>

        <ResponseField name="statusText" type="string" required>
          HTTP status text, or the network error label.
        </ResponseField>

        <ResponseField name="severity" type="string" required>
          `"ok"`, `"warning"`, `"broken"`, or `"info"`.
        </ResponseField>

        <ResponseField name="reason" type="string" required>
          URL-specific explanation of the outcome.
        </ResponseField>

        <ResponseField name="category" type="string" required>
          Grouping label, e.g. `"Client Error (4xx)"`, `"Network Error"`.
        </ResponseField>

        <ResponseField name="redirected" type="boolean" required>
          `true` if the link responded with a 3xx redirect.
        </ResponseField>

        <ResponseField name="finalUrl" type="string">
          Redirect target, present when `redirected` is `true`.
        </ResponseField>

        <ResponseField name="responseTimeMs" type="integer">
          Round-trip time, present when a response was received.
        </ResponseField>

        <ResponseField name="errorCode" type="string">
          Low-level network error, e.g. `"ENOTFOUND"`, when applicable.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.pocketutils.com/v1/broken-links \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url": "https://example.com"}'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch("https://api.pocketutils.com/v1/broken-links", {
    method: "POST",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url: "https://example.com" }),
  });
  const { data } = await res.json();
  ```
</RequestExample>

<ResponseExample>
  ```json Response (abridged) theme={null}
  {
    "success": true,
    "data": {
      "url": "https://example.com",
      "scannedAt": "2026-07-21T09:14:02.000Z",
      "totalLinks": 42,
      "checkedLinks": 42,
      "truncated": false,
      "healthScore": 88,
      "grade": "B+",
      "summary": { "ok": 37, "warnings": 2, "broken": 3, "info": 0, "internal": 30, "external": 12 },
      "statusBreakdown": [
        { "code": "200", "label": "OK", "severity": "ok", "count": 37 },
        { "code": "404", "label": "Not Found", "severity": "broken", "count": 3 }
      ],
      "links": [
        {
          "url": "https://example.com/old-page",
          "originalHref": "/old-page",
          "text": "Learn more",
          "type": "internal",
          "rel": null,
          "statusCode": 404,
          "statusText": "Not Found",
          "severity": "broken",
          "reason": "The page no longer exists at this URL.",
          "category": "Client Error (4xx)",
          "redirected": false,
          "responseTimeMs": 182
        }
      ]
    }
  }
  ```
</ResponseExample>

***

## Crawl an entire site

For more than one page, start an async crawl and poll it — see [Async Jobs & Polling](/async-jobs) for the full pattern, phase list, and crawl limits (250 pages, 6-minute budget).

### Start the crawl

`POST /v1/broken-links/crawl`, body `{ "url": "..." }` — same as the single-page scan, any URL on the site.

Returns `202` immediately:

```json theme={null}
{ "success": true, "data": { "jobId": "c3d4e5f6-1234-4a5b-8c9d-0e1f2a3b4c5d" } }
```

### Poll the status

`GET /v1/broken-links/crawl/:jobId` — free and uncached. Returns the [standard job envelope](/async-jobs#response-shape). Once `status` is `"done"`, `result` holds a `SiteCrawlResult`: the same `summary`/`statusBreakdown`/`grade`/`healthScore` shape as the single-page result, plus:

<ResponseField name="result.pagesCrawled" type="integer" required>
  Internal pages fetched.
</ResponseField>

<ResponseField name="result.pagesDiscovered" type="integer" required>
  Internal pages known (crawled + queued).
</ResponseField>

<ResponseField name="result.totalUniqueLinks" type="integer" required>
  Unique links found across every crawled page.
</ResponseField>

<ResponseField name="result.truncatedPages" type="boolean" required>
  `true` if the 250-page cap was hit.
</ResponseField>

<ResponseField name="result.truncatedLinks" type="boolean" required>
  `true` if the 1,500-link check cap was hit.
</ResponseField>

<ResponseField name="result.links" type="array" required>
  Same shape as the single-page `links`, plus `sources` (sample source pages, capped at 25) and `sourceCount` (total pages the link appeared on).
</ResponseField>

<ResponseField name="result.pages" type="array" required>
  Every page crawled: `url`, `statusCode`, `statusText`, `severity`, `linksOnPage`, `depth` (BFS depth from the entry URL), `fromSitemap`.
</ResponseField>

<RequestExample>
  ```bash Start crawl theme={null}
  curl -X POST https://api.pocketutils.com/v1/broken-links/crawl \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url": "https://example.com"}'
  ```

  ```bash Poll status theme={null}
  curl https://api.pocketutils.com/v1/broken-links/crawl/c3d4e5f6-1234-4a5b-8c9d-0e1f2a3b4c5d \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```
</RequestExample>

<ResponseExample>
  ```json In progress theme={null}
  {
    "success": true,
    "data": {
      "jobId": "c3d4e5f6-1234-4a5b-8c9d-0e1f2a3b4c5d",
      "status": "crawling",
      "progress": {
        "phase": "crawling",
        "message": "Crawling internal pages…",
        "sitemapFound": true,
        "sitemapUrlCount": 58,
        "pagesCrawled": 22,
        "pagesDiscovered": 58,
        "pagesCap": 250,
        "linksFound": 340,
        "linksChecked": 0,
        "linksTotal": 0
      }
    }
  }
  ```
</ResponseExample>

## Errors

| Status | Cause                                                     |
| ------ | --------------------------------------------------------- |
| `400`  | `url` missing from the request body                       |
| `404`  | `jobId` doesn't exist or its 30-minute window has expired |

See the full [error reference](/errors) for the platform-level codes (`401`, `402`, `429`) that apply to every endpoint.


## Related topics

- [Link Extractor](/api-reference/link-extract.md)
- [Async Jobs & Polling](/async-jobs.md)
- [Introduction](/index.md)
- [Rate Limits & Credits](/rate-limits-and-credits.md)
- [Down Checker](/api-reference/down-check.md)
