# Broken Link Checker: Find Dead Links and 404s on Any Website (`planetlinux98/broken-link-checker`) Actor

Crawl a website and check every link and image once: 404s, dead domains, server errors, redirects and missing #sections. Get a fix list grouped by broken link, with every page and link text, a plain-language report, and monitoring for new problems.

- **URL**: https://apify.com/planetlinux98/broken-link-checker.md
- **Developed by:** [Elliott Tyson](https://apify.com/planetlinux98) (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

from $2.00 / 1,000 page scanneds

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** crawls a website and checks **every link on every page**, once each: links to its own pages, links to other websites, and (if you like) images, scripts, stylesheets, videos and #section links. You get a **fix list grouped by broken link**, with every page that links to it and the link text people see there, so you know exactly what to change and where.

It keeps **"broken" apart from "couldn't check"**. A link that answers 404 is broken; a link to a site that turns away automated checks is not, so it isn't counted as broken. Every result is explained in plain words, with a suggested fix and a readable HTML report you can hand to whoever maintains the site.

It's built for website owners, SEO specialists, agencies and developers who want a clean site, and for anyone who wants a **weekly check that only reports new problems**.

### What can this broken link checker do?

- **Check a whole website:** it follows the site's own links from the start page, respecting robots.txt, and checks every link it finds. Or scan only the pages in the sitemap, only the pages you list, or just check a list of URLs.
- **Fix list grouped by broken link:** one row per broken address, with every page that links to it (up to 50 listed) and the link text on each. Links in the **header, footer or menu** (on most pages) are marked, so you fix them once in the template.
- **Broken, warning or couldn't check:** 404s, removed pages, server errors, domains that no longer exist, refused connections, invalid certificates, redirect loops and malformed addresses are **broken**. Old addresses that redirect, "page not found" pages that answer OK, missing #sections and http links on an https site are **warnings**. Bot protection, robots.txt, logins and timeouts are **couldn't check**, never "broken".
- **Every link checked properly, once:** a real GET request (many servers answer HEAD requests wrongly), redirects followed one by one with the full chain (keeping cookies along the way, like a browser), and each address checked once per run however many pages link to it. An old `http://` link whose site now only answers on `https://` is a warning to update, not "broken".
- **Images and files too (optional):** broken images, scripts and stylesheets, and insecure http files that browsers block on https pages.
- **Missing #sections (optional):** for links like `/guide#install`, checks that the page really has that section.
- **Plain-language report:** an HTML report with what's broken, where, and how to fix it, worst and most widespread first.
- **Monitoring:** schedule it weekly and get only **new** problems, plus a list of problems **fixed** since last time.
- **Orphan pages:** in "Both" mode it also lists pages in the sitemap that no page links to.

### How to find broken links on a website

1. Click **Try for free** (you'll need a free Apify account).
2. Add one or more websites, one per line, for example `example.com`. To check one section only, give its address, like `https://example.com/blog/`.
3. Choose how many pages to scan, and optionally turn on images and files, #section links, or monitoring.
4. Click **Start**.
5. Open the **Broken links** tab for the fix list, or the **Report** for a readable summary. Download results as JSON, CSV or Excel, or send them to Google Sheets, Slack, Make or Zapier.

### Input example

```json
{
    "startUrls": ["https://crawlee.dev"],
    "mode": "crawl",
    "maxPagesPerWebsite": 500,
    "checkExternalLinks": true,
    "checkResources": true,
    "checkAnchors": false,
    "reportRedirects": "internal"
}
```

### Output example

![The plain-language report from a real run on crawlee.dev with the default settings. Summary: checked 20 pages and 247 links, no broken links found, and 14 links couldn't be checked, which aren't counted as broken. Notes explain that pages beyond the page limit, and most of 638 GitHub addresses, weren't checked. Below, the "Links we couldn't check" table: a Twitter link that robots.txt asks bots not to load, and a Stack Overflow link behind Cloudflare bot protection, each marked as being in the header, footer or menu, with the pages and link text where they appear.](https://api.apify.com/v2/key-value-stores/Q61NH7quzCxD8IodC/records/broken-link-checker-results.png)

Each row of the **Broken links** fix list is one address with a problem, with the pages that link to it (shortened):

```json
{
    "url": "https://example.com/spring-sale",
    "website": "example.com",
    "result": "broken",
    "problems": ["not-found"],
    "problemSummary": "Page not found (404).",
    "howToFix": "Update the link to the right page, or remove it. If the page moved, add a redirect from the old address. It's on most pages (probably the header, footer or menu), so fixing it there fixes it everywhere.",
    "statusCode": 404,
    "linkType": "internal",
    "kind": "link",
    "timesLinked": 212,
    "foundOnPageCount": 212,
    "siteWide": true,
    "foundOn": [
        { "pageUrl": "https://example.com/", "linkText": "Spring sale", "kind": "link" },
        { "pageUrl": "https://example.com/about", "linkText": "Spring sale", "kind": "link" }
    ]
}
```

A link to a site that turns away automated checks is listed apart, and not counted as broken:

```json
{
    "url": "https://stackoverflow.com/questions/tagged/crawlee",
    "result": "not-checked",
    "problems": ["bot-protection"],
    "problemSummary": "The site's bot protection (Cloudflare) turns away automated checks, so we could not tell whether the link works.",
    "howToFix": "Open the link in a browser to check it by hand.",
    "statusCode": 403,
    "linkType": "external",
    "foundOnPageCount": 16
}
```

The **Pages scanned** tab has one row per page with its own counts and problem links, and the **Websites** tab has one row per site with totals and a short summary. From the same real run (shortened):

```json
{
    "website": "https://crawlee.dev",
    "status": "ok",
    "summary": "Checked 20 pages and 247 links. No broken links found. 14 links couldn't be checked (for example, the site turns away automated checks); those aren't counted as broken.",
    "pagesScanned": 20,
    "uniqueLinks": 247,
    "brokenLinks": 0,
    "warnings": 0,
    "notChecked": 14,
    "problems": [
        { "code": "bot-protection", "result": "not-checked", "links": 8, "message": "The site turns away automated checks, so we could not tell whether the link works." },
        { "code": "robots-txt", "result": "not-checked", "links": 5, "message": "The site's robots.txt asks bots like ours not to load this address, so we respected that." }
    ]
}
```

### How much does it cost?

You pay **$2.00 per 1,000 pages scanned**, plus a tiny start fee per run. **Every link on those pages is checked at no extra cost**, however many there are. In "Only the URLs I list" mode, you pay **$1.00 per 1,000 URLs checked**.

- A 50-page small business site: about $0.10
- A 500-page website: about $1.00
- A weekly monitor of a 200-page site: about $1.70 a month

You aren't charged for websites that block us or can't be reached, or for pages that fail to load. Apify's free plan includes $5 of monthly credit, which covers about 2,500 pages. To cap a run, set **Max pages to scan** or a maximum cost when you start it.

### Sites that block bots

Some websites use bot protection (Cloudflare, Akamai, DataDome and similar) that turns away automated tools. This Actor identifies itself honestly and never tries to get around these protections.

- **The website you're checking:** it loads the start page first, so a site that blocks us is stopped within seconds, listed as `blocked` in the Websites tab, and **not charged**. If it's your own site, allow our bot (its name starts with `BrokenLinkChecker`) in your bot protection settings.
- **Other websites your pages link to:** if one of them turns us away, that link is listed under **couldn't check**, not as broken. Open those in a browser to check them by hand.

### Good to know

- **Polite by default:** at most 4 requests a second to the website being checked (you can raise it for your own site), and **at most one a second to each other website**. A site's robots.txt Crawl-delay always wins.
- **robots.txt is respected,** for the website being checked and for the other websites it links to. Links that robots.txt asks bots not to load are listed under couldn't check.
- **LinkedIn and Meta sites** (Facebook, Instagram, WhatsApp, Threads) are never contacted; links to them are listed under couldn't check.
- **Page limit:** we stop scanning new pages at **Max pages to scan**, but the links on the pages already scanned are still checked, including the most linked pages beyond the limit. When a site links to one other website hundreds of times (for example its code on GitHub), we check a sample to stay polite, and the Websites tab says how many.
- **Long runs end cleanly:** a run stops shortly before its time limit and saves everything found so far, so it ends successfully.
- **JavaScript websites:** we read each page's HTML without running its scripts. Sites that build their links with JavaScript may show only a few links; the Websites tab says when that seems to be the case.
- **No personal data is collected.** Only addresses, link text and page titles.

### FAQ

#### How is this different from other broken link checkers?

Most report every link they couldn't load as broken, so sites behind bot protection fill the report with false alarms. This one keeps "broken" and "couldn't check" apart, groups the fix list by broken link with every page and its link text, marks links in the site-wide header or footer, explains each problem in plain words, and charges per page rather than per link.

#### Can I check only some pages, or a list of URLs?

Yes. Choose **Only the pages I list** to check the links on specific pages, or **Only the URLs I list** to check that each address on a list works (for example after a site move). You can also limit scanning to a section with **Only scan pages containing**.

#### Can it tell me about new broken links every week?

Yes. Turn on monitoring and schedule the run weekly. The first run reports everything; after that you get only new problems, and the Websites tab lists the links fixed since last time.

#### Can I use it through the API or with AI agents?

Yes. Start runs and read results through the Apify API (JavaScript and Python clients are available), call it from AI agents through Apify's MCP server, or schedule it and send results to Google Sheets, Slack, Make or Zapier.

#### Something's wrong or unclear?

Please open an issue on the **Issues** tab with the website and what you expected.

# Actor input Schema

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

One per line. Type a website (<code>example.com</code>) and we follow its links from the homepage, checking every link on every page. To check one section only, give its address (<code>https://example.com/blog/</code>): only pages under it are scanned, though links from them to anywhere are checked.

## `mode` (type: `string`):

<b>Whole website</b> follows the site's own links from the start page. <b>Pages in the sitemap</b> scans the pages the site's sitemap lists. <b>Both</b> does both, so pages missing from either side are covered, and lists sitemap pages that nothing links to. <b>Only the pages I list</b> checks the links on each page you give, without following them. <b>Only the URLs I list</b> checks that each address on your list works (one result per address).

## `maxPagesPerWebsite` (type: `integer`):

We stop scanning new pages of a website after this many, but every link on the pages already scanned is still checked. Each page scanned is charged; the links on it are checked at no extra cost.

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

Also check links that leave the site. We ask each other website at most once a second, and respect its robots.txt.

## `checkResources` (type: `boolean`):

Also checks everything a page loads, not just its links. Finds broken images and files, and insecure http files that browsers block on https pages.

## `checkAnchors` (type: `boolean`):

For links like <code>/guide#install</code>, also checks that the page has a section with that id. A missing section is a warning: the page opens, but at the top instead of the right spot.

## `reportRedirects` (type: `string`):

A link that redirects still works, but pointing it at the final address saves a hop. <b>Links within the site</b> is usually what you can fix; other sites redirect all the time.

## `includeWorkingLinks` (type: `boolean`):

Adds a row for every link checked, not only those with problems. Handy for a full link inventory. Doesn't change the price.

## `includeUrlPatterns` (type: `array`):

Scan only pages whose address contains one of these. Use <code>*</code> for any text, for example <code>/docs/</code> or <code>/products/*</code>. Links to other pages are still checked; they just aren't scanned for links of their own. The start page is always scanned.

## `excludeUrlPatterns` (type: `array`):

Skip scanning pages whose address contains any of these, for example <code>/tag/</code>, <code>?page=</code> or <code>/fr/</code>. Same rules as above.

## `ignoreLinkPatterns` (type: `array`):

Links that contain any of these aren't checked or reported at all, for example <code>/logout</code>, <code>/cart/add</code> or <code>example-partner.com</code>.

## `onlyNewProblems` (type: `boolean`):

Remembers the problems it has already reported for each website and returns only new ones. The Websites tab also lists links fixed since last time. The first run reports everything. Ideal with a weekly schedule. Monitor memory is kept in a key-value store called <code>broken-link-checker-monitors</code> in your account.

## `monitorName` (type: `string`):

Optional. Give each monitor a name (for example <code>main-site-weekly</code>) so several schedules can watch the same site separately.

## `maxRequestsPerSecond` (type: `integer`):

How fast we load pages from the website being checked. The default is polite for any site; only raise it for sites you own or manage. A site's robots.txt Crawl-delay always wins. Other websites that pages link to are never asked more than once a second.

## Actor input object example

```json
{
  "startUrls": [
    "https://crawlee.dev"
  ],
  "mode": "crawl",
  "maxPagesPerWebsite": 20,
  "checkExternalLinks": true,
  "checkResources": false,
  "checkAnchors": false,
  "reportRedirects": "internal",
  "includeWorkingLinks": false,
  "onlyNewProblems": false,
  "maxRequestsPerSecond": 4
}
```

# Actor output Schema

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

One row per linked address with a problem, with every page that links to it and the link text.

## `report` (type: `string`):

A plain-language report: what is broken, where, and how to fix it.

## `pages` (type: `string`):

One row per page scanned, with its broken links.

## `websites` (type: `string`):

One row per website: totals, problem types and notes.

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

Totals for the run, invalid entries, and anything not processed.

# 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": [
        "https://crawlee.dev"
    ],
    "maxPagesPerWebsite": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("planetlinux98/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": ["https://crawlee.dev"],
    "maxPagesPerWebsite": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("planetlinux98/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": [
    "https://crawlee.dev"
  ],
  "maxPagesPerWebsite": 20
}' |
apify call planetlinux98/broken-link-checker --silent --output-dataset

```

## MCP server setup

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