# Broken Link Checker from Sitemap (`bjgomes/broken-link-checker-from-sitemap`) Actor

Check every page in your XML sitemap for broken links. It reads the sitemap, visits each page and checks the links on it. You get 404s, server errors, redirect chains and slow pages, each with the page it was found on. If a site blocks the run, a row says why. Export to CSV or JSON.

- **URL**: https://apify.com/bjgomes/broken-link-checker-from-sitemap.md
- **Developed by:** [Brian Gomes](https://apify.com/bjgomes) (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.70 / 1,000 link 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

Broken Link Checker from Sitemap checks every page in a website's XML sitemap, and the links on those pages, and lists each broken one with the page it's on.

### What it does

Give it a website or a sitemap URL. It reads the sitemap, checks the status of every page in it, then opens each page and checks the links it finds there. You get one row per problem: a 404, a server error, a redirect or a slow response, with the page the link was found on.

It starts from the sitemap, not from a crawl. So it checks the pages the site says it has, including ones no menu links to anymore.

When something goes wrong, the run doesn't fail silently. A site with no sitemap, a 403, a 429 or a timeout gives you a row that says what happened.

Here's what five rows look like:

| result | linkUrl | statusCode | sourcePage |
|---|---|---|---|
| broken | https://example.com/old-pricing | 404 | https://example.com/blog/launch |
| broken | https://partner.example.org/guide | 500 | https://example.com/docs/start |
| broken | https://example.com/careers | 404 | https://example.com/sitemap.xml |
| redirected | http://example.com/about | 200 | https://example.com/ |
| ok (slow) | https://example.com/files/spec.pdf | 200 | https://example.com/docs/api |

(Example data. A page from the sitemap that is itself broken shows the sitemap as `sourcePage`. For a redirected link, `statusCode` is where the redirects end and `redirectCount` says how many there were.)

### Who it's for

- **SEO audits.** Find every broken internal link before a search engine does.
- **After a site migration.** Check that every page in the new sitemap loads and nothing still points at an old URL.
- **Content and docs teams.** Find links to partner sites and files that have gone dead since the page was written.
- **Scheduled checks.** Run it weekly and catch a broken link the week it breaks.

### Input

| Field | What it does | Default |
|---|---|---|
| Websites or sitemap URLs | One or more homepages (`https://example.com`) or sitemaps (`https://example.com/sitemap.xml`). It works out which one you gave it. | none, required |
| Maximum pages to scan | Stops after this many pages from the sitemap. Use it to test on a big site first. | 100 |
| Maximum links to check | Stops after checking this many unique links. | 5,000 |
| Check external links | Also checks links that go to other websites. Off: only links on your own site (with or without `www.`). | on |
| Include working links | Also lists links that answered 200 quickly with no redirect. Off: only broken, redirected, slow and unchecked links are listed. | off |
| Slow response threshold (ms) | A page or link that takes longer than this to respond is flagged slow, and always listed. | 3,000 |
| Maximum concurrent requests | Requests in flight at once, never more than 4 to one site. | 5 |
| Request timeout (seconds) | The most any one page or link request may take, from connecting to the last byte. | 20 |

Input field names in the API: `startUrls`, `maxPages`, `maxLinks`, `checkExternalLinks`, `includeOkLinks`, `includeNotChecked`, `slowThresholdMs`, `maxConcurrency`, `requestTimeoutSecs`.

The form starts small: 10 pages and up to 100 links, so your first run costs at most $0.11. Raise both when you're ready for the whole site.

Example input:

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

### Output

One row per problem link on each page it's on (and per working link too, with "Include working links" on), plus a row for anything it couldn't read. Export it from the Console as CSV, JSON, Excel, XML or HTML, or read it through the Apify API.

| Field | What it holds |
|---|---|
| `type` | `link` for a page or link that was checked, `error` for something it couldn't read (no sitemap, a sitemap that failed). |
| `result` | `broken` (4xx, 5xx, timeout, unreachable or a malformed URL), `redirected` (ended on a working page after one or more redirects), `ok`, `not-checked` (robots.txt said no, it was over `maxLinks`, or the run stopped first; `error` says which; only listed when `includeNotChecked` is on, otherwise counted by reason in the run's status message), or `error`. |
| `sourcePage` | The page the link was found on. For a page from the sitemap, the sitemap it's listed in. |
| `linkUrl` | The link that was checked. |
| `anchorText` | The link's visible text (or its image's alt text), up to 300 characters. |
| `isInternal` | `true` when the link is on the same site as your input (with or without `www.`). |
| `statusCode` | The HTTP status code it ended on. |
| `finalUrl` | Where the URL ends up after redirects. |
| `redirectCount` | How many redirects it followed. |
| `responseTimeMs` | Time to the response headers, in milliseconds, redirects included. |
| `slow` | `true` when the response took longer than your threshold. |
| `foundIn` | `page` for a link found on a page, `sitemap` for a page listed in the sitemap. |
| `error` | Empty when the row is fine. Otherwise it says what went wrong, like `HTTP 404`, `timed out after 20 s` or `not checked: disallowed by robots.txt`. |
| `startUrl` | The input this row came from. |

Example row (JSON):

```json
{
  "type": "link",
  "result": "broken",
  "sourcePage": "https://example.com/blog/launch",
  "linkUrl": "https://example.com/old-pricing",
  "anchorText": "see our pricing",
  "isInternal": true,
  "statusCode": 404,
  "finalUrl": "https://example.com/old-pricing",
  "redirectCount": 0,
  "responseTimeMs": 212,
  "slow": false,
  "foundIn": "page",
  "error": "HTTP 404",
  "startUrl": "https://example.com/"
}
```

### Pricing

You pay per page scanned and per link checked. There's no monthly fee.

- **Each page from the sitemap that answers:** $0.0005 (50 cents per 1,000 pages).
- **Each link checked:** $0.001 (1 dollar per 1,000 links), on top of the page price. A link that appears on many pages is checked and charged once, and reported on every page it's on. A link to a page it has already opened isn't checked or charged again.
- **Not charged:** anything robots.txt disallows, links it never requested because the run stopped, links and pages that time out or can't be reached (reported as broken, with the reason), and malformed links (reported as broken without a request).

A worked example. A site has 500 pages in its sitemap and 3,000 unique links across them.

- Pages and links: 500 × $0.0005 + 3,000 × $0.001 = $0.25 + $3.00 = **$3.25**.
- Internal links only (Check external links off): the same sum with only the links on your own site counted. If 1,200 of the 3,000 are internal, that's $0.25 + $1.20 = **$1.45**.

Apify also charges its standard start fee for each run. Set Maximum pages, or a maximum cost per run, for a hard ceiling: the run checks nothing it can't charge for, stops cleanly when it reaches your maximum cost, and its status line says so.

### How it handles the hard cases

- **No sitemap.** You get one row with the error "no sitemap found" and nothing else is charged. The run finishes normally.
- **Nested sitemap indexes and gzipped sitemaps.** It reads them the same way as Sitemap URL Extractor & Status Checker.
- **403 and 429.** A blocked request or a rate limit is recorded in the `error` field instead of failing the run. It doesn't retry a 429 in this version.
- **Redirect chains.** It follows up to 10 redirects and records how many it took. A link that redirects is reported even when it ends on a 200, so you can point it at the final URL.
- **A site that stops answering.** After 20 requests in a row get no answer (or 10 minutes spent waiting on them), the run stops and its status line says why, rather than timing out page after page for hours. Those requests aren't charged.
- **Typos in links.** A link with a broken host name (like `http://www..example.com/`) or a bad port is reported as `broken`, with the reason, and the run carries on.
- **Private addresses.** It never connects to a private, internal or loopback address, even when a page links there or a redirect points there. Those links are listed with the reason.
- **Spreadsheet safety.** Anchor text that starts with `=`, `+`, `-` or `@` gets a leading `'` so a CSV opened in Excel or Sheets can't run it as a formula.
- **Timeouts.** A page or link that doesn't answer in time is recorded as an error row. The timeout covers the whole request, from connecting to the last byte, and is 20 seconds unless you change it. The row's `error` says `timed out after 20 s`.
- **robots.txt.** It respects robots.txt on your site and on every site it checks a link on. A disallowed URL is counted in the status message (listed only with `includeNotChecked` on), not fetched, and not charged. If robots.txt itself returns a server error, it requests nothing else on that site and counts those links as not checked. If the site can't be reached at all (no DNS, connection refused, timeout), its links are reported as `broken`, with the reason, and not charged.
- **Load on the site.** At most 4 requests at a time to any one host, and it honours a `Crawl-delay` of up to 10 seconds; a site asking for longer has its URLs counted as not checked rather than holding your run for hours. It identifies itself with an honest user agent, `SitemapBrokenLinkChecker/0.1`.

### Compared with the free tools

You can do this for free. Google Search Console lists 404s that Google found on your site. The W3C Link Checker checks one page's links in a browser. Desktop crawlers like Screaming Frog check a whole site, and the free version stops at 500 URLs. For a small site, one of those is enough.

This Actor fits when the site is bigger, when you want it on a schedule, or when you want the result in your own code through the API. It runs in the cloud, so nothing has to stay open on your computer.

Compared with other broken link checkers in the store, the difference is the starting point. It checks every page the sitemap lists, including orphan pages a crawl would never reach. And a site it can't read gives you a row that says why, not a failed run.

### FAQ

**How do I find broken links on my website?**
Put your homepage or sitemap URL in the input and run it. Every row in the result is a page or link that returned an error, redirected, was slow or couldn't be checked, and `result` says which. Export it as CSV and fix from the top.

**What counts as a broken link?**
Any link that returns a 4xx or 5xx status, times out or can't be reached. Redirects and slow responses are reported too, but they're flagged separately (`result` is `redirected`; `slow` is `true`) so you can tell them apart.

**Why was I charged for links that weren't broken?**
Every link has to be checked to know whether it's broken. With "Include working links" off, the good ones are left out of the dataset, but they were still checked. A link on many pages is charged once.

**Does it check links to other websites?**
Yes, with "Check external links" on. Turn it off to check only links within your own site.

**Why did my run return nothing?**
Check the `error` field. It says "no sitemap found", a status like 403, or a timeout. If your sitemap lives somewhere unusual, put its full URL in the input instead of the homepage.

**Does it respect robots.txt?**
Yes, on your site and on every site it checks a link on. It also limits how fast it requests pages from any one host.

**Can I run it on a schedule?**
Yes. Use Apify Schedules to run it weekly with the same input. Each run makes a new dataset, so you can see what broke since last time.

**Can I call it from my own code?**
Yes. Start it and read the results through the Apify API or the Python and JavaScript clients. It also works with webhooks, Make, Zapier and Google Sheets.

### Limits

- It checks the pages in your sitemap and the links on them. It doesn't follow links further than that, so a page that isn't in the sitemap is only checked if a sitemap page links to it, and its own links aren't followed.
- It reads links from the HTML as served. Links added later by JavaScript aren't seen.
- It checks links in `<a href>` (and image-map `<area href>`) only. Images, scripts and stylesheets aren't checked in this version. It reads the first 5 MB of each page and the first 5,000 links on it; a page past either limit says so in a row.
- It works on public pages. It doesn't log in.
- It reads XML sitemaps, including gzipped ones. Plain-text and RSS sitemaps aren't read in this version.
- It runs in 256 MB by default. A sitemap file over 55 MB uncompressed is refused.

### Integrations

This Actor runs on Apify, so everything the platform does works with it.

- **API.** Start a run and read the results from your own code, in any language. [Apify API](https://docs.apify.com/integrations/api)
- **Schedules.** Run it weekly and catch a broken link the week it breaks. [Schedules](https://docs.apify.com/actors/running/schedules)
- **Tasks.** Save your input once as a task and run it again with one click, or on a schedule. [Tasks](https://docs.apify.com/actors/running/tasks)
- **Webhooks.** Get a call to your own URL when a run succeeds or fails. [Webhooks](https://docs.apify.com/integrations/webhooks)
- **Google Sheets and Drive.** Download the results as CSV or Excel and open them in Sheets. Or add a Google Sheets step after this Actor in Make or Zapier. Or send the files to a Google Drive folder. [Datasets and exports](https://docs.apify.com/storage/dataset), [Google Drive](https://docs.apify.com/integrations/drive)
- **Zapier, Make and n8n.** Start a run from another app and send the results on to the next step. [Zapier](https://docs.apify.com/integrations/zapier), [Make](https://docs.apify.com/integrations/make), [n8n](https://docs.apify.com/integrations/n8n)
- **AI agents (MCP).** Claude, ChatGPT and other MCP clients can find and run this Actor through the Apify MCP server. [Apify MCP server](https://docs.apify.com/integrations/mcp)
- **Other Actors.** Pass this Actor's results to another Actor when a run finishes. [Actor-to-Actor](https://docs.apify.com/integrations/actors)

### Works with

- [Site Audit](https://apify.com/bjgomes/site-audit-seo): an SEO score from 0 to 100 for every page, with a fix for each issue.
- [Sitemap URL Extractor & Status Checker](https://apify.com/bjgomes/sitemap-url-extractor-status-checker): the full URL list of a site, with lastmod and status codes.
- [Bulk PageSpeed Insights for a Sitemap](https://apify.com/bjgomes/bulk-pagespeed-insights-for-a-sitemap): performance score and Core Web Vitals for every URL in a sitemap.

### Support

Found a bug or need a feature? Open an issue on this Actor's **Issues** tab. Include the run ID and the URL you gave it.

# Actor input Schema

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

A website (example.com) to find its sitemaps from robots.txt, /sitemap.xml and /sitemap\_index.xml, or a sitemap URL to read directly. Every page listed is scanned for links.

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

Stop after scanning this many sitemap pages across all inputs.

## `maxLinks` (type: `integer`):

Stop after checking this many unique links. Each unique link is checked once, however many pages it's on.

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

Also check links to other websites. Off: only links on the same site (www. or not) are checked.

## `includeOkLinks` (type: `boolean`):

Also output links that answered 200 quickly with no redirect. Off: only broken, redirected, slow and error rows are output. Every link is checked either way.

## `includeNotChecked` (type: `boolean`):

Also output a row for each link that wasn't requested (robots.txt said no, it was over maxLinks, or the run stopped first). Off: they're counted by reason in the run's status message instead. They're never charged.

## `slowThresholdMs` (type: `integer`):

A page or link whose response takes longer than this is flagged slow and always output.

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

Link checks in flight at once. Never more than 4 at a time to any one host, and robots.txt Crawl-delay is honoured.

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

How long to wait for any single sitemap, robots.txt, page or link request.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://apify.com"
    }
  ],
  "maxPages": 10,
  "maxLinks": 100,
  "checkExternalLinks": true,
  "includeOkLinks": false,
  "includeNotChecked": false,
  "slowThresholdMs": 3000,
  "maxConcurrency": 5,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

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

Every broken, redirected, slow or unchecked link (and working links if asked), with the page it was found on.

# 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://apify.com"
        }
    ],
    "maxPages": 10,
    "maxLinks": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("bjgomes/broken-link-checker-from-sitemap").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://apify.com" }],
    "maxPages": 10,
    "maxLinks": 100,
}

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

```

## MCP server setup

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

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/X4SpwJZfijq5NuAvC/builds/LxC5puTU6sLerD2j3/openapi.json
