# Link Extractor - All Links from Any Page with Anchor Text (`eliai/webpage-links-extractor`) Actor

Extracts every link from a list of page URLs with anchor text and nofollow, sponsored and ugc flags, so it works as a nofollow backlink checker. Returns absolute URLs, internal or external type, and mailto and tel lists as JSON. For SEO specialists, link builders and affiliate managers.

- **URL**: https://apify.com/eliai/webpage-links-extractor.md
- **Developed by:** [Broke to Built](https://apify.com/eliai) (community)
- **Categories:** Developer tools, SEO tools, Automation
- **Features:** Limited permissions
- **Stats:** 6 total users, 3 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 page scans

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Link Extractor & Nofollow Backlink Checker: every link with anchor text and rel flags

**Give it a page URL, get every `<a href>` on it** - resolved to an absolute URL, classified
internal vs external, with clean anchor text and the `rel` attributes that matter for SEO
(`nofollow`, `sponsored`, `ugc`). `mailto:` and `tel:` links come back in their own lists.

One record per page, all links nested inside it, **$0.01 per page scanned** no matter how many
links the page turns out to have. No API key, no browser, no crawl to configure.

***

### What you get, per page

```
counts   total, internal, external, nofollow, sponsored, ugc, mailto, tel
links[]  href, text, type, rel, target, nofollow, sponsored, ugc
mailto[] href, email, text
tel[]    href, tel, text
url, finalUrl, statusCode, redirected
```

- **Absolute URLs.** A page writes `/pricing`; you get `https://site.com/pricing`. Resolution
  honours a `<base href>` element if the page has one, and otherwise resolves against the **final**
  URL after redirects - so a link list from a redirected page still points at the right host.
- **Clean anchor text.** Nested tags stripped, HTML entities decoded (`&amp;`, `&#8217;`, `&mdash;`
  and ~60 named entities), whitespace collapsed. An anchor wrapping an icon and a span comes back
  as the words a human sees.
- **Internal vs external** by hostname (see the honest limit on subdomains below).
- **rel flags parsed, not just echoed.** `rel="nofollow sponsored"` becomes
  `nofollow: true, sponsored: true` as well as the raw string.
- **Deduplicated** on href + anchor text, so a nav link repeated in the footer appears once, while
  the same URL linked from two different phrases appears twice - which is what a link audit wants.
- **`mailto:` and `tel:` separated out** with the address and number already parsed. Fragment-only
  (`#top`), `javascript:`, `data:` and `about:` hrefs are dropped.

***

### Input

```json
{ "url": "https://apify.com/store" }
```

Bulk - one record and one charge per page:

```json
{ "urls": ["https://apify.com/store", "https://en.wikipedia.org/wiki/Hyperlink"], "maxUrls": 50 }
```

| Field | Type | Default | Notes |
|---|---|---|---|
| `url` | string | - | One page URL. A bare domain gets `https://` prepended. |
| `urls` | array of strings | - | Bulk list; takes precedence over `url`. |
| `maxUrls` | integer | `50` | Hard cap on pages processed, and therefore on spend. |

***

### Output - a real record

Verbatim from a live run on 2026-08-15, `links` truncated to the first three of 138:

```json
{
  "url": "https://apify.com/store",
  "finalUrl": "https://apify.com/store",
  "statusCode": 200,
  "redirected": false,
  "counts": {
    "total": 138, "internal": 90, "external": 48,
    "nofollow": 19, "sponsored": 0, "ugc": 0, "mailto": 0, "tel": 0
  },
  "links": [
    { "href": "https://apify.com/", "text": "", "type": "internal", "rel": null, "target": null, "nofollow": false, "sponsored": false, "ugc": false },
    { "href": "https://console.apify.com/sign-up", "text": "Get started", "type": "external", "rel": null, "target": null, "nofollow": false, "sponsored": false, "ugc": false },
    { "href": "https://console.apify.com/sign-in", "text": "Log in", "type": "external", "rel": null, "target": null, "nofollow": false, "sponsored": false, "ugc": false }
  ],
  "mailto": [],
  "tel": []
}
```

Two things in that record are worth pointing at, because they will surprise you otherwise:

- `console.apify.com` is typed **external**. Classification compares the **exact hostname**, so a
  subdomain of the same site counts as external. If you are auditing a site that spans
  `www.`, `blog.` and `app.` subdomains, group by hostname yourself rather than trusting `type`.
- The first link has `"text": ""`. That is a logo anchor wrapping an image - a real, correctly
  reported empty anchor, and exactly the kind of thing an accessibility or SEO audit is looking
  for.

The same run on `https://en.wikipedia.org/wiki/Hyperlink` returned **436 links** (288 internal,
148 external, 44 nofollow) in one record, for the same $0.01.

***

### Call it from code or an agent

```bash
curl -X POST "https://api.apify.com/v2/acts/EliAI~webpage-links-extractor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "content-type: application/json" \
  -d '{"urls":["https://example.com"],"maxUrls":50}'
```

Agents can call it as a tool over [Apify MCP](https://mcp.apify.com):

- **Capability:** list every hyperlink on one or many pages, with anchor text and rel flags
- **Required input:** `url` (string) or `urls` (array)
- **Returns:** one JSON record per page; `counts` for a summary, `links` for the detail
- **Bounded:** `maxUrls` caps the run; it does **not** follow links or crawl
- **Side effects:** none - one GET per page

***

### Example

A real run on 2026-10-31 with the input this page's form is prefilled with:

```json
{
  "url": "https://github.com",
  "urls": []
}
```

It finished in 1 second and returned 1 result. It was charged for 1 `page-scanned` event ($0.01 on the Free plan). The first result (long text and lists cut for display):

```json
{
  "url": "https://github.com",
  "finalUrl": "https://github.com/",
  "statusCode": 200,
  "redirected": true,
  "counts": {
    "total": 126,
    "internal": 99,
    "external": 27,
    "nofollow": 0,
    "sponsored": 0,
    "ugc": 0,
    "mailto": 0,
    "tel": 0
  },
  "links": [
    {
      "href": "https://github.com/",
      "text": "",
      "type": "internal",
      "rel": null,
      "target": null,
      "nofollow": false,
      "sponsored": false,
      "ugc": false
    },
    {
      "href": "https://github.com/login",
      "text": "Sign in",
      "type": "internal",
      "rel": null,
      "target": null,
      "nofollow": false,
      "sponsored": false,
      "ugc": false
    },
    {
      "href": "https://github.com/platform",
      "text": "GitHub Repository hosting, code review, and collaboration",
      "type": "internal",
      "rel": null,
      "target": null,
      "nofollow": false,
      "sponsored": false,
      "ugc": false
    },
    "… 123 more"
  ],
  "mailto": [],
  "tel": []
}
```

### Pricing

**$0.01 per page scanned** - one `page-scanned` event per URL that returned a usable HTML
response. **No actor-start fee**, and the price does not move with the number of links: the
138-link page and the 436-link page above each cost $0.01.

**Honest comparison, measured on the store 2026-08-15.** The shape of the price is the whole
difference here:

| Actor | Price shape | A 436-link page |
|---|---|---|
| **eliai/webpage-links-extractor** (this one) | $0.01 per page, no start fee | **$0.01** |
| `automation-lab/link-extractor` | $0.035 start + per-URL event | $0.035 before a single link |
| `ninhothedev/website-link-extractor` | $0.00005 start + $0.0005 per dataset item | $0.22 if each link is an item |
| `codescraper/website-social-links-scraper` | $0.00005 start + $0.0005 per item (+$0.002 per social profile) | scales the same way |

Per-dataset-item pricing gets cheaper than this on pages with **very few** links, and more
expensive as pages get link-heavy - a real site's navigation alone is often 80+ links. A flat
per-page price is predictable, which is the property that matters when a script or an agent is
deciding how many pages to scan.

### When NOT to use this

- **Crawling a site.** It does not follow links. Give it the URLs you want; to discover them,
  use [Sitemap URL Extractor](https://apify.com/eliai/sitemap-url-extractor).
- **Checking whether the links work.** It reports what the page links to, not what those URLs
  answer. For that use [Broken Link Checker](https://apify.com/eliai/broken-link-checker) or
  [Redirect Chain Checker](https://apify.com/eliai/redirect-chain-checker).
- **Links injected by JavaScript.** The served HTML is parsed with no browser, so anchors that a
  client-side framework renders after load are not there to find.
- **Treating subdomains as one site.** `type` compares exact hostnames - see the note above.
- **Harvesting email addresses at scale.** `mailto:` links are returned because a contact page's
  address is a legitimate part of a link audit; scraping addresses for unsolicited mail is not
  what this is for, and it is your obligation under GDPR/CAN-SPAM either way.
- **Pages behind a login or aggressive bot protection.** It fetches anonymously - no cookies, no
  session, no proxy rotation.

***

### Limits

- Default run settings: 4096 MB memory, 3600 s timeout. Change them per run under Options if a large input needs more.
- Time: the prefilled example finished in 1 s on 2026-10-31; run time grows with the number of inputs.
- What it is not built for: see *When NOT to use this* on this page.

### FAQ

**How do I get all the links on a webpage with their anchor text?**
Pass the URL. Every `<a href>` comes back as an absolute URL with clean anchor text, deduplicated,
with `mailto:` and `tel:` in their own lists - one JSON record for the whole page.

**Can it tell internal links from external ones?**
Yes, by hostname, with `counts.internal` and `counts.external` for the summary. Note that a
subdomain (`blog.site.com` seen from `site.com`) is typed external - group by hostname yourself if
you want registrable-domain semantics.

**Does it show nofollow, sponsored and UGC links?**
Yes - the raw `rel` string plus parsed booleans per link, and totals in `counts`. That is what a
link-equity or paid-placement audit needs.

**Why do absolute URLs matter?**
A relative href like `/pricing` is meaningless once the link leaves the page. Resolution respects
`<base href>` and the post-redirect URL, so every href in the output works standalone in a
spreadsheet, a crawler queue, or a model's context.

**Can I extract links from many pages at once?**
Yes - pass an array in `urls`, capped by `maxUrls` (default 50). One record and one $0.01 charge
per page, and one bad URL never stops the others.

**How are duplicate links handled?**
Deduplicated on href **plus** anchor text. The same nav link repeated in the footer collapses to
one entry; the same target linked from two different phrases stays as two, because those are two
different signals in an SEO audit.

**Does it follow the links it finds?**
No. It reads one page per URL you pass and does not crawl. That is what keeps the price flat and
the run bounded.

**What happens on a page that is not HTML, or that fails to load?**
You get a record with an `error` field instead of links, the run continues, and that URL is not
charged.

***

### Related actors

- [Broken Link Checker](https://apify.com/eliai/broken-link-checker) - crawl a page and find every dead link
- [Redirect Chain Checker](https://apify.com/eliai/redirect-chain-checker) - trace every 301/302 hop to the final URL
- [Outbound Link Auditor](https://apify.com/eliai/outbound-link-auditor) - audit who you link out to
- [Sitemap URL Extractor](https://apify.com/eliai/sitemap-url-extractor) - every URL a site declares
- [Webpage Text Extractor](https://apify.com/eliai/webpage-text-extractor) - the readable content of the same page
- [Contact Details Extractor](https://apify.com/eliai/contact-details-extractor) - emails, phones and socials from a site

Built by Broke to Built.

### Changelog

- 2026-11-01: Page text only: a clearer page heading, an Example section from a real run and a Limits section. Code, output and price are unchanged.

### For AI agents

This Actor is built to be called by software, not just by people.

- **Mount it directly as an MCP tool** — no Store search, no ranking, just this one tool:
  `https://mcp.apify.com/?actors=eliai/webpage-links-extractor`
- **Or call it over HTTP** and get the results in the same request:
  `POST https://api.apify.com/v2/acts/eliai~webpage-links-extractor/run-sync-get-dataset-items`
- **Costs are predictable before you call.** Pricing is pay-per-event (see Pricing above), so an agent can budget a run in advance instead of discovering the bill afterwards.
- **Send only the field you mean.** If you pass the bulk field, it is used on its own; the single-value field is a fallback, never merged into your request. You are charged for the items you sent and nothing else.

### Who uses it

- SEO specialists checking which outbound links on a page carry `nofollow` or `sponsored`
- Link builders confirming a placed backlink exists with the agreed anchor text
- Affiliate managers auditing a partner review page for missing `sponsored` attributes
- Scraper builders collecting a page's internal links to decide what to crawl next
- Sales researchers pulling `mailto:` and `tel:` links from a list of company pages

# Actor input Schema

## `url` (type: `string`):

A single page URL to extract links from.

## `urls` (type: `array`):

Multiple page URLs to process in one run.

## `maxUrls` (type: `integer`):

Safety cap on how many URLs to process.

## Actor input object example

```json
{
  "url": "https://github.com",
  "urls": [],
  "maxUrls": 50
}
```

# Actor output Schema

## `results` (type: `string`):

Every item this run produced. Export as JSON, CSV or Excel from the dataset view.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "url": "https://github.com",
    "urls": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("eliai/webpage-links-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "url": "https://github.com",
    "urls": [],
}

# Run the Actor and wait for it to finish
run = client.actor("eliai/webpage-links-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "url": "https://github.com",
  "urls": []
}' |
apify call eliai/webpage-links-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,eliai/webpage-links-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/g40TMzw0TZt5ov219/builds/Hv1dBV7i8WhFsNyQt/openapi.json
