# Link Extraction API

> Fetches a live public page and lists every link on it: the target URL, anchor text, `rel` values (nofollow, sponsored, ugc), whether it is internal, and how often it appears. One page per GET, or up to 600 pages in a job.

Need the page's text or metadata too? Use [Scrape](https://rankfield.dev/endpoints/scrape) with `include=links,metadata`.

- Page: https://rankfield.dev/endpoints/html-links
- Full reference: https://rankfield.dev/docs/link-extraction/extract-links

## Request

`GET https://rankfield.dev/v1/html/link-extraction?url={url}`

Send `Authorization: Bearer YOUR_API_KEY`. Get a free key at https://rankfield.dev/?tab=key#playground (no account).

```bash
curl "https://rankfield.dev/v1/html/link-extraction?url=https://rankfield.dev/blog/what-is-site-strength" \
  -H "Authorization: Bearer $RANKFIELD_API_KEY"
```

Response:

```json
{
  "url": "https://rankfield.dev/blog/what-is-site-strength",
  "finalUrl": "https://rankfield.dev/blog/what-is-site-strength",
  "status": 200,
  "title": "What is Site Strength? — Rankfield",
  "redirects": [],
  "collectedAt": "2026-10-25T14:35:34.625Z",
  "cached": false,
  "counts": { "links": 17, "internal": 10, "external": 7, "occurrences": 32 },
  "links": [
    {
      "href": "https://rankfield.dev/",
      "anchor": "Rankfield",
      "internal": true,
      "rel": [],
      "nofollow": false,
      "sponsored": false,
      "ugc": false,
      "occurrences": 3
    },
    …
  ],
  "view": "unique",
  "jobId": "01a1737d-00f5-7896-a257-8bd5c7886d77"
}
```

## Parameters

| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
| `url` | query | string | yes |  | A public `https://` page. `http://` is upgraded and a missing scheme gets `https://`. Credentials, custom ports and private hosts are refused. |
| `view` | query | string | no | `unique` | `unique`: one entry per target, with `occurrences`. `occurrences`: one entry per link, in page order. |
| `type` | query | string | no | `all` | `all`, `internal` (same hostname as the final URL) or `external`. `counts` always cover the whole page. |
| `maxAge` | query | integer | no | `60` | Seconds, 0 to 3,600. A copy collected at most this long ago may be reused (`cached: true`); `0` always fetches. |
| `wait` | query | integer | no | `60` | Seconds to wait for the page, 1 to 85. Not ready by then: `202` with `resultsUrl`, where the answer arrives. |

## Response

| Field | Type | Description |
|---|---|---|
| `url, finalUrl, status, redirects` | string, integer, array | What you asked, the page that answered after redirects, its HTTP status and each redirect. |
| `title` | string \| null | The page's `<title>`. |
| `collectedAt, cached` | string, boolean | When the page was fetched, and whether this copy was reused (see `maxAge`). |
| `counts` | object | `links` (distinct targets), `internal`, `external`, `occurrences` (every link). |
| `links` | array | `href`, `anchor`, `internal`, `rel`, `nofollow`, `sponsored`, `ugc`, `occurrences`. |
| `view, jobId` | string | The view used, and the job that fetched the page. |

## Errors

Errors come as `{"error":{"message":"…"}}` with the HTTP status.

| Status | When |
|---|---|
| 202 | Still running after `wait` seconds. Read the answer at `resultsUrl`. |
| 400 | The URL or a parameter is not valid, e.g. `{"error":{"message":"Expected a public https:// page URL in ?url="}}`. |
| 401 | The key is missing, unknown or revoked. |
| 409 | A `jsonl` or `csv` download of a job that has not finished. |
| 413 | More than 600 URLs in one job. |
| 422 | The website answered with an error, timed out or sent something that is not HTML. The body has the page's `status` and `error` with `message`, `code` (such as `SOURCE_HTTP_404`) and `retryable`. |
| 429 | Busy right now: our capacity is full, or the website is being paced. Retry after `Retry-After`. |
| 503 | The service is busy or briefly unavailable. Retry after `Retry-After`. |

## Price and limits

- Free; no per-call charge.
- No per-key limits. A job holds up to 600 URLs; more is a `413`. A single-page GET is a one-URL job.

## Up to 600 pages in one job

`POST /v1/html/link-extraction/jobs` with `{"urls":[…]}` (1 to 600 URLs), `view`, `delivery` (`standard`, the default, or `priority`), `maxAge`, and an optional `Idempotency-Key` header: the same key and body return the same job instead of a new one. The answer is `202` with `statusUrl`, `resultsUrl` and `deadlineAt`.

The deadline is fixed per job, whatever its size: `priority` 90 seconds, `standard` 330 seconds (`deadlineAt` in the answer). By then every page is answered, as a result or as failed. Pages from one website are fetched at up to 5 a second, so about 400 pages of one site fit a `priority` job and about 1,500 a `standard` one; a job spread over many sites fits easily. For many pages of one site, use `standard`.

`GET /v1/html/link-extraction/jobs/{id}/results` returns pages as each one finishes, with `position` (its index in your list). Pass `nextCursor` back as `cursor` until it is `null`. Add `wait` (0 to 25 seconds) to hold the request until the next page is ready. A finished job also streams as `?format=jsonl` or `?format=csv`. Jobs are kept for 24 hours.

Reference: https://rankfield.dev/docs/get-started/batch-jobs

```bash
curl -X POST "https://rankfield.dev/v1/html/link-extraction/jobs" \
  -H "Authorization: Bearer $RANKFIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crawl-2026-10-25" \
  -d '{"urls":["https://example.com/","https://example.com/about"]}'
```

## Notes

- CSV rows, one per link: `page_url, page_status, final_url, href, anchor, internal, rel, nofollow, sponsored, ugc, occurrences, error`.

## MCP

Tools on https://rankfield.dev/mcp: `extract_page_links`, `start_link_extraction_job`, `get_link_extraction_results`.
