# Broken Link Checker: Bulk URL Status, Redirects & Source Pages (`hillcountrybots/broken-link-checker`) Actor

Find broken links on any site: crawl, URL list or sitemap. Status codes, redirect chains and the page each link is on. $0.80 per 1,000 URLs, failures free.

- **URL**: https://apify.com/hillcountrybots/broken-link-checker.md
- **Developed by:** [Hill Country Bots](https://apify.com/hillcountrybots) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.80 / 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.

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: Bulk URL Status, Redirects & Source Pages

Find **broken links** on any website in one run. This **broken link checker** crawls a site (or takes a pasted **URL list** or an **XML sitemap**), checks every unique link once, and tells you the **HTTP status code**, the full **redirect chain**, the **pages each link was found on**, the anchor text, and a plain verdict: **broken, redirect, blocked or ok**. Internal links, external links and (optionally) images, scripts and stylesheets. **Plain HTTP, no browser, respects robots.txt.** **$0.80 per 1,000 URLs checked. Invalid inputs, dead-domain lookups, timeouts and robots.txt skips are free.**

### What does this broken link checker do?

Pick one of three modes:

- **Crawl** (default): start at a homepage, follow internal links up to `maxPagesToCrawl` pages, and check every link found on those pages: internal, external and optionally assets
- **List**: check only the URLs you paste (bulk URL status checker, redirect checker)
- **Sitemap**: read the site's XML sitemap (robots.txt discovery, sitemap indexes, gzip) and check every URL it lists, so you catch dead pages your sitemap still advertises

For each unique URL you get:

- `statusCode` (final) and `firstStatusCode` (for example 301), `redirectCount` and the hop-by-hop `redirectChain`
- `foundOn`: the pages that link to it (up to 20) and `foundOnCount` (all of them), plus `anchorText`
- `verdict`: `broken`, `unreachable`, `blocked`, `redirect`, `ok` or `skipped`, and a simple `isBroken` true/false
- a ready-to-send **BROKEN_LINKS CSV** with one line per broken link and the page it sits on

### Who is it for?

- **SEO agencies and web designers:** a client-ready list of dead links and the exact page to fix each one on
- **Site owners and content teams:** catch 404s, dead outbound links and redirect chains before Google and visitors do
- **Developers and AI agents:** stable JSON through the Apify API or MCP, one row per URL, predictable price

### Quick start

1. Paste a homepage (for example `https://crawlee.dev`). The form is prefilled, so **Start** works right away.
2. Choose a mode. For a full site check, keep **Crawl** and raise `maxPagesToCrawl` and `maxUrlsToCheck`.
3. Click **Start**. Broken links come first in the dataset. Download JSON, CSV or Excel, or grab the `BROKEN_LINKS` CSV from the key-value store.

#### Sample output

From a real run on **November 12, 2026, at 7:58 AM CT** (America/Chicago): crawl mode on crawlee.dev, 25 pages crawled, 300 URLs checked in 46 seconds. Result: 1 broken external link, 50 redirects, 3 blocked by the target site (Stack Overflow answers bots with 403), 231 ok, 15 skipped because the target's robots.txt disallows bots.

| URL | Verdict | Status | Found on | Anchor text |
|---|---|---|---|---|
| https://jobops.app/ | broken | 402 | https://crawlee.dev/blog | JobOps |
| https://www.crawlee.dev/blog/launching-crawlee-python | redirect | 301 → 200 | 1 page | |
| https://sdk.apify.com/docs/guides/getting-started | redirect | 301 → 302 → 200 | 13 pages | |
| https://stackoverflow.com/questions/tagged/crawlee | blocked | 403 | https://crawlee.dev/ | |

#### Example inputs

Crawl a site and check every link (internal and external):

```json
{
  "startUrls": ["https://crawlee.dev"],
  "mode": "crawl",
  "maxPagesToCrawl": 100,
  "maxUrlsToCheck": 2000
}
```

Check a pasted list of URLs (bulk status and redirect check):

```json
{
  "startUrls": ["https://example.com/", "http://github.com/apify", "https://httpbin.org/status/404"],
  "mode": "list"
}
```

Check every URL in a sitemap:

```json
{
  "startUrls": ["https://www.sitemaps.org/"],
  "mode": "sitemap",
  "maxUrlsToCheck": 5000
}
```

Internal links only, broken images too, problems only in the output:

```json
{
  "startUrls": ["https://crawlee.dev"],
  "checkExternalLinks": false,
  "includeAssets": true,
  "outputMode": "problemsOnly",
  "maxPagesToCrawl": 200
}
```

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | array of strings | (required, prefilled) | Crawl: start pages. List: URLs to check. Sitemap: websites or sitemap URLs. Private and internal addresses are refused (free). |
| `mode` | `crawl`, `list`, `sitemap` | `crawl` | How URLs are found. |
| `maxPagesToCrawl` | integer | `50` | Internal HTML pages opened to collect links (crawl mode). |
| `maxUrlsToCheck` | integer | `1000` | Spend cap: unique URLs checked in the run, crawled pages included. |
| `checkExternalLinks` | boolean | `true` | Check links to other sites (HEAD first, never crawled). |
| `includeAssets` | boolean | `false` | Also check images, scripts, stylesheets and icons. |
| `outputMode` | `all`, `problemsOnly` | `all` | `problemsOnly` stores only non-ok rows (healthy URLs are still checked and charged). |
| `includePatterns` / `excludePatterns` | arrays | (none) | `*` globs or `/regex/` to limit which links are checked or crawled. |
| `respectRobotsTxt` | boolean | `true` | Disallowed URLs are not fetched and are reported as `skipped` (free). |
| `maxConcurrency` | integer 1-30 | `10` | Requests in flight in total. |
| `maxConcurrencyPerHost` | integer 1-5 | `2` | Requests in flight per host, to stay polite to small sites. |
| `requestTimeoutSecs` | integer | `15` | Seconds before a URL is marked unreachable. |

### Output example

```json
{
  "url": "https://jobops.app/",
  "isBroken": true,
  "verdict": "broken",
  "statusCode": 402,
  "firstStatusCode": 402,
  "finalUrl": "https://jobops.app/",
  "redirectCount": 0,
  "redirectChain": [],
  "linkType": "external",
  "linkTag": "a",
  "foundOn": ["https://crawlee.dev/blog"],
  "foundOnCount": 1,
  "anchorText": "JobOps",
  "contentType": "text/html",
  "responseTimeMs": 352,
  "checkMethod": "GET",
  "status": "ok",
  "errorCode": "HTTP_402",
  "errorMessage": "HTTP 402",
  "checkedAt": "2026-11-12T13:59:08.251Z"
}
```

The key-value store also gets:

- `RUN_SUMMARY`: totals (`urlsChecked`, `broken`, `redirects`, `blocked`, `byStatusCode`) and `topPagesWithBrokenLinks`
- `BROKEN_LINKS`: CSV with `broken_url, verdict, status_code, error_code, found_on, anchor_text, link_type`

#### Verdicts (for AI agents and integrations)

- `broken`: 4xx or 5xx (except the "blocked" codes below), a domain that does not resolve, a refused connection, a TLS error or a redirect loop
- `unreachable`: timeout or network error. `isBroken` is true for both `broken` and `unreachable`
- `blocked`: 401, 403, 429 or 999. The server refused an automated check; open it in a browser to confirm. Not counted as broken
- `redirect`: works, but only after one or more redirects (worth updating internal links)
- `ok`: answered 2xx directly
- `skipped`: robots.txt disallowed, private address or unsafe redirect. Not fetched, free
- `status` is `ok` whenever an HTTP status came back (that is the billable event, even for a 404). `status: "error"` rows are free.

### Use it from the API, MCP or an AI agent

- **Actor ID:** `hillcountrybots/broken-link-checker`

```bash
curl -X POST "https://api.apify.com/v2/acts/hillcountrybots~broken-link-checker/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"startUrls":["https://crawlee.dev"],"maxPagesToCrawl":20,"outputMode":"problemsOnly"}'
```

- **MCP (Claude, Cursor, ChatGPT and other MCP clients):** add `https://mcp.apify.com?tools=hillcountrybots/broken-link-checker`.

### How much does it cost?

**$0.80 per 1,000 URLs checked** ($0.0008 per `url-checked` event, one per unique URL that returned an HTTP status). No start fee. Each URL is charged once per run, however many pages link to it. You pay nothing for:

- invalid or private start URLs
- dead domains, timeouts and network errors (no HTTP answer)
- URLs skipped by robots.txt, filters or the cap
- the run summary and the broken links CSV

Example: a 100-page site with 1,500 unique links costs about $1.20. Cap spend with `maxUrlsToCheck` or the run's max charge setting.

### Limits

- Plain HTTP only. Links added by JavaScript after page load are not seen, and pages behind a login are never crawled.
- Some sites (Stack Overflow, LinkedIn, X) block automated checks. Those come back as `blocked` or `skipped`, not broken.
- Crawl mode follows internal links without query strings and skips login, account, cart and admin paths.
- `foundOn` lists up to 20 pages per URL; `foundOnCount` has the full number.

### FAQ

**Does it check external links?** Yes, by default. External URLs are checked with HEAD (GET if HEAD is refused) and never crawled.

**Can I check images and scripts?** Turn on `includeAssets`.

**Will it hammer my site?** No. Two requests per host at a time by default, robots.txt respected, no browser.

**Does it collect personal data?** No. It records URLs, status codes and link text only.

**How do I fix redirects?** Use the `Redirect chains` view, then update the link on each `foundOn` page to point at `finalUrl`.

### More tools from Hill Country Bots

See everything we make at [hillcountrybots.com](https://hillcountrybots.com).

- [SEO Audit Tool](https://apify.com/hillcountrybots/seo-audit): 0-100 SEO score per page, top 5 fixes and a client-ready HTML report. $2 per 1,000 pages, failures free.
- [Sitemap URL Extractor](https://apify.com/hillcountrybots/sitemap-url-extractor): every URL from a site's XML sitemaps. $0.50 per 1,000 URLs.
- [Bulk PageSpeed Insights Test](https://apify.com/hillcountrybots/bulk-pagespeed-checker): Lighthouse scores and Core Web Vitals for many URLs. $3 per 1,000 audits, failures free.
- [Tech Stack Detector](https://apify.com/hillcountrybots/tech-stack-detector): CMS, analytics, CDN and frameworks for many sites. $0.50 per 1,000 sites.
- [Bulk Domain Availability Checker](https://apify.com/hillcountrybots/domain-availability-checker): available or taken, from official registry RDAP. $1 per 1,000 domains.

### Support

Spotted a false "broken"? Open an issue on the **Issues** tab with the run ID and the URL. We reply within 2 business days (US Central time). Built and maintained by **Hill Country Bots**.

# Actor input Schema

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

Required. One per line. Crawl mode: the homepage or page to start crawling from (a bare domain like example.com works). List mode: every URL you want checked. Sitemap mode: a website (its sitemap is found via robots.txt or /sitemap.xml) or a direct sitemap URL. Private, local and internal addresses are refused for free.

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

crawl = follow internal links from the start pages and check every link found (internal, external and optionally assets). list = check only the URLs you pasted. sitemap = check every URL listed in the site's XML sitemap (find dead pages your sitemap still advertises).

## `maxPagesToCrawl` (type: `integer`):

How many internal HTML pages are opened to collect links. Each crawled page is also checked. 10 for a quick look, 100 to 500 for a typical small business site. Ignored in list and sitemap modes.

## `maxUrlsToCheck` (type: `integer`):

Total unique URLs checked in this run, crawled pages included. This caps spend: only URLs that return an HTTP status are charged. The run's own max-charge setting is also respected. Links found after the cap is reached are counted in RUN_SUMMARY as skippedOverCap.

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

Also check links that point to other websites (crawl mode). External sites are checked once per URL with HEAD and never crawled.

## `includeAssets` (type: `boolean`):

Also check img src, script src and stylesheet/icon links found on crawled pages. Finds missing images and dead CDN files. Adds more URLs per page.

## `outputMode` (type: `string`):

all = one row per checked URL. problemsOnly = store only broken, unreachable, blocked, redirect and skipped rows (the healthy URLs are still checked and counted in RUN_SUMMARY and charged).

## `includePatterns` (type: `array`):

Optional. Globs with \* (e.g. https://example.com/blog/\*) or /regex/. Links that do not match are not checked or crawled.

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

Optional. Globs or /regex/, e.g. */tag/* or https://twitter.com/\*. Matching links are not checked or crawled.

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

On (recommended): URLs disallowed by a site's robots.txt are not fetched and are reported as skipped (free).

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

Requests in flight across all hosts (1-30).

## `maxConcurrencyPerHost` (type: `integer`):

Requests in flight to any single host (1-5). Keep this low to be polite to small sites.

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

Seconds to wait for a response before a URL is marked unreachable (timeouts are retried once for crawled pages).

## Actor input object example

```json
{
  "startUrls": [
    "https://crawlee.dev"
  ],
  "mode": "crawl",
  "maxPagesToCrawl": 10,
  "maxUrlsToCheck": 100,
  "checkExternalLinks": true,
  "includeAssets": false,
  "outputMode": "all",
  "includePatterns": [],
  "excludePatterns": [],
  "respectRobotsTxt": true,
  "maxConcurrency": 10,
  "maxConcurrencyPerHost": 2,
  "requestTimeoutSecs": 15
}
```

# Actor output Schema

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

Dataset of one row per unique checked URL, broken first: status code, redirect chain, verdict and the pages it was found on.

## `brokenLinksCsv` (type: `string`):

CSV with one line per broken link and the page it was found on, ready for a client fix list. Key: BROKEN_LINKS.

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

JSON run summary (URLs checked, broken, redirects, status-code counts, pages with the most broken links). Key: RUN_SUMMARY.

# 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"
    ],
    "maxPagesToCrawl": 10,
    "maxUrlsToCheck": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("hillcountrybots/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"],
    "maxPagesToCrawl": 10,
    "maxUrlsToCheck": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("hillcountrybots/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"
  ],
  "maxPagesToCrawl": 10,
  "maxUrlsToCheck": 100
}' |
apify call hillcountrybots/broken-link-checker --silent --output-dataset

```

## MCP server setup

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