# Broken Link Checker: 404s, Redirect Chains & Sitemaps (`plainrows/broken-link-checker`) Actor

Crawl any website and find broken links (404, 500, dead domains), redirect chains and sitemap problems, with the page and anchor text of each bad link. Checks internal, external, image, script and CSS links. Fast HTTP crawler, no browser. Pay only per page crawled.

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

## Pricing

from $0.60 / 1,000 pages

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

## Broken Link Checker: 404s, Redirect Chains & Sitemaps

Find every **broken link** on a website: 404 and 410 pages, server errors, dead domains, redirect chains and loops, broken images, scripts and stylesheets, plus **sitemap errors** (sitemap URLs that redirect, return 404, are noindex or are linked from nowhere). Give it your home page and get one row per problem, with the **page it was found on and its anchor text**, so you know exactly what to fix.

It is a fast HTTP crawler (no browser): it reads your pages, checks each unique link once (HEAD, then GET when a server refuses HEAD), follows redirects one by one to show the full chain, and respects robots.txt. **You pay per page crawled, all link checks included.**

### Typical uses

- **SEO audits**: broken internal links, 404s, redirect chains and noindex pages before they cost you rankings and crawl budget.
- **Site migrations and redesigns**: run it after a launch to catch links still pointing to old URLs (301 chains) or to pages that no longer exist.
- **Sitemap checks**: find sitemap URLs that redirect, return errors, are noindex, or are not linked from any page (orphan pages).
- **Agencies and freelancers**: audit client sites in bulk (up to 100 websites per run) and feed the results to Google Sheets or a report.
- **Monitoring**: schedule a weekly run and get alerted when a new broken link appears.

### What you get for each problem link

| Field | Description |
|---|---|
| `issueType` | `broken` (4xx, 5xx, dead domain, SSL error, redirect loop), `redirect` (works, but through 301/302...), `blocked` (401/403/429/999: the server refuses automated checks, verify by hand), `timeout` |
| `statusCode`, `error` | Final HTTP status after redirects, or the network error (`Domain not found (DNS lookup failed)`, `Redirect loop`, ...) |
| `linkUrl`, `linkType`, `anchorText` | The link, its kind (`a`, `img`, `script`, `css`, `sitemap`, `start`) and its text (or image alt) |
| `sourcePage` | The page where the link was found (for sitemap rows, the sitemap file) |
| `isInternal`, `isNofollow`, `occurrences` | Same website or not, `rel="nofollow"`, how many times it appears on that page |
| `finalUrl`, `redirectCount`, `redirectChain` | Where redirects end and each step as `{url, statusCode}` |
| `contentType`, `responseTimeMs`, `checkedWith` | Content type, response time, HEAD or GET |
| `site`, `observedAt` | The website checked and when (UTC) |

Missing values are `null`, never guessed. A site where nothing is wrong gets one free marker row (`found: false`) saying so.

**Run summary (OUTPUT)**, per website: pages crawled, unique links checked (internal and external), broken links by status code, top broken links by number of pages, redirecting links and chains longer than one hop, blocked and timed-out links, robots.txt status, sitemaps read and their URL counts, **sitemap URLs not linked from any page** (when the whole site was crawled), noindex pages, and why the crawl stopped.

### Input

```json
{
  "startUrls": [{ "url": "https://www.example.com/" }],
  "maxPages": 500,
  "reportRedirects": "all",
  "checkExternalLinks": true,
  "checkResources": true,
  "useSitemaps": true
}
```

- **startUrls**: one or more websites (up to 100). Links on the same domain are followed, `www.` included; other domains are checked, never crawled. A URL ending in `.xml` is read as a sitemap.
- **maxPages**: HTML pages to crawl per website, 1 to 10,000 (default 200). Each page is billed once, whatever its number of links.
- **reportRedirects**: `all` (default), `internal` (only redirects on your site) or `none` (broken links only).
- **checkExternalLinks**, **checkResources**: also check links to other sites, and image/script/stylesheet URLs (both on by default).
- **useSitemaps**: read the sitemaps listed in robots.txt (or `/sitemap.xml`), crawl their URLs too and report sitemap problems.
- **Advanced**: `reportAllLinks` (one row per link, working ones included), `excludeUrlPatterns` (ignore URLs containing a text, e.g. `/tag/`), `respectRobotsTxt`, `maxConcurrency` (requests in parallel to your site, default 4), `requestTimeoutSecs` (default 15).

### Output example

Two real rows from a run on 2026-10-07 on [the-internet.herokuapp.com](https://the-internet.herokuapp.com/), a public test site full of deliberate errors (a broken image, and an internal link that redirects):

```json
[
  {
    "found": true,
    "site": "the-internet.herokuapp.com",
    "sourcePage": "https://the-internet.herokuapp.com/broken_images",
    "linkUrl": "https://the-internet.herokuapp.com/asdf.jpg",
    "linkType": "img",
    "anchorText": null,
    "issueType": "broken",
    "statusCode": 404,
    "error": null,
    "isInternal": true,
    "isNofollow": false,
    "occurrences": 1,
    "finalUrl": null,
    "redirectCount": 0,
    "redirectChain": [],
    "contentType": "text/html",
    "responseTimeMs": 26,
    "checkedWith": "GET",
    "observedAt": "2026-10-07T15:18:52.606Z",
    "schemaVersion": "1.0"
  },
  {
    "found": true,
    "site": "the-internet.herokuapp.com",
    "sourcePage": "https://the-internet.herokuapp.com/",
    "linkUrl": "https://the-internet.herokuapp.com/notification_message",
    "linkType": "a",
    "anchorText": "Notification Messages",
    "issueType": "redirect",
    "statusCode": 200,
    "error": null,
    "isInternal": true,
    "isNofollow": false,
    "occurrences": 1,
    "finalUrl": "https://the-internet.herokuapp.com/notification_message_rendered",
    "redirectCount": 1,
    "redirectChain": [
      {
        "url": "https://the-internet.herokuapp.com/notification_message",
        "statusCode": 303
      },
      {
        "url": "https://the-internet.herokuapp.com/notification_message_rendered",
        "statusCode": 200
      }
    ],
    "contentType": "text/html",
    "responseTimeMs": 40,
    "checkedWith": "HEAD",
    "observedAt": "2026-10-07T15:18:52.606Z",
    "schemaVersion": "1.0"
  }
]
```

### Pricing

**Pay per page crawled**, no subscription, no fee per run, no proxy costs. One event per HTML page of your site, with every link on it checked (internal, external, images, scripts, stylesheets): see the **Pricing** tab, cheaper on higher Apify plans. For comparison, a 500-page site costs about $0.50 on the Bronze plan, however many links it has.

Free: result rows, link checks, redirects, broken internal URLs, PDFs and other non-HTML files, sitemap reading and the run summary. The run never spends more than the maximum cost you set: it stops crawling cleanly and keeps everything found so far.

**Free Apify plan:** up to 50 crawled pages per run, so you can check the output. A paid Apify plan removes this limit.

### Use it from code, automations or AI agents

**API (one call, results returned directly):**

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

**n8n, Make, Zapier:** use the Apify integration, pick `plainrows/broken-link-checker`, and send new broken links to Slack, Google Sheets or your ticketing tool. Schedule it weekly in Apify to monitor a site.

**AI agents (MCP):** add `https://mcp.apify.com?tools=plainrows/broken-link-checker` to Claude, Cursor or any MCP client. The agent gets a typed tool and pays per page crawled.

### What this is not

- **Not a browser.** Links added by JavaScript after the page loads are not seen; pages that only render with JavaScript show few links. Most sites (WordPress, Shopify, Webflow, static and server-rendered sites) are fully covered.
- **No login.** Pages behind a login are not crawled; links to them show as `blocked` (401/403).
- **`blocked` is not `broken`.** Sites such as LinkedIn, Stack Overflow or some shops refuse automated requests (403, 429, 999). They are listed separately so they don't drown real errors; check them in a browser if they matter.
- Not a full SEO suite: no page titles audit, no Core Web Vitals, no keyword data.

### Good to know

- Polite by default: at most 4 parallel requests to your site (adjustable), 3 to any other site, robots.txt rules and crawl-delay respected (up to 5 s) for crawling. Links to pages disallowed by robots.txt are still checked once, like a visitor clicking them. A robots.txt that answers with a server error (5xx) means no page of that host is crawled, as RFC 9309 requires: retry later, or set `respectRobotsTxt` to false for your own site.
- Each unique URL is checked once per run, even if hundreds of pages link to it; a domain that does not exist is not retried for every link.
- The User-Agent is `PlainRowsLinkChecker/1.0`: allow it in your firewall if your own site blocks bots.

### Run summary

Each run stores the summary described above in the key-value store (`OUTPUT`), with per-site counts, top broken links, sitemap and noindex findings, and why the crawl stopped (`all pages crawled`, `maxPages reached`, `maximum cost reached`, `free plan limit`, `run time limit`, `no HTML page reachable`, `too many URLs that are not pages`: the crawl stops when far more URLs turn out to be files or broken pages, which are free, than pages crawled).

### FAQ

**How do I find broken links on my website?**
Enter your home page in **startUrls** and run: one row per problem (404, server error, dead domain, redirect chain or loop, broken image or script), with the page it was found on.

**How many pages does it crawl?**
1 to 10,000 HTML pages per website (**maxPages**, default 200). Each crawled page is billed once, whatever its number of links.

**Does it check links to other websites?**
Yes, with **checkExternalLinks** (on by default): other domains are checked, never crawled.

**Does it check my sitemap?**
Yes, with **useSitemaps**: sitemap URLs that redirect, return 404, are noindex or are linked from nowhere are reported.

**Why are some links marked `blocked`?**
Some sites refuse automated requests (403, 429, 999). They are listed apart from real errors so you can check them by hand.

### More PlainRows tools

Same pay-per-result pricing, same clean JSON, built to chain together in n8n, Make, Zapier or AI agents.

- **SEO and keywords:** [Keyword Search Volume, CPC & Difficulty](https://apify.com/plainrows/keyword-search-volume) · [Keyword Ideas Generator](https://apify.com/plainrows/keyword-ideas-generator) · [Google Autocomplete & People Also Ask](https://apify.com/plainrows/google-autocomplete-questions) · [Google Trends Explorer](https://apify.com/plainrows/google-trends-explorer) · [Website Organic Traffic & Top Keywords](https://apify.com/plainrows/domain-seo-overview) · [Bulk Domain Authority Checker](https://apify.com/plainrows/domain-authority-checker) · [Backlink Checker](https://apify.com/plainrows/backlink-checker) · [AI Search Visibility Tracker (ChatGPT, Gemini, AI Mode)](https://apify.com/plainrows/ai-search-brand-visibility) · [Google Maps Rank Tracker (Geo-Grid Local SEO)](https://apify.com/plainrows/google-maps-rank-tracker)
- **Leads and companies:** [Google Maps Scraper](https://apify.com/plainrows/google-maps-businesses) · [Website Technology Checker](https://apify.com/plainrows/website-tech-stack) · [ATS Jobs Scraper (Workday, Greenhouse, Lever, Ashby)](https://apify.com/plainrows/ats-job-postings) · [Google Jobs Scraper](https://apify.com/plainrows/google-jobs-scraper) · [Google Ads Scraper (Transparency Center)](https://apify.com/plainrows/google-ads-transparency) · [Company Registry Lookup (SIRENE, LEI, Brreg)](https://apify.com/plainrows/company-registry-lookup) · [Public Tenders Search (EU TED & UK)](https://apify.com/plainrows/public-tenders-search)
- **E-commerce and travel:** [Amazon Product Search Scraper](https://apify.com/plainrows/amazon-product-search) · [Google Shopping Prices Scraper](https://apify.com/plainrows/google-shopping-prices) · [Google Hotels Prices Scraper](https://apify.com/plainrows/google-hotels-prices) · [Tripadvisor Reviews Scraper](https://apify.com/plainrows/tripadvisor-reviews)
- **Content and media:** [Google News Scraper](https://apify.com/plainrows/google-news-scraper) · [YouTube Search Scraper](https://apify.com/plainrows/youtube-search-scraper) · [YouTube Comments Scraper](https://apify.com/plainrows/youtube-comments-scraper) · [Google Images Scraper](https://apify.com/plainrows/google-images-scraper) · [Website Screenshot API](https://apify.com/plainrows/website-screenshot) · [Google Maps Reviews Scraper](https://apify.com/plainrows/google-maps-reviews) · [App Store & Google Play Reviews Scraper](https://apify.com/plainrows/app-reviews-scraper)

### Support

Missing a check or found a false positive? Open an issue on this page. Issues are read and answered.

# Actor input Schema

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

Home page (or any page) of each website to check, e.g. https://example.com. The crawler follows links on the same domain (www. included). You can also add a sitemap URL ending in .xml. Up to 100 URLs per run.

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

Stop after crawling this many HTML pages per website. Each page crawled is billed once, with all its links checked; see Pricing.

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

Links that answer with a redirect (301, 302, ...) still work but slow pages down and waste crawl budget. Choose which ones to list.

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

Also check links pointing to other websites (they are checked, never crawled).

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

Also check <img>, <script> and stylesheet URLs, not only <a> links.

## `useSitemaps` (type: `boolean`):

Read the sitemaps listed in robots.txt (or /sitemap.xml), crawl their URLs too, and report sitemap URLs that are broken, redirected, noindex or not linked from any page.

## `reportAllLinks` (type: `boolean`):

One row per link found on each page, including working ones (issueType "ok"). Useful for a full link inventory. Rows are free either way.

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

URLs containing any of these texts are neither crawled nor checked, e.g. "/tag/", "?replytocom=", "/wp-admin". Case-insensitive.

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

Do not crawl pages that robots.txt disallows (links to them are still checked). Turn off only for your own site.

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

How many requests the crawler sends at once to the website being checked. Keep it low for small servers. Other websites get at most 3.

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

A link that does not answer within this time (twice) is reported as "timeout".

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://the-internet.herokuapp.com/"
    }
  ],
  "maxPages": 200,
  "reportRedirects": "all",
  "checkExternalLinks": true,
  "checkResources": true,
  "useSitemaps": true,
  "reportAllLinks": false,
  "respectRobotsTxt": true,
  "maxConcurrency": 4,
  "requestTimeoutSecs": 15
}
```

# Actor output Schema

## `results` (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": [
        {
            "url": "https://the-internet.herokuapp.com/"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("plainrows/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": [{ "url": "https://the-internet.herokuapp.com/" }] }

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

```

## MCP server setup

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