# Broken Link Checker & 404 Finder for Websites (`opendatalabs/broken-link-site-qa-checker`) Actor

Crawls a site's static HTML, checks every link, image, script and stylesheet it finds, and reports broken links, redirect chains, redirect loops and mixed content, with the referring pages needed to fix each one.

- **URL**: https://apify.com/opendatalabs/broken-link-site-qa-checker.md
- **Developed by:** [Shyheim Ulrickson](https://apify.com/opendatalabs) (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.68 / 1,000 checked links

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

**Use it to** find every broken link, image, script and stylesheet on your site, with the exact pages to edit, and get alerted when something new breaks.

**Cost:** $0.80 per 1,000 links checked plus $1 per 1,000 pages crawled. That is about $0.85-0.92 per 1,000 links on the sites we tested.

**Why this one**

- **Monitor mode.** On a schedule it reports only links that are newly broken, changed status, or recovered, instead of the same full list every day.
- **No false alarms from bot walls.** A Cloudflare challenge, captcha or 429 is reported as `blocked`, never as `broken`.
- **Actionable rows.** Every finding lists its referring pages and anchor text, and each unique link is checked once even if it appears on 500 pages.

**Sample output** (real run on `info.cern.ch`, Oct 19, 2026):

| url | status | statusCode | anchorText | sourcePageCount |
|---|---|---|---|---|
| http://home.web.cern.ch/topics/birth-web | broken | 404 | Learn about the birth of the web | 1 |
| http://info.cern.ch/hypertext/WWW/TheProject.html | mixed_content | 200 | Browse the first website | 11 |
| http://line-mode.cern.ch/www/hypertext/WWW/TheProject.html | mixed_content | 200 | Browse the first website using the line-mode browser simulator | 1 |

[![Broken link checker output on info.cern.ch: broken links, mixed content and the pages they are on](https://api.apify.com/v2/key-value-stores/CHfh24KK6KF51EHqU/records/broken-link-checker-mixed-content-info-cern-ch.png)](https://apify.com/opendatalabs/broken-link-site-qa-checker/examples/find-mixed-content-http-links)

*Real output from the [Find mixed content and insecure HTTP links](https://apify.com/opendatalabs/broken-link-site-qa-checker/examples/find-mixed-content-http-links) example task on info.cern.ch (Oct 21, 2026).*

Crawl a site, check every link on it, and get back a list of what is broken and exactly which pages to edit to fix it.

Not just `<a href>`: images, scripts and stylesheets are checked too, because a 404 on a logo or a stylesheet is a visible defect that most link checkers miss entirely. Each result carries the referring pages and the anchor text used, so a finding is actionable without hunting for where the link lives.

### What does this broken link checker do?

- **Crawls static HTML.** Breadth-first from your start URLs, or seeded from your sitemap. No JavaScript rendering, which keeps runs fast and cheap.
- **Checks every unique link once.** A footer link on 500 pages is one request, not 500. The row still records all 500 referring pages.
- **Follows redirects by hand, up to 10 hops,** and reports the whole chain — so you can see that `/old` reaches `/new` through four hops, or that two URLs point at each other forever.
- **Tells "broken" apart from "we were not allowed to look".** A Cloudflare challenge or a captcha is reported as `blocked`, never as `broken`. This Actor does not attempt to get past bot protection.
- **Finds mixed content.** An `http://` image or script on an `https://` page is a real defect browsers block, even though the URL itself answers 200.
- **Monitor mode.** On a schedule, report only links that are newly broken or that changed status, including ones that recovered.

### How to check a website for broken links

1. Enter your homepage in **Start URLs**, or your sitemap (or just your domain) in **Sitemap URL or domain** to cover every listed page.
2. Set **Max pages to crawl** and **Max links to check** to cap the cost of the run.
3. Leave **Check external links** on to test outbound links too, or turn it off for a fast internal-only QA pass.
4. Click **Start**. Broken links, redirect chains and mixed content appear in the **Problems** view, each with the pages that link to it.
5. For ongoing monitoring, turn on **Only report new or changed problems**, save the input as a task and add a nightly schedule.

### Example tasks: pre-launch QA and 404 monitoring

Ready-made configurations you can copy and run in one click:

- [Pre-launch broken link check](https://apify.com/opendatalabs/broken-link-site-qa-checker/examples/pre-launch-broken-link-check): internal links, images and scripts only, for a fast QA pass before release.
- [Nightly 404 monitor for your website](https://apify.com/opendatalabs/broken-link-site-qa-checker/examples/nightly-404-monitor): reports only links that newly broke or recovered since the last run.
- [Find mixed content and insecure HTTP links](https://apify.com/opendatalabs/broken-link-site-qa-checker/examples/find-mixed-content-http-links): `http://` assets and links on `https://` pages after an HTTPS migration.

### How are broken links classified?

| Status | Meaning |
|---|---|
| `ok` | 2xx or 3xx, at most one redirect, nothing else wrong. |
| `broken` | 4xx, 5xx, DNS failure, or a connection that could not be made. |
| `redirect_chain` | Reached a final URL, but took more hops than your threshold. |
| `redirect_loop` | Came back to a URL it had already visited, or ran past the hop limit. |
| `mixed_content` | An `http://` link or asset referenced from an `https://` page. |
| `blocked` | A bot challenge, a captcha, a 429, or a path `robots.txt` disallows. The link is **not** known to be broken. |
| `timeout` | No response inside the timeout. |

A single redirect is treated as normal by default, because `http` to `https` and adding a trailing slash are everyday behaviour, not defects. Raise **Redirects before a link counts as a chain** if your site legitimately redirects twice.

### Why does this link checker avoid false alarms?

Most of the work in this Actor goes into not crying wolf:

| Problem with naive link checkers | What this one does |
|---|---|
| Reports every Cloudflare 403 as a dead link | Challenges are fingerprinted from headers, challenge cookies and body markers, and reported as `blocked`. A plain 404 served *through* Cloudflare is still correctly reported as broken. |
| Reports links as dead because the server refuses `HEAD` | A 405, 403 or 404 answer to `HEAD` triggers one `GET` retry before the link is called broken. |
| Counts `/page#intro` and `/page#setup` as two links | Fragments are dropped, and tracking parameters such as `utm_*`, `gclid` and `fbclid` are stripped, so one page is one link. |
| Hammers one small origin server with 20 parallel requests | Per-host concurrency defaults to **2**, independent of overall run concurrency. |
| A bad night at your origin wipes the monitoring baseline | Monitor mode refuses to overwrite the baseline when a run checked nothing, when too many pages failed, or when most links were blocked. |
| Treats a rate-limited 429 as a dead link | `429` is `blocked`: it tells you nothing about whether the link works. |

### Which pages count as "your site"?

By default, only the **exact hostnames you started from**. Starting at `info.cern.ch` does not adopt `line-mode.cern.ch` or `home.web.cern.ch` — those are separate properties run by other people, and they come back as `external`.

Matching on the registrable domain instead would be wrong more often than it is right. On shared hosts it is actively dangerous: `alice.github.io` and `bob.github.io` share `github.io` but belong to different people, and the same goes for `myshopify.com`, `substack.com` and every other platform that gives customers a subdomain. A domain-wide default would quietly crawl strangers' sites.

When your site genuinely spans `www`, `blog` and `shop`, set **Treat subdomains as the same site** (`includeSubdomains: true`).

One case is handled for you either way: **if a start URL redirects to another host, that host is adopted automatically.** A site that sends `example.com` to `www.example.com` would otherwise have every one of its own pages classified as external. Only the URLs you named get this treatment — a redirect found deeper in the crawl, say to a CDN, does not widen the scope.

Every seed counts as your site, including URLs that came from your sitemap. The hosts in play are listed in `RUN_SUMMARY` under `scope.inScopeHosts`.

Scope affects two things: which pages are crawled (when `sameDomainOnly` is on) and whether a link is checked at all (when `checkExternal` is off). It never changes whether a link is reported as broken.

### Broken link checker input examples

Minimum viable input:

```json
{
  "startUrls": ["https://example.com"]
}
```

Whole-site check seeded from the sitemap:

```json
{
  "sitemapUrl": "https://example.com",
  "maxPages": 2000,
  "maxLinksToCheck": 20000,
  "checkExternal": true,
  "concurrencyPerHost": 2
}
```

A nightly monitor that only reports what changed:

```json
{
  "startUrls": ["https://example.com"],
  "maxPages": 500,
  "onlyNewProblems": true,
  "stateStoreName": "example-link-monitor",
  "stateKey": "marketing-site"
}
```

Internal links only, for a fast pre-release gate:

```json
{
  "startUrls": ["https://staging.example.com"],
  "checkExternal": false,
  "maxDepth": 5,
  "respectRobots": false
}
```

A site spread across several subdomains:

```json
{
  "startUrls": ["https://example.com"],
  "includeSubdomains": true,
  "maxPages": 1000
}
```

> Turn `respectRobots` off only for a site you own and are authorised to crawl in full. It never affects bot protection: challenges and captchas are always reported as `blocked`, never bypassed.

#### Key settings

| Field | Default | Notes |
|---|---|---|
| `startUrls` | — | Pages to crawl from. Give these, `sitemapUrl`, or both. With neither, the run is the 5-page demo crawl described under `includeOkLinks` instead of failing. |
| `sitemapUrl` | — | Sitemap URL, or a bare domain to have the sitemap discovered through `robots.txt`. Indexes, gzip, text and RSS all work. |
| `maxPages` | `500` | Pages fetched and parsed. Breadth-first, so truncation still spans the site. |
| `maxDepth` | `3` | Link depth from the start URLs. `0` checks only the start pages. |
| `sameDomainOnly` | `true` | Keeps the crawl on your own hosts. |
| `includeSubdomains` | `false` | Exact hostnames only by default. See below. |
| `checkExternal` | `true` | Off-site links are checked but never crawled into. |
| `respectRobots` | `true` | Leave on unless you own the site. Applies to pages and to every link check, internal and external: a link (or a redirect hop) that a host's robots.txt disallows for this Actor is never requested and is reported as `blocked`. |
| `concurrencyPerHost` | `2` | In-flight requests against any one host. |
| `requestsPerSecond` | `8` | Global ceiling for the whole run. |
| `onlyNewProblems` | `false` | Monitor mode. |
| `includeOkLinks` | `false` | Turn on for a full inventory instead of just problems. The Console form prefills it on, with a demo crawl of `quotes.toscrape.com` (a long-running public practice site for crawlers with no robots.txt restrictions) capped at 5 pages and 50 links, about $0.05, so the first run shows what rows look like. An empty input runs the same demo. Switch it off to see only problems. |

### What data do you get for each broken link?

One row per unique link. A real result from a live run against `info.cern.ch`:

```json
{
  "url": "http://info.cern.ch/hypertext/WWW/TheProject.html",
  "status": "mixed_content",
  "statusCode": 200,
  "sourcePages": [
    {
      "pageUrl": "https://info.cern.ch/",
      "anchorText": "Browse the first website",
      "linkType": "anchor"
    }
  ],
  "sourcePageCount": 1,
  "anchorText": "Browse the first website",
  "linkType": "anchor",
  "scope": "internal",
  "host": "info.cern.ch",
  "method": "HEAD",
  "errorMessage": "Insecure http:// resource referenced from an https:// page",
  "firstSeen": "2026-10-19T02:42:02.695Z",
  "checkedAt": "2026-10-19T02:42:02.695Z",
  "durationMs": 360
}
```

A broken link with its redirect chain:

```json
{
  "url": "http://home.web.cern.ch/topics/birth-web",
  "status": "broken",
  "statusCode": 404,
  "finalUrl": "https://home.web.cern.ch/topics/birth-web",
  "redirectChain": [
    {
      "url": "http://home.web.cern.ch/topics/birth-web",
      "statusCode": 301,
      "location": "https://home.web.cern.ch/topics/birth-web"
    }
  ],
  "redirectCount": 1,
  "sourcePageCount": 1,
  "linkType": "anchor",
  "scope": "external",
  "host": "home.web.cern.ch",
  "errorMessage": "HTTP 404"
}
```

By default the dataset holds only links that need attention. Set **Include working links** for a full inventory.

The dataset ships five views: **Problems**, **Broken links**, **Redirects**, **Blocked**, and **Changes since last run**.

#### Run summary

`RUN_SUMMARY` in the key-value store holds the whole-run picture, including links that were checked but not written:

```json
{
  "pagesCrawled": 1,
  "pagesFailed": 0,
  "linksChecked": 4,
  "countsByStatus": {
    "ok": 0, "broken": 1, "redirect_chain": 0, "redirect_loop": 0,
    "mixed_content": 3, "blocked": 0, "timeout": 0
  },
  "worstBrokenLinks": [
    {
      "url": "http://home.web.cern.ch/topics/birth-web",
      "status": "broken",
      "statusCode": 404,
      "sourcePageCount": 1,
      "exampleSourcePages": ["https://info.cern.ch/"]
    }
  ],
  "errorsByHost": [
    { "host": "home.web.cern.ch", "broken": 1, "blocked": 0, "timeout": 0, "total": 2 }
  ],
  "monitorMode": false,
  "truncated": false
}
```

`worstBrokenLinks` is ranked by how many pages reference each link. A dead link in the site footer and a dead link in one old blog post are both one fix, but the footer one is worth doing first.

`errorsByHost` exists so that one unreachable third party reads as a single cause rather than as fifty unrelated broken links.

### Use cases for a website link checker

- **Pre-release QA gate.** Crawl staging with `checkExternal: false` and fail the release if anything internal is broken.
- **Scheduled site monitoring.** Run nightly with `onlyNewProblems` and get told only what changed.
- **Content audits after a migration.** Find every link still pointing at the old URL structure, and every chain the redirect map introduced.
- **HTTPS migration cleanup.** `mixed_content` rows are exactly the assets still loading over `http`.
- **SEO housekeeping.** Redirect chains waste crawl budget and leak link equity; this finds them with the full hop list.
- **Site health suite.** Pair it with the [Sitemap URL Extractor with Change Tracking](https://apify.com/opendatalabs/sitemap-change-tracker) to find new pages, then check only those for broken links.

### Limitations and known issues

- **No JavaScript rendering.** Links injected at runtime by a framework are invisible to this Actor. For a client-rendered single-page app, point it at your sitemap instead, or use a rendering crawler.
- **External links produce most of the noise.** Large sites frequently challenge or rate-limit automated requests. Those come back as `blocked`, which is honest but not actionable — check the `blockedBy` column before chasing anything.
- **`blocked` does not mean working.** It means unknown. If a link matters, open it in a browser.
- **`HEAD` is not universally supported.** The Actor falls back to `GET`, but a server that answers both with 403 will be reported as broken even if a browser can load it.
- **Subdomains are not your site unless you say so.** If your content lives on `www` and you started at the apex without a redirect between them, turn on `includeSubdomains` or the crawl will stop at the first page.
- **Monitor mode's first run reports everything.** There is no baseline yet; the comparison starts on run two.
- **Renaming `stateStoreName` or `stateKey` starts a fresh baseline.**
- **Public pages only.** Nothing behind a login is crawled, no credentials are sent, and no personal data is collected — links and anchor text only. `mailto:` and `tel:` links are skipped rather than recorded.
- **`robots.txt` is honoured by default**, with longest-match Allow/Disallow resolution. An unreachable `robots.txt` means no restrictions, as the standard prescribes.

### How much does it cost to check a website for broken links?

| Event | Price | Charged when |
|---|---|---|
| Actor start | $0.00005 | Once per run (per GB of memory). |
| `page-crawled` | $0.001 ($1 per 1,000) | One page was fetched and parsed for links successfully. |
| `link-checked` | $0.0008 ($0.80 per 1,000) | One unique link was checked. |

Apify subscribers on the Bronze, Silver and Gold plans pay 5%, 10% and 15% less per event; the Pricing tab shows the price for your plan.

A page that failed to load is not charged. `link-checked` bills once per unique URL no matter how many pages reference it, so a dead footer link on 500 pages is one charge, not 500.

When the run reaches your maximum total charge it stops cleanly, recording the reason in `RUN_SUMMARY` rather than failing. `RUN_SUMMARY.charging` shows the units of work performed next to the units actually billed.

### FAQ

#### How do I get alerted only when something new breaks?

Turn on **monitor mode** (`onlyNewProblems`) and schedule the Actor (Console > Schedules). The first run sets a baseline; later runs report only links that newly broke, changed status or recovered.

#### Why is a link marked `blocked` instead of `broken`?

The server answered with a bot challenge, a captcha, a 429 or a `robots.txt` rule, so the link's real status is unknown. Open it in a browser if it matters. The Actor never tries to get past bot protection.

#### Does it check images, scripts and stylesheets?

Yes. Assets are checked alongside `<a href>` links, and `http://` assets on `https://` pages are flagged as `mixed_content`.

#### Does it render JavaScript?

No. It reads static HTML, which keeps runs fast and cheap. For a client-rendered site, seed the crawl from your sitemap with `sitemapUrl`.

#### Can I export the results or send them to Slack?

Yes. Download the dataset as CSV, Excel or JSON, or use Apify's integrations (Slack, Google Sheets, Zapier, Make, n8n, webhooks) to send each scheduled run's problems where your team works.

### Related Actors from opendatalabs

- [Sitemap URL Extractor with Change Tracking](https://apify.com/opendatalabs/sitemap-change-tracker): list every URL in a site's sitemaps and see which pages were added, changed or removed since the last run.
- [Grants.gov Grant Opportunity Monitor](https://apify.com/opendatalabs/grants-gov-monitor): a daily feed of new US federal grants and deadline changes.

### Feedback and support

This Actor is new and actively maintained. If a run fails, a field is missing, or a result looks wrong, please open an issue on the **Issues** tab with the run ID. We read and reply to every issue, and suggestions for new filters or fields are very welcome.

# Actor input Schema

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

Pages to start crawling from. Usually just your homepage. Give a bare domain and it is upgraded to https://. Provide these, a sitemap URL, or both. Leave this and the sitemap empty to run a small demo crawl (5 pages, 50 links).

## `sitemapUrl` (type: `string`):

Seeds the crawl from a sitemap instead of (or as well as) discovering pages by following links. Give a sitemap URL directly, or a bare domain to have the sitemap found through robots.txt, falling back to common paths. Sitemap indexes, gzipped sitemaps, plain-text URL lists and RSS feeds are all handled. This is the fastest way to cover a whole site without deep crawling.

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

Hard ceiling on pages fetched and parsed for links. The crawl is breadth-first, so stopping at this limit still covers the site broadly instead of tunnelling into one section.

## `maxDepth` (type: `integer`):

How many links deep to follow from the start URLs. 0 checks only the links on the start pages themselves. Pages seeded from a sitemap all start at depth 0.

## `sameDomainOnly` (type: `boolean`):

Keeps the crawl on the hosts that count as your site. Off-site links are still checked when 'Check external links' is on, they are just not crawled into. Which hosts count as yours is controlled by 'Treat subdomains as the same site' below.

## `includeSubdomains` (type: `boolean`):

Off by default, so only the exact hostnames you started from count as your site. Turn it on when your site genuinely spans several subdomains, such as www, blog and shop. Leaving it off matters on shared hosts like github.io, myshopify.com or substack.com, where a domain-wide match would pull in unrelated people's sites. Either way, if a start URL redirects to another host, for example example.com to www.example.com, that host is added automatically.

## `checkExternal` (type: `boolean`):

Also check links that point off your site. These are where most rot happens, but they are also where most false alarms come from, because large sites often challenge or rate-limit automated requests. Those come back as 'blocked', never as 'broken'.

## `respectRobots` (type: `boolean`):

Leave this on. Pages disallowed by robots.txt are not crawled. Turn it off only for a site you own and are authorised to crawl in full. It is never a way to get around bot protection: challenges and captchas are always reported as 'blocked' rather than bypassed.

## `concurrencyPerHost` (type: `integer`):

How many requests may be in flight against any single host at once. Low on purpose: a crawl that hammers one small origin server gets rate-limited and returns misleading results. The overall run still progresses in parallel across different hosts.

## `requestsPerSecond` (type: `integer`):

Global rate ceiling across the whole run, enforced by a token bucket.

## `maxLinksToCheck` (type: `integer`):

Safety cap on unique links checked. Each unique URL is checked once no matter how many pages reference it, so this number is usually much smaller than you expect.

## `maxRedirects` (type: `integer`):

Redirects are followed one hop at a time and the whole chain is recorded. Exceeding this limit is reported as a redirect loop.

## `redirectChainThreshold` (type: `integer`):

A single redirect is normal (http to https, or adding a trailing slash). Raise this if your site legitimately redirects twice and you only want to hear about longer chains.

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

How long to wait for a single page fetch or link check before giving up on that attempt. A link that exceeds this is reported as 'timeout' rather than 'broken', because a slow server is not the same as a dead one. Raise it if you are checking links to known-slow hosts.

## `maxRetries` (type: `integer`):

Total attempts, including the first. Only transient failures are retried; a 404 is answered immediately rather than retried three times.

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

Monitor mode. Compares this run against the previous run's baseline and emits only links that are newly broken or whose status changed, including links that recovered. A link that has been broken for a month is real but is not news. The first run has no baseline, so it reports every problem and then stores the baseline.

## `stateStoreName` (type: `string`):

Named key-value store holding the previous run's link statuses. A named store survives between runs, unlike the default store, which is what makes monitor mode work. Use one store per monitoring job.

## `stateKey` (type: `string`):

Key inside that store. Leave empty to derive one from the first start URL, so several sites can share one store safely. Set it explicitly if you change start URLs but want to keep the same history.

## `maxPageFailurePercentForBaselineUpdate` (type: `integer`):

Safety valve for monitor mode. If more than this share of pages fail to load, the baseline is left untouched, so one bad day at your origin server does not make the next run report the whole site as newly broken. A run that checks zero links never overwrites the baseline either.

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

Off by default, so the dataset contains only links that need attention. Turn it on to get a full inventory of every link on the site with its status.

## `seedFromSitemap` (type: `boolean`):

When no sitemap URL is given, still try to find one through the start URL's robots.txt and common paths. Defaults to on only when you supplied a sitemap URL.

## `userAgentContact` (type: `string`):

Appended to the User-Agent header, for example a URL or an address, so site owners can see who is checking their links. Optional; the Actor sends a descriptive User-Agent either way.

## Actor input object example

```json
{
  "startUrls": [
    "https://quotes.toscrape.com/"
  ],
  "sitemapUrl": "https://example.com/sitemap.xml",
  "maxPages": 5,
  "maxDepth": 3,
  "sameDomainOnly": true,
  "includeSubdomains": false,
  "checkExternal": true,
  "respectRobots": true,
  "concurrencyPerHost": 2,
  "requestsPerSecond": 8,
  "maxLinksToCheck": 50,
  "maxRedirects": 10,
  "redirectChainThreshold": 1,
  "requestTimeoutSecs": 30,
  "maxRetries": 2,
  "onlyNewProblems": false,
  "stateStoreName": "broken-link-checker-state",
  "maxPageFailurePercentForBaselineUpdate": 50,
  "includeOkLinks": true,
  "seedFromSitemap": false,
  "userAgentContact": "+https://your-company.example/bot"
}
```

# Actor output Schema

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

Checked links with status, redirect chain and the page they were found on (OK links only when includeOkLinks is on).

## `runSummary` (type: `string`):

Run summary: pages crawled, links checked, counts by status, worst broken links and errors by host.

# 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://quotes.toscrape.com/"
    ],
    "maxPages": 5,
    "maxLinksToCheck": 50,
    "includeOkLinks": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("opendatalabs/broken-link-site-qa-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://quotes.toscrape.com/"],
    "maxPages": 5,
    "maxLinksToCheck": 50,
    "includeOkLinks": True,
}

# Run the Actor and wait for it to finish
run = client.actor("opendatalabs/broken-link-site-qa-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://quotes.toscrape.com/"
  ],
  "maxPages": 5,
  "maxLinksToCheck": 50,
  "includeOkLinks": true
}' |
apify call opendatalabs/broken-link-site-qa-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,opendatalabs/broken-link-site-qa-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/tNiRb8B0kvTuzaV3z/builds/o3awMSVGb8CIb02dc/openapi.json
