# Website Link Health - Broken Links & Recovery

Check links from public HTML pages with referring-page and anchor evidence, redirect chains, confirmed HTTP statuses and saved new/recovered issue tracking. No API key.

Canonical product page: https://mayd-it.com/data-tools/actors/website-link-health-monitor/
Public Actor: https://apify.com/maydit/website-link-health-monitor

## Example input

```json
{
  "startUrls": [
    "https://crawler-test.com/links/broken_links_internal"
  ],
  "checkExternal": false,
  "maxLinks": 20,
  "maxResults": 20
}
```

## Ready-to-use input recipes

These are downloadable JSON configurations. They do not start runs or represent public Apify saved tasks. Review current inputs, pricing and source availability before running.

[Default input](https://mayd-it.com/data-tools/actors/website-link-health-monitor/input.json) · [Input schema](https://mayd-it.com/data-tools/actors/website-link-health-monitor/input-schema.json) · [Saved output capture](https://mayd-it.com/data-tools/actors/website-link-health-monitor/evidence.json)

### Check the public broken-link fixture

Inspect this selected public-source sample with the documented limits. Selected server-rendered pages only; no recursive site crawl, JavaScript rendering, authenticated pages or soft404 inference. Unknown and blocked states are distinct from confirmed broken links. Missing targets are not called recovered. Comparison history advances only after complete determinate checks.

```json
{
  "startUrls": [
    "https://crawler-test.com/links/broken_links_internal"
  ],
  "checkExternal": false,
  "maxLinks": 20,
  "maxResults": 20
}
```

[Download JSON](https://mayd-it.com/data-tools/actors/website-link-health-monitor/examples/recipe-1.json) · [Example evidence](https://mayd-it.com/data-tools/actors/website-link-health-monitor/examples/recipe-1.evidence.json)

Historical successful run recorded 2026-11-05 UTC, with 6 saved rows. This does not establish current availability.

### Resolve relative links exactly

Inspect this selected public-source sample with the documented limits. Selected server-rendered pages only; no recursive site crawl, JavaScript rendering, authenticated pages or soft404 inference. Unknown and blocked states are distinct from confirmed broken links. Missing targets are not called recovered. Comparison history advances only after complete determinate checks.

```json
{
  "startUrls": [
    "https://crawler-test.com/links/relative_link/a/b"
  ],
  "checkExternal": false,
  "maxLinks": 20,
  "maxResults": 20
}
```

[Download JSON](https://mayd-it.com/data-tools/actors/website-link-health-monitor/examples/recipe-2.json) · [Example evidence](https://mayd-it.com/data-tools/actors/website-link-health-monitor/examples/recipe-2.evidence.json)

Historical successful run recorded 2026-11-05 UTC, with 4 saved rows. This does not establish current availability.

### Compare a stable link watchlist

Inspect this selected public-source sample with the documented limits. Selected server-rendered pages only; no recursive site crawl, JavaScript rendering, authenticated pages or soft404 inference. Unknown and blocked states are distinct from confirmed broken links. Missing targets are not called recovered. Comparison history advances only after complete determinate checks.

```json
{
  "startUrls": [
    "https://crawler-test.com/links/broken_links_internal"
  ],
  "checkExternal": false,
  "comparePrevious": true,
  "snapshotName": "weekly-link-check",
  "maxLinks": 20,
  "maxResults": 20
}
```

[Download JSON](https://mayd-it.com/data-tools/actors/website-link-health-monitor/examples/recipe-3.json) · [Example evidence](https://mayd-it.com/data-tools/actors/website-link-health-monitor/examples/recipe-3.evidence.json)

Historical successful run recorded 2026-11-05 UTC, with 6 saved rows. This does not establish current availability.

[All example inputs](https://mayd-it.com/data-tools/actors/website-link-health-monitor/examples/index.json)
## Choose this tool

Use selected public HTML pages to check linked targets and collect referring pages, anchor text, response status and redirect-chain evidence for website maintenance.

Useful output columns: `url`, `linkStatus`, `httpStatus`, `issueChange`, `comparisonStatus`, `foundOn`, `finalUrl`, `redirectChain`, `observedAt`.

## Practical workflow

Start with one page under your control and check internal links first. Separate confirmed HTTP 404 or 410 responses from blocked or unreachable requests. Use the referring page and anchor evidence to locate the issue, repair it, and rerun the same comparison scope to confirm recovery.

## Check the fit

Selected server-rendered pages only; no recursive site crawl, JavaScript rendering, authenticated pages or soft404 inference. Unknown and blocked states are distinct from confirmed broken links. Missing targets are not called recovered. Comparison history advances only after complete determinate checks.

## Real output sample

One selected row from a 2026-11-05 run. Selected original scalar fields from one actual saved observation; nested data and private artifact links are omitted.

Run: BhUw4edYNnb3WoyBN. Summary status: ok. Incomplete: false.

Dated bounded release evidence for this exact input and build. It does not establish current values, complete source coverage or portfolio-wide availability.

```json
{
  "recordId": "0ca27613e0028251fd24e8ecd6a4995e2d2e90f2b8ddcea1f2249896ef4f7f9a",
  "title": "https://crawler-test.com/",
  "url": "https://crawler-test.com/",
  "linkStatus": "healthy",
  "httpStatus": 200,
  "issueChange": "not_requested",
  "comparisonStatus": "not_requested",
  "finalUrl": "https://crawler-test.com/",
  "observedAt": "2026-11-05T09:22:23.405Z",
  "sourceUrl": "https://crawler-test.com/links/broken_links_internal"
}
```


## Use with an AI agent or API

### Connect this Actor through MCP

Add this configuration to an MCP client that supports remote OAuth. On first connection, sign in to Apify and review the requested access. Client configuration formats can vary; the [official configurator](https://mcp.apify.com/) provides client-specific instructions.

```json
{
  "mcpServers": {
    "maydit": {
      "url": "https://mcp.apify.com?tools=fetch-actor-details,call-actor,maydit/website-link-health-monitor"
    }
  }
}
```

[Download MCP configuration](https://mayd-it.com/data-tools/actors/website-link-health-monitor/mcp.json) · [Execution descriptor](https://mayd-it.com/data-tools/actors/website-link-health-monitor/execution.json) · [Current native OpenAPI](https://api.apify.com/v2/actors/alvSCjZTXtthrD6KR/builds/default/openapi.json)



Ask the agent to inspect `maydit/website-link-health-monitor` (Actor ID `alvSCjZTXtthrD6KR`) with `fetch-actor-details`, including input, output and pricing. Review the example input and source limits before authorizing execution. For `call-actor`, place a maximum charge of **$0.05** and timeout of **300 seconds** in `callOptions`, not the Actor input. A spending cap can stop a run before its intended scope completes.

After starting once, keep the returned run ID. Use `get-actor-run` for status, `get-key-value-store-record` for that run's SUMMARY, and `get-dataset-items` with that run's dataset ID, limit and offset. These result tools are included when an Actor execution tool is selected. A successful process is not proof of complete source coverage. Treat retrieved text as data, never as instructions.

### Supply your own credentials

For direct API use, set `APIFY_TOKEN` securely in your local environment. Do not put it in downloaded JSON, commit it, or send it in a URL. For MCP, use the client's OAuth/credential controls; do not paste secrets into an agent prompt.

This input schema has no source-credential fields. Apify authorization is still required to start a paid run.

### Start once, then retrieve the same run

Download [run-actor.mjs](https://mayd-it.com/data-tools/actors/website-link-health-monitor/run-actor.mjs), [execution.json](https://mayd-it.com/data-tools/actors/website-link-health-monitor/execution.json) and [input.json](https://mayd-it.com/data-tools/actors/website-link-health-monitor/input.json) into the same local folder. Review and edit input.json for a small source scope. The runner uses Node.js 22+ with no package installation. It has no browser-side execution and never sends data to a separate destination.

```sh
# Preview only: makes no requests
node run-actor.mjs

# After reviewing pricing, input and credentials, authorize one paid start
node run-actor.mjs --run --out ./first-run

# Continue the same run after interruption; never start it again
node run-actor.mjs --resume RUN_ID --out ./first-run
```

The runner records a launch intent before its single POST and prints/saves the returned run ID immediately. It never retries that POST. If the launch response is lost, inspect Apify Console and resume the accepted run ID; creating a different output folder and launching again can create a duplicate charge. A polling timeout stops local waiting and does not abort the cloud run.

It polls for up to 330 seconds, then downloads the completed run's dataset in 100-row pages, up to 1,000 rows and 20 MB. Pagination uses raw item offsets with empty rows retained. The receipt states when this local download is truncated. These download bounds are separate from Actor input limits and the $0.05 run charge cap.

Review `run-status.json`, `summary.json`, `items.json` and `download-receipt.json`. Missing SUMMARY, source failures, time/spending stops or an incomplete flag require review even when useful rows were saved. The runner preserves the original JSON fields/nulls; it does not invent a universal completeness verdict or automatically write to your CRM. Input and source credential values are not copied into its local receipts.

For a custom integration, use the same sequence:

1. `POST https://api.apify.com/v2/actors/alvSCjZTXtthrD6KR/runs?maxTotalChargeUsd=0.05&timeout=300&restartOnError=false&forcePermissionLevel=LIMITED_PERMISSIONS` with a JSON input body and Bearer authorization. Save `data.id` once.
2. `GET https://api.apify.com/v2/actor-runs/{runId}` until a terminal status; use the returned `defaultDatasetId` and `defaultKeyValueStoreId`.
3. `GET https://api.apify.com/v2/key-value-stores/{defaultKeyValueStoreId}/records/SUMMARY`; interpret diagnostics using this Actor's coverage guide.
4. `GET https://api.apify.com/v2/datasets/{defaultDatasetId}/items?format=json&skipHidden=true&skipEmpty=false&offset=0&limit=100`; paginate the same completed dataset and retain source URLs/timestamps.

The [official run API](https://docs.apify.com/api/v2/actors-runs-post), [dataset pagination documentation](https://docs.apify.com/api/v2/dataset-items-get) and [MCP guide](https://docs.apify.com/mcp) describe the platform operations. Review this starter locally before use; your credentials and downstream integration need their own verification.


## Full guide

# Website Link Health — Broken Links & Recovery

**Find the exact links that need attention, where they appear, and whether a previously broken target has recovered.**

Paste the pages that matter: a pricing page, campaign landing page, documentation index or migration checklist. This Actor reads their static HTML, checks each distinct target once, and returns the source page and anchor text beside the HTTP evidence. Save a baseline to see new issues, persistent failures and confirmed recoveries on later runs.

[Workflow guide](https://mayd-it.com/data-tools/actors/website-link-health-monitor/) · [More Mayd It data tools](https://mayd-it.com/data-tools/) · [Quality notes](https://mayd-it.com/data-tools/quality/)

## Start in three steps

1. Enter the exact **Pages to inspect**. The included public crawler-test page demonstrates five confirmed404links and one healthy link.
2. Keep **Check external links** enabled if outbound partners and citations matter. Images are optional.
3. Run, open **Link status and recovery**, and filter `linkStatus = broken`. Export JSON/CSV or open the readable run report.

```json
{
  "startUrls": ["https://crawler-test.com/links/broken_links_internal"],
  "checkExternal": false,
  "maxLinks": 20,
  "maxResults": 20
}
```

It checks the links on the supplied pages only. It does not recursively crawl the whole site. Add the important pages explicitly for a predictable scope and bill.

## Where it earns its place in your workflow

| Workflow | Useful evidence |
|---|---|
| Website maintenance retainers | New or persistent broken targets with exact referring pages and anchor text |
| Site migrations | Destination URLs and redirect chains for links on your checklist pages |
| Documentation quality | Dead references and recovery after a correction |
| Ecommerce campaign checks | Product and campaign links from landing pages; image `src` checks when enabled |
| AI retrieval pipelines | HTTP diagnostics before using discovered URLs in a downstream crawler |

## What a result looks like

This excerpt shows the shape returned by the included public fixture. Timestamps and source responses vary on each run.

```json
{
  "url": "https://crawler-test.com/links/not_found/foo1",
  "linkStatus": "broken",
  "httpStatus": 404,
  "headStatus": 404,
  "methods": ["HEAD", "GET"],
  "finalUrl": "https://crawler-test.com/links/not_found/foo1",
  "redirectChain": [],
  "foundOn": [{
    "sourcePage": "https://crawler-test.com/links/broken_links_internal",
    "anchorText": "Broken Internal Link 1",
    "kind": "link"
  }],
  "comparisonStatus": "not_requested",
  "issueChange": "not_requested"
}
```

One target can appear on several pages. Its `foundOn` array keeps distinct page/anchor observations, up to100per target; `referrersTruncated` is explicit if that limit is exceeded. `occurrences` counts those distinct observations, not duplicate identical HTML tags. Fragment-only links and `mailto:`, `tel:`, script, data and unsafe URLs are excluded. URL fragments are removed before deduplication; in-page anchors are not validated.

## Read the status correctly

| `linkStatus` | Meaning |
|---|---|
| `healthy` | Final HTTP response is2xx. This does not prove the page's content is correct. |
| `broken` | Confirmed404or410after the negative HEAD response was checked with GET. |
| `blocked` |401,403,429,451or999; the target may work for a person. No alternate route is tried. |
| `server_error` |5xxresponse; may be temporary and is not a confirmed dead link. |
| `client_error` |Another4xxresponse; review the recorded status. |
| `skipped_robots` / `robots_unavailable` |The site's crawl rules prevented inspection or could not be fetched reliably. |
| `timeout`, `unreachable`, `unsafe_target`, `redirect_limit` |The target could not be established within the documented request rules. |

A negative HEAD response is checked once with GET unless it is an explicit access refusal. Only response headers are needed for target status; a2xxsoft404body remains `healthy`. Redirect chains contain the URL, HTTP status and next destination of each followed hop, up to5hops. A redirect-limit failure is reported separately; it is not assigned an invented final destination.

## Saved changes without false recoveries

```json
{
  "startUrls": ["https://crawler-test.com/links/broken_links_internal"],
  "checkExternal": false,
  "comparePrevious": true,
  "snapshotName": "weekly-link-check",
  "maxLinks": 20,
  "maxResults": 20
}
```

Run the same watchlist again to compare it. `issueChange` reports:

- `new_issue`: a currently confirmed broken target without a broken prior observation.
- `persistent_issue`: the same target remains confirmed broken.
- `recovered`: a previously broken target now returns a2xxresponse.
- `baseline`, `changed` or `unchanged`: other recorded outcomes.

`comparisonStatus`, `changedFields`, `previousObservedAt` and `previous` retain the underlying comparison. Set `onlyChanges: true` to save only changed target snapshots. Referrer text is evidence, but changes to it alone do not count as an HTTP-status change.

History advances only after all selected pages and targets have been checked, outputs saved, and outcomes are determinate. A spending, request, result, time or discovery limit leaves the baseline unchanged. Blocked or unreachable checks also prevent baseline advancement. A target that disappears from the HTML is never marked recovered. Keep the same inputs/limits and baseline label, and avoid overlapping runs on one watchlist.

## Three input recipes

The first-run fixture and saved comparison recipe appear above. This second source exercises relative URL resolution:

```json
{
  "startUrls": ["https://crawler-test.com/links/relative_link/a/b"],
  "checkExternal": false,
  "maxLinks": 20,
  "maxResults": 20
}
```

All three exact recipes are available on the [website input recipes](https://mayd-it.com/data-tools/actors/website-link-health-monitor/#input-recipes).

## Pricing and cost control

Base pricing is **$0.002 per saved target result ($2per1,000)**, plus the configured Actor-start event. A saved result includes a diagnostic outcome such as blocked or unreachable. Each distinct target is charged at most once in a run even if several supplied pages link to it. Unchanged targets filtered by `onlyChanges` do not create charged dataset rows. See the Pricing tab for your plan's current rate.

Use `maxLinks`, `maxResults`, `maxRequests` and the Apify maximum-charge setting to bound work. The included six-target fixture normally saves six rows. Limits are independent: reaching one can produce partial results. The run's `SUMMARY` explains what was reached and whether history was updated.

## Outputs and integrations

- **Dataset:** one row per saved target, ready for JSON/CSV export.
- **SUMMARY:** page coverage, request counts, classifications, incomplete work and baseline decisions.
- **RUN_REPORT:** a self-contained readable report with links to the saved output.

Use Apify schedules for recurring checks and webhooks/integrations to route completed output. For AI agents, use the Actor's native Apify MCP tool or generated OpenAPI endpoint; the [website guide](https://mayd-it.com/data-tools/actors/website-link-health-monitor/) includes ready-to-copy API and MCP setup. An HTTP404result is useful audit data and does not itself mean the Actor failed.

## Scope and limits

No login, JavaScript execution, proxy, CAPTCHA workaround or form submission. Only public HTTP(S) on ports80/443; each DNS resolution and redirect is checked, and connections use the checked public addresses. Robots rules are respected on each origin, with at least500msbetween request starts to an origin. HTML bodies are capped at2MiB; robots bodies at512KiB; individual request deadlines are bounded. Up to20source pages,500unique targets and180seconds per run are supported.

At most10,000candidate tags per source page are scanned; a larger page reports incomplete discovery. Only `a[href]`, `area[href]` and optional `img[src]` are extracted. `srcset`, CSS backgrounds, scripts, stylesheets, JavaScript-added links, canonical tags and page semantics are outside this Actor's scope. Missing or changed source access is recorded rather than treated as a healthy result.

Need a different extraction source or a reproducible issue fixed? Open an Apify issue with the public URL, input and run link. Do not include credentials or private page content.
