# Broken Link Checker (`arched_friend/broken-link-checker`) Actor

Crawl any website and find every broken link, dead image and redirect chain, reported with the exact page and anchor text each one sits on. Covers internal and external links, and tells a genuine 404 apart from a site that simply blocks robots.

- **URL**: https://apify.com/arched\_friend/broken-link-checker.md
- **Developed by:** [Peach O](https://apify.com/arched_friend) (community)
- **Categories:** SEO tools, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 page crawleds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Broken Link Checker: Find Every Dead Link on Your Site

Point it at a website and it crawls the pages, follows every link and image, and tells you which ones are dead — with the exact page each broken link sits on and the anchor text a visitor clicks.

Built for SEO teams, agencies and site owners who need the list of what to fix, not a score out of 100.

### How it works

```mermaid
flowchart LR
    A["Your website"] --> B["Crawl pages<br/>(canonical URLs only)"]
    B --> C["Collect every link<br/>and image"]
    C --> D["Check each target once"]
    D --> E{"What came back?"}
    E -->|404, 410, 5xx, no DNS| F["broken"]
    E -->|403, 429, timeout| G["unverified"]
    E -->|2xx or 3xx| H["ok"]
    F --> I[("Fix list")]
    G --> I
```

Every distinct target is checked **once**, however many pages link to it, so a site-wide footer link costs one request rather than one per page.

### It does not cry wolf

Most link checkers produce reports nobody trusts. Three deliberate choices here:

- **A 403 is not a broken link.** Plenty of large sites refuse automated requests while serving the page perfectly to a real visitor. Those are reported as `unverified`, separately from genuine breakage, so your 404 list stays short and real.
- **`mailto:`, `tel:` and `#anchor` links are skipped**, not reported as dead URLs.
- **Tracking parameters are stripped before crawling.** Without that, every `?utm_source=` variant of a page gets crawled as if it were a new page and the crawl balloons.

### Built for

- **SEO teams** clearing 404s that waste crawl budget and leak link equity
- **Agencies** running a site audit before a pitch or a handover
- **Site owners** after a migration, when internal links break silently
- **Content teams** whose older posts link to sites that have since died

### Input

```json
{
  "startUrls": ["example.com"],
  "maxPages": 200,
  "checkExternalLinks": true,
  "checkImages": true
}
```

| Setting | What it does |
| --- | --- |
| `startUrls` | The site to check, as a domain or full URL |
| `maxPages` | How many pages to crawl. This is what the run is charged on |
| `maxDepth` | How many clicks from the start page to follow |
| `checkExternalLinks` | Also verify links pointing off your site, where most rot happens |
| `checkImages` | Verify every image source too |
| `excludePatterns` | Skip crawling anything matching, for example `/admin` |
| `reportWorkingLinks` | Return every link rather than only the problems |

### Output

```json
{
  "url": "https://example.com/no-such-page",
  "foundOn": "https://example.com/blog/old-post",
  "anchorText": "our old pricing page",
  "type": "link",
  "isInternal": true,
  "statusCode": 404,
  "state": "broken",
  "reason": "http-404",
  "redirected": false,
  "redirectedTo": null,
  "redirectChanged": false,
  "checkedAt": "2026-09-17T15:58:12.004Z"
}
```

`state` is the field to filter on: `broken`, `unverified` or `ok`. The run summary also names the ten pages carrying the most breakage, which is the list to work through first.

### Redirects worth knowing about

A link that still returns 200 can still be a problem. `redirectChanged` is true when a link lands on a **different domain** than it pointed at, which is the usual signature of an expired domain that now serves a parked page or something worse. Those are returned even when you ask only for problems.

### Run it as an API

```bash
curl -X POST "https://api.apify.com/v2/acts/arched_friend~broken-link-checker/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "startUrls": ["example.com"],
    "maxPages": 100,
    "checkExternalLinks": true
  }'
```

### Pricing

You pay $0.003 for each page crawled. Checking the links on those pages is included, however many there are — one run above checked 787 links across 25 pages.

| Auditing a 500-page site monthly | Monthly cost |
| --- | --- |
| Checking by hand | not realistically possible |
| Most SEO suites | $99 to $500 per month |
| This Actor | $1.50 per run |

### Common questions

**Why is a link marked `unverified` rather than broken?** The server would not confirm it either way: a 403 to bots, a 429 rate limit, or a timeout. Those need a human glance; they are not automatically broken.

**Does it respect robots.txt?** It identifies itself honestly as a link checker and crawls only the site you point it at. Use `excludePatterns` to keep it out of any area you do not want crawled.

**Can it check a site behind a login?** No. It checks what a logged-out visitor and a search engine can reach, which is what matters for SEO.

**Why did it find fewer pages than my site has?** Either `maxPages` capped it, `maxDepth` stopped it, or those pages are not linked from anywhere it crawled. Orphan pages are invisible to a crawler by definition.

### Related products

- **Website Change Monitor** to watch the pages you just fixed for regressions
- **SEO Meta & Content Auditor** to find missing titles and duplicate descriptions
- **Domain & SSL Monitor** to catch the certificate and expiry problems behind dead links
- **Tech Stack Checker** to profile the sites you are auditing
- **Website Lead Extractor** to pull contacts from the sites you audit

# Actor input Schema

## `startUrls` (type: `array`):

The site to check, as a domain or full URL. Add more than one to check several sites in the same run.

## `maxPages` (type: `integer`):

Upper limit on how many pages of the site to open looking for links. This is what the run is charged on.

## `maxDepth` (type: `integer`):

How many clicks from the starting page to follow. 0 checks only the pages you list.

## `includeSubdomains` (type: `boolean`):

Count blog.example.com as part of example.com, both for crawling and for deciding what counts as an internal link.

## `checkExternalLinks` (type: `boolean`):

Also verify links pointing away from your site. These are where most rot happens, since you do not control them.

## `checkImages` (type: `boolean`):

Also verify every image source, which finds broken images as well as broken links.

## `excludePatterns` (type: `array`):

Do not crawl any URL containing one of these strings, for example /admin or ?print=. Links to them are still checked.

## `reportWorkingLinks` (type: `boolean`):

Return every link checked rather than only the problems. Useful for a full audit export, noisier for routine monitoring.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each link before recording it as a timeout.

## `maxConcurrency` (type: `integer`):

How many pages and links to handle at once. Links are checked by a worker pool, so one slow link never blocks the others. Lower this if a site rate limits you.

## `proxyConfiguration` (type: `object`):

Optional, and off by default. Checking links does not need a proxy; turn one on only if a site rate limits you during a large crawl.

## Actor input object example

```json
{
  "startUrls": [
    "apify.com"
  ],
  "maxPages": 200,
  "maxDepth": 5,
  "includeSubdomains": true,
  "checkExternalLinks": true,
  "checkImages": false,
  "reportWorkingLinks": false,
  "requestTimeoutSecs": 15,
  "maxConcurrency": 20
}
```

# Actor output Schema

## `links` (type: `string`):

One row per problem link, with the page it sits on, its anchor text, the status code and what went wrong.

## `runSummary` (type: `string`):

Counts of broken, unverified and working links, a breakdown by cause, and the ten pages carrying the most breakage.

# 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 = {
    "startUrls": [
        "apify.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arched_friend/broken-link-checker").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 = { "startUrls": ["apify.com"] }

# Run the Actor and wait for it to finish
run = client.actor("arched_friend/broken-link-checker").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 '{
  "startUrls": [
    "apify.com"
  ]
}' |
apify call arched_friend/broken-link-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arched_friend/broken-link-checker"
        }
    }
}
```

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/0I4LI2W5u3qGdRYcU/builds/LxMHiu6WGTiQAG0Tr/openapi.json
