# Broken Link Checker: Find Dead Links & 404 Errors on Websites (`spokentext/broken-link-checker`) Actor

Crawl a website and check every link: broken links, 404 errors, server errors, timeouts and redirect chains, with the page and anchor text where each bad link was found. Internal and external links. Pay per link checked.

- **URL**: https://apify.com/spokentext/broken-link-checker.md
- **Developed by:** [clement](https://apify.com/spokentext) (community)
- **Categories:** SEO tools, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 url checkeds

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

### What is Broken Link Checker?

**Broken Link Checker** crawls a website and **checks every link** it finds, internal and external, then lists the ones that fail:

- **broken links:** 404 and 410 errors, and links to domains that no longer exist
- **server errors** (5xx), **timeouts** and **unreachable** servers
- **redirect chains**, hop by hop, when you ask for them
- for each bad link, **the page where it was found and its anchor text**, so you know exactly what to fix

It is built for SEO specialists, webmasters, agencies and developers who audit sites, prepare migrations or check a site every week.

### Why use it?

- **Fix links in minutes:** every row says which page holds the bad link and what the link says.
- **No false alarms from bot blockers:** sites that refuse automated checks, such as LinkedIn (answers 999) or pages answering 403 or 429, are listed apart as `blocked`, not as broken.
- **Accurate checks:** links that refuse a light HEAD request are checked again with a full GET before they count as broken.
- **Redirects explained:** each hop and status (301, 302...) after a migration.
- **Polite crawling:** respects robots.txt and spreads requests.
- **Pay per link checked**, with a cap you set per site.

Because it runs on Apify, you also get scheduling (check your site every week), API access, integrations (Slack, email, Make, Zapier, n8n, Google Sheets) and exports to JSON, CSV or Excel.

### What data do you get?

| Field | What it tells you |
|---|---|
| `url` | The link that fails |
| `outcome` | `broken`, `server-error`, `timeout`, `unreachable`, `blocked` or `redirect` |
| `status`, `error` | The HTTP status, or the network error (`not-found-host`, `connection-refused`, `ssl-error`...) |
| `linkType` | `internal` (your site) or `external` |
| `linkText` | The link's anchor text |
| `foundOn` | Up to 10 pages that contain the link, with the link text on each |
| `finalUrl`, `redirects` | Where redirects lead, and each hop |

The **Summary** in the Output tab gives the totals per site: pages crawled, URLs checked, and how many were OK, broken, failing, blocked or redirected.

### How to use it

1. Enter one or more **websites**, such as `https://example.com`.
2. Set the **maximum pages to crawl** and the **maximum URLs to check** per site.
3. Choose whether to check **external links**, **images** and **redirects**.
4. Click **Start**. The **Output** tab lists the broken links, worst first.
5. To watch a site, save the run as a task and schedule it weekly, with a Slack or email alert.

### How much does it cost?

You pay per URL checked, pages and links alike, each once per run: **$0.0008** at the Bronze tier, less at Silver and Gold.

| Usage | Cost (Bronze) |
|---|---|
| A small site: 40 pages, about 300 URLs | $0.24 |
| A medium site: about 2,000 URLs | $1.60 |
| 500 URLs every week for a month | $1.60 |

Set **Maximum URLs to check per site** to cap what a run can cost.

### Input example

```json
{
    "startUrls": ["http://info.cern.ch/"],
    "maxPagesPerSite": 40,
    "maxLinksPerSite": 2000,
    "checkExternalLinks": true,
    "includeRedirects": true
}
```

### Output example

Real results for `info.cern.ch`, CERN's restored copy of the first website, captured on 2026-11-05: 40 pages crawled and 287 URLs checked in 31 seconds.

| Outcome | Links |
|---|---|
| OK | 191 |
| Broken | 66 (38 domains that no longer exist, 28 pages answering 404) |
| Unreachable | 29 |
| Blocked automated checks | 1 |
| Redirected | 10 |

A broken internal link:

```json
{
    "website": "http://info.cern.ch/",
    "url": "http://info.cern.ch/Help/Webmaster",
    "outcome": "broken",
    "status": 404,
    "error": null,
    "linkType": "internal",
    "kind": "link",
    "linkText": "Webmaster",
    "foundOn": [{ "page": "http://info.cern.ch/hypertext/DataSources/WWW/Servers.html", "text": "Webmaster" }],
    "finalUrl": null,
    "redirects": []
}
```

A redirect chain:

```json
{
    "url": "http://fatty.law.cornell.edu./usr2/wwwtext/lii.table.html",
    "outcome": "redirect",
    "status": 200,
    "linkType": "external",
    "linkText": "Cornell Law school",
    "finalUrl": "https://www.law.cornell.edu/",
    "redirects": [
        { "url": "http://fatty.law.cornell.edu./usr2/wwwtext/lii.table.html", "status": 301 },
        { "url": "http://www.law.cornell.edu/", "status": 301 }
    ]
}
```

### Use cases

- **SEO audits:** broken links waste crawl budget and frustrate visitors.
- **Site migrations:** list every redirect chain and every link that broke.
- **Weekly monitoring:** schedule a check and get an alert when a link breaks.
- **Content maintenance:** find dead external sources in blog posts and documentation.
- **Agency reporting:** one list of fixes per client site.

### FAQ

#### What counts as broken?

A 404 or 410 answer, any other 4xx answer except 401, 403 and 429, and a link to a domain that no longer exists. Server errors (5xx), timeouts and refused connections are reported with their own outcome, so you can tell a dead page from a server having a bad moment.

#### Why are some links "blocked"?

Some sites answer every automated request with 401, 403, 429 or 999 (LinkedIn), even for pages that work in a browser. They are listed apart, and you can hide them with **Report links that block automated checks**.

#### Does it run JavaScript?

No. It reads the HTML that the server sends, which holds the links on most sites. Links added only by JavaScript in the browser are not seen.

#### Does it follow robots.txt?

Yes, by default: pages that the site's robots.txt keeps crawlers out of are skipped.

### Limits

- Up to 20 websites per run, 10,000 pages crawled and 50,000 URLs checked per site.
- Pages larger than 3 MB are checked but their links are not read.

### Other Actors you may find useful

| Actor | What it does |
|---|---|
| [Website Contact Scraper & Email Extractor](https://apify.com/spokentext/website-contact-scraper) | Emails, phones and social profiles from a list of websites |
| [Keyword Search Volume Checker](https://apify.com/spokentext/keyword-search-volume) | Google search volume, CPC and difficulty for your keywords |
| [AI Share of Voice](https://apify.com/spokentext/ai-share-of-voice) | How often AI assistants recommend your brand |

### Run it from the API

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")
run = client.actor("spokentext/broken-link-checker").call(run_input={
    "startUrls": ["https://example.com"],
    "maxPagesPerSite": 200,
})
for link in client.dataset(run["defaultDatasetId"]).iterate_items():
    if link["outcome"] == "broken":
        print(link["url"], link["status"], "on", link["foundOn"][0]["page"])
```

### Feedback

Something failed or gave a wrong result? Open an issue in the **Issues** tab of this page, with the error message from your results: every report is read and fixed. If the Actor saved you time, a rating helps others find it.

# Actor input Schema

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

The websites to check, such as https://example.com. The crawl stays on each site and follows its internal links. Up to 20 per run.

## `maxPagesPerSite` (type: `integer`):

Pages of the site whose links are read. Links on other pages are still checked if they were found.

## `maxLinksPerSite` (type: `integer`):

Caps the cost: each URL checked, page or link, is charged once.

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

Also check links to other websites.

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

Also check the images used on the pages.

## `includeRedirects` (type: `boolean`):

Also list links that work but go through redirects, with each hop. Useful after a site migration.

## `includeBlocked` (type: `boolean`):

Some sites, such as LinkedIn, answer 403, 429 or 999 to any automated request. They are listed apart as "blocked", not as broken.

## `respectRobotsTxt` (type: `boolean`):

Skip the site's pages that its robots.txt keeps crawlers out of.

## Actor input object example

```json
{
  "startUrls": [
    "http://info.cern.ch/"
  ],
  "maxPagesPerSite": 40,
  "maxLinksPerSite": 2000,
  "checkExternalLinks": true,
  "checkImages": false,
  "includeRedirects": false,
  "includeBlocked": true,
  "respectRobotsTxt": true
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

# 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": [
        "http://info.cern.ch/"
    ],
    "maxPagesPerSite": 40
};

// Run the Actor and wait for it to finish
const run = await client.actor("spokentext/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": ["http://info.cern.ch/"],
    "maxPagesPerSite": 40,
}

# Run the Actor and wait for it to finish
run = client.actor("spokentext/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": [
    "http://info.cern.ch/"
  ],
  "maxPagesPerSite": 40
}' |
apify call spokentext/broken-link-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,spokentext/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/6gkAYbVQALHpBn9ub/builds/WOHhlmFEmO7H3vEAD/openapi.json
