# Broken Link Checker for Website SEO Audits (`zinin/broken-link-checker`) Actor

Audit links found in permitted HTML pages: unique URL checks, referrer evidence, redirect chains and a clear split between broken, blocked and unchecked links.

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

## Pricing

Pay per event

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Broken Link Checker for Website SEO Audits

Audit links discovered in permitted HTML pages with unique URL checks, referrer evidence, redirect chains and clear distinctions between broken, blocked and unchecked links.

**Evidence status (2026-09-05).** The actor was built on the Apify platform (build AelPO0Mkmb5uWRNME), its pay-per-event tariff was read back from the platform, and four private acceptance runs on Apify infrastructure produced the rows shown in this document: the owned link fixture, a page without links, a capped run and a real website (the author's own GitHub Pages site). Three independent review rounds preceded the build.

### R1. What you get

The actor extracts anchor destinations from permitted HTML, deduplicates them, checks their HTTP status and retains referring-page evidence. It treats a confirmed missing destination as a useful result, while a403 or timeout remains blocked or unknown. This difference prevents an access restriction from being misreported as a page that does not exist.

Supported sources are public HTTP(S) websites the buyer owns or is authorized to audit. The buyer is responsible for authorization and downstream use. URLs cannot contain credentials or fragments. Every redirect is checked by the same URL safety and DNS-pinned SSRF policy, and its final URL is recorded. Robots rules, 401/403 restrictions and the one-request-per-second host limit are respected; no access control is bypassed.

### R2. Who uses it

SEO reviewers can triage confirmed404/410 links. Release engineers can compare a fixed set of pages before and after a migration. Content editors can use referrers to locate where an obsolete URL appears. The output is an evidence queue; it does not edit source HTML, submit a sitemap or create external tickets.

### R3. How to run

The prefill points to links-index.html in the named Apify KVS. The three fixtures are uploaded and were checked with real KVS GETs on 2026-09-05. MANIFEST.sha256 matches the body after the 183-byte platform prefix. The missing 404 key is intentional.

Start with one source and a small result limit. Run it once, inspect both the dataset and the OUTPUT summary, and save the input next to the run ID. This gives your team a reproducible baseline before a recurring schedule starts. A successful platform status means execution finished; it does not by itself establish that a useful result was obtained. Check found, resultCount, partial and error in the actual rows.

Use the dataset preview to inspect nested values before exporting to a spreadsheet. JSON preserves arrays, nulls and booleans most faithfully. CSV is a presentation format: arrays may appear as serialized values and spreadsheet applications can reinterpret strings. Download a JSON copy when you need an audit record. Keep the source URL and checkedAt value attached to every derived row so a colleague can identify the observation behind a decision.

After that first inspection, connect the run to an existing Apify schedule or your own workflow. Choose a frequency that matches how often the source changes. Scheduling faster than the publisher updates usually adds cost without adding evidence. A schedule is a series of independent runs, each with its own input, resource limits and start event. The actor does not create schedules, send messages or modify the source on your behalf.

Before handing the workflow to someone else, document the expected paid unit, the chosen caps and the action for an empty or partial response. Give the receiving team one successful dataset example and one free failure example from your accepted runs. Do not teach an integration to treat every dataset row as a billable result: explanatory rows deliberately share the same dataset so they remain visible during export.

### R4. Pricing

A confirmed404 is billable because it completes the exact question the buyer asked: whether that unique destination exists at the observed time. A403,429, timeout or unsafe destination does not complete that question and has no result event. Fetching a crawl page solely to discover links is also not a paid check.

The price is **$0.003 per start + $0.001 per url**, read back from the platform tariff on 2026-09-05 (`apify-actor-start` $0.003, `result-found` $0.001, model PAY\_PER\_EVENT). The primary paid unit is one completed check of a unique discovered URL, including a proven HTTP404 or410. Crawl-page retrieval and skipped or uncertain checks are not paid units. The start event is charged once per GB of run memory: the default 256 MB run pays one start unit, while the acceptance runs below were started with 4096 MB and therefore show `apify-actor-start: 4`. Keep the default memory unless a site needs more.

| Completed paid units | Proposed total including one start |
|---|---|
| 0 | $0.0030 |
| 1 | $0.0040 |
| 5 | $0.0080 |
| 20 | $0.0230 |
| 100 | $0.1030 |

These are arithmetic examples at the tariff above, not receipts. Input and response limits can require multiple runs; each additional run has its own start. Free result rows do not refund a start event that has already occurred.

Billing follows delivery of the documented unit. One completed url is written with the named result-found event in the same SDK call. An incomplete unit or diagnostic notice is written without that event. The distinction is visible in found and resultCount; the OUTPUT summary separately reports delivered, paid and free. A dataset can therefore contain more rows than the number of paid results. Count result events when reconciling invoices.

The actor reads the actual run pricing before source work. On the platform, a missing result price, unreadable pricing or a priced automatic dataset-item event stops the run. If writing an explanation might itself cost money, the explanation stays in the log and failed run status. The start event may already have been charged. A failed run is not a promise that every previous activity was free, so reconcile the recorded event counts rather than assuming a refund.

A budget check runs inside the same lock as the paid write. Concurrent delivery attempts cannot both reserve the same last available amount. The check compares remaining money with the next row price before calling the SDK. This guards against the SDK behavior that can otherwise permit one final row beyond the limit. A budget just below one complete unit produces zero additional paid rows; it is not rounded up to buy a unit the budget does not cover.

If the SDK says the last paid row reached the event limit, that row still counts as delivered and paid. Only subsequent work stops. If delivery or charging throws, the outcome can be uncertain: a dataset write may have succeeded before a charge response failed. The actor stops and marks the run failed instead of automatically retrying the row. Reconcile the dataset and event counters before rerunning an uncertain delivery, because a new run can repeat previously obtained data.

Platform evidence for the tariff above (charged event counts of the accepted runs): fixture audit xE8pbbgtoxGuAmbVF `result-found: 2` (one working link and one proven 404, the external link free); page without links LuR3OVvO54bK7sNGj `0`; maxLinks=1 run adkmbQyYM7TelFGrC `1` plus a free partial notice; real website https://timzinin.com/ run RcbLLQftHbae97aiQ `3` (three internal pages checked, 22 external links reported free because checkExternal was false). Platform account subscriptions, prepaid credits and discounts are governed by the account's current terms.

### R5. Input contract

The internal fixture links are relative file names, such as links-ok.html. Resolving them against the KVS record URL preserves the records/ prefix. KVS keys are flat: links/index.html would not be the correct key. The intentionally missing key links-missing-404.html must never be uploaded; its404 JSON response is part of the test contract.

```json
{
  "startUrls": [
    "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html"
  ],
  "checkExternal": false,
  "maxPages": 5,
  "maxLinks": 20
}
```

#### startUrls

One to five absolute public HTTP(S) start URLs on sites you own or are authorized to audit. The prefill remains our daily KVS fixture.

Type: `array`. Default/prefill: `["https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html"]`. Declared constraints: `{"maxItems": 5}`. The runtime validates this field before starting source work; invalid values produce a failed run through the free log/status channel. Numeric strings and truthy strings are not silently accepted as numbers or booleans.

#### checkExternal

With false, off-origin discovered links are free unchecked\_external rows without a request. True checks public off-origin destinations with robots, host pacing and HEAD→GET on 405/501.

Type: `boolean`. Default/prefill: `false`. Declared constraints: `{}`. The runtime validates this field before starting source work; invalid values produce a failed run through the free log/status channel. Numeric strings and truthy strings are not silently accepted as numbers or booleans.

#### maxPages

Maximum HTML crawl pages, including starting pages. Reaching this limit can leave discovery partial while completed URL checks remain billable.

Type: `integer`. Default/prefill: `5`. Declared constraints: `{"minimum": 1, "maximum": 20}`. The runtime validates this field before starting source work; invalid values produce a failed run through the free log/status channel. Numeric strings and truthy strings are not silently accepted as numbers or booleans.

#### maxLinks

Maximum distinct eligible URLs actually checked with HEAD or fallback GET. Redirect hops and fallback requests do not create extra paid URL units.

Type: `integer`. Default/prefill: `20`. Declared constraints: `{"minimum": 1, "maximum": 200}`. The runtime validates this field before starting source work; invalid values produce a failed run through the free log/status channel. Numeric strings and truthy strings are not silently accepted as numbers or booleans.

Send one JSON object as input. Arrays are bounded before work starts, and the implementation rejects unknown input fields rather than silently ignoring a misspelled option. Use JSON booleans true and false, not the strings "true" or "false". Use integer values for limits. Limits are ceilings, not promises that the source contains that many useful records. A valid request can return fewer records or a free status row.

Supported sources are public HTTP(S) websites the buyer owns or is authorized to audit. The buyer is responsible for authorization and downstream use. URLs cannot contain credentials or fragments. Every redirect is checked by the same URL safety and DNS-pinned SSRF policy, and its final URL is recorded. Robots rules, 401/403 restrictions and the one-request-per-second host limit are respected; no access control is bypassed.

The caps work together. Increasing a per-source limit does not override the run deadline, byte limit, maximum number of input sources or output-size limits. Split a large job into bounded runs and record which inputs belong to each batch. If you change a limit between runs, preserve that change in your evidence log: two outputs collected under different truncation settings are not directly comparable without checking partial flags.

Defaults are intended to make a small first run inspectable. They are not a production source recommendation or a guarantee of permanent availability. A source owner can move content, change markup or temporarily reject requests. Keep your selected production input in a saved task after acceptance so an updated default does not silently change what your schedule monitors. Recheck permissions and source scope whenever the production URL changes.

### R6. Output examples and result states

Expected synthetic fixture behavior: one complete ok result for links-ok.html, one complete broken result for links-missing-404.html and one free unchecked\_external row for https://example.com/. mailto:, the same-document fragment and the duplicate link create no extra checks. The empty fixture has no anchors and yields exact not\_found/false/empty-error/zero.

A301→200 redirect is exercised against a local HTTP server because immutable KVS records cannot express that HTTP status behavior. The local test verifies the full chain. A separate405→200 test verifies HEAD fallback, and403 verifies that blocked access is free.

A row with found=true and resultCount=1 represents the actor's documented completed unit. A row with found=false and resultCount=0 needs its status interpreted. Clean absence has an empty error string; transport, parsing and delivery problems carry a nonempty error or fatal run summary. Do not collapse these cases into a single empty-table branch. They answer different operational questions and require different follow-up actions.

The partial flag describes completeness under the selected input and caps. It can appear on a free notice after complete units have already been delivered, or on an incomplete unit that is not billable. The per-product rules in this section tell you which case applies. A downstream workflow should preserve the flag even when it keeps the useful data. Removing the warning during CSV flattening can turn an honest limited observation into an unsupported claim of completeness.

The default key-value store record OUTPUT is the run summary. Requested describes the input scope; processed describes units or inputs examined according to this actor's documented counter; unprocessed identifies remaining work. Delivered and paid describe successful result delivery, free describes explanatory rows, failed counts delivered error notices, and deliveryUncertain identifies writes that require reconciliation. These counts are not interchangeable with HTTP request count.

The examples below are dataset rows from private acceptance runs executed on Apify infrastructure on 2026-09-05 with the deployed build and the live tariff. They are unedited; where a value is shortened for display the marker is inside the value.

#### Accepted platform run examples (2026-09-05)

Examples A–C come from run xE8pbbgtoxGuAmbVF on the owned fixture page: a working internal link (paid), a proven 404 (paid, error stays empty) and an external link left unchecked (free). Example D is the free row for a valid page without links, run LuR3OVvO54bK7sNGj. Example E is the free partial notice from run adkmbQyYM7TelFGrC (maxLinks=1, one paid check before it). Example F is a paid row from the real website run RcbLLQftHbae97aiQ.

Example A — working internal link, paid.

```json
{
  "input": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-ok.html",
  "sourceUrl": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html",
  "found": true,
  "status": "ok",
  "resultCount": 1,
  "partial": false,
  "error": "",
  "warnings": [],
  "checkedAt": "2026-09-05T13:07:15.618Z",
  "schemaVersion": "1.0",
  "httpStatus": 200,
  "method": "HEAD",
  "redirectChain": [],
  "finalUrl": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-ok.html",
  "linkStatus": "ok",
  "url": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-ok.html",
  "referrers": [
    "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html"
  ],
  "referrersTruncated": false,
  "isInternal": true,
  "checkedUrlCount": 2,
  "syntheticFixture": true
}
```

Example B — proven 404, paid: found=true, linkStatus=broken, error="".

```json
{
  "input": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-missing-404.html",
  "sourceUrl": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html",
  "found": true,
  "status": "ok",
  "resultCount": 1,
  "partial": false,
  "error": "",
  "warnings": [],
  "checkedAt": "2026-09-05T13:07:15.717Z",
  "schemaVersion": "1.0",
  "httpStatus": 404,
  "method": "HEAD",
  "redirectChain": [],
  "finalUrl": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-missing-404.html",
  "linkStatus": "broken",
  "url": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-missing-404.html",
  "referrers": [
    "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html"
  ],
  "referrersTruncated": false,
  "isInternal": true,
  "checkedUrlCount": 2,
  "syntheticFixture": true
}
```

Example C — external destination with checkExternal=false, free.

```json
{
  "input": "https://example.com/",
  "sourceUrl": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html",
  "found": false,
  "status": "unchecked_external",
  "resultCount": 0,
  "partial": false,
  "error": "",
  "warnings": [],
  "checkedAt": "2026-09-05T13:07:15.759Z",
  "schemaVersion": "1.0",
  "linkStatus": "unchecked_external",
  "httpStatus": null,
  "method": null,
  "redirectChain": [],
  "finalUrl": null,
  "url": "https://example.com/",
  "referrers": [
    "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html"
  ],
  "referrersTruncated": false,
  "isInternal": false,
  "checkedUrlCount": 2,
  "syntheticFixture": true
}
```

Example D — valid page without eligible links, free not\_found.

```json
{
  "input": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-empty.html",
  "sourceUrl": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-empty.html",
  "found": false,
  "status": "not_found",
  "resultCount": 0,
  "partial": false,
  "error": "",
  "warnings": [],
  "checkedAt": "2026-09-05T13:07:20.084Z",
  "schemaVersion": "1.0",
  "checkedUrlCount": 0,
  "syntheticFixture": true
}
```

Example E — maxLinks reached, free partial notice.

```json
{
  "input": null,
  "sourceUrl": "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html",
  "found": false,
  "status": "partial",
  "resultCount": 0,
  "partial": true,
  "error": "",
  "warnings": [
    "Crawl, link, request, deadline or delivery limit left work incomplete."
  ],
  "checkedAt": "2026-09-05T13:07:26.921Z",
  "schemaVersion": "1.0",
  "checkedUrlCount": 1
}
```

Example F — real website page, paid.

```json
{
  "input": "https://timzinin.com/terms.html",
  "sourceUrl": "https://timzinin.com/",
  "found": true,
  "status": "ok",
  "resultCount": 1,
  "partial": false,
  "error": "",
  "warnings": [],
  "checkedAt": "2026-09-05T13:07:35.013Z",
  "schemaVersion": "1.0",
  "httpStatus": 200,
  "method": "HEAD",
  "redirectChain": [],
  "finalUrl": "https://timzinin.com/terms.html",
  "linkStatus": "ok",
  "url": "https://timzinin.com/terms.html",
  "referrers": [
    "https://timzinin.com/"
  ],
  "referrersTruncated": false,
  "isInternal": true,
  "checkedUrlCount": 3,
  "syntheticFixture": false
}
```

### R7. Field dictionary

#### `input`

Normalized discovered URL for a link row, starting page for a crawl-page error, and null for a run-level partial notice. Do not assume every row refers to a checked destination.

#### `sourceUrl`

First observed referring page for a URL result. The complete retained referring-page list is in referrers. For page-level errors this is the requested crawl page.

#### `url`

The unique normalized destination whose HTTP behavior was checked. Fragments are removed, query values are preserved by discovery, and redirects do not overwrite this identity. Scope can still refuse a query-bearing destination.

#### `referrers`

Up to20 distinct pages containing this URL. Duplicate anchors on one page do not duplicate the page entry. Referrers are collected before output delivery so later pages can add evidence to an earlier discovered URL.

#### `referrersTruncated`

True if more than20 referring pages were found. The URL check can still be complete and billable; the warning identifies limited provenance rather than an incomplete HTTP status.

#### `httpStatus`

Final HTTP status if one was received. A completed404/410 proves the documented broken-link condition. Null means no final response was obtained or the link was deliberately unchecked.

#### `method`

HEAD for the normal check. GET only when HEAD returns405 or501 and the permitted fallback completes or fails. A skipped link has null; an unknown response can still identify which method was attempted.

#### `redirectChain`

Ordered objects with url, status and absolute location for every observed301/302/303/307/308 hop. At most five are followed per attempt. A forbidden destination is not requested even though its advertised Location can appear as evidence.

#### `finalUrl`

Final requested response URL when a final response was obtained, otherwise null. Keep both url and finalUrl to distinguish the original link from where it ended.

#### `isInternal`

Whether the original destination shares an origin with one of the starting pages. This describes URL topology. The buyer is responsible for authorization for every site checked.

#### `linkStatus`

ok means a completed2xx without redirects; redirected means a completed2xx reached through redirects; broken means404/410. blocked includes401/403/429, unknown covers other incomplete outcomes, unchecked\_external follows the checkbox, and unchecked\_rights denotes a safety policy refusal (private network, URL credentials or unsupported scheme).

#### `checkedUrlCount`

Number of unique URL checks attempted in this run when output is finalized. It is repeated as run context on link rows, not an incremental invoice or a number to sum across rows.

#### `found`

For this actor, true means the HTTP check is complete. In particular, a broken404 link has found=true and is a useful paid result. Use linkStatus to decide whether the link works.

#### `status`

Top-level ok for completed checks, including broken destinations. Other values describe a skipped or incomplete link, source\_error for a failed crawl page, not\_found for no links, and partial for an incomplete crawl notice.

#### `resultCount`

One for a completed URL check, zero for all free notices. A redirect chain or multiple referrers do not multiply this count.

#### `partial`

A run-level free notice marks page, link or delivery caps. Complete URL rows remain complete even if the surrounding crawl is incomplete. Inspect the OUTPUT summary before claiming a whole-site audit.

#### `error`

Empty for completed200 or404 checks, clean no-links and checkbox-skipped external links. Nonempty for blocked, transport, source-scope and crawl-page failures. Do not label every nonempty error as a broken page.

#### `warnings`

Additional limitations such as truncated referrers or incomplete discovery. These stay attached to the evidence through exports.

#### `checkedAt`

UTC observation time for the finalized result row. HTTP outcomes can change immediately afterward. Use it as evidence of the audit attempt, not a permanent guarantee of destination availability.

#### `syntheticFixture`

True when all start URLs are from the named owned Wave A KVS fixture scope. Synthetic content tests behavior and is not a real commercial site canary. Local HTTP proof rows are identified by their local fixture URL and proof log.

#### `schemaVersion`

Row-contract version1.0. Store it with an audit export if your integration depends on the present distinction between top-level status and linkStatus.

### R8. Evidence and boundaries

The KVS owner permission and flat key prefix are recorded in WAVE\_A\_REVIEW.md. A read-only probe on2026-09-05 observed robots HTTP200 with User-agent:\* and no prohibition. The starting key initially returned404 before upload. That is recorded as current hosting state, not hidden by substituting a bundled success response in production.

Every observation is tied to the response actually obtained. checkedAt is the actor's collection time; it does not automatically mean that the source data was published or updated at that time. Cached pages and feeds can be older than their HTTP response time. Source dates, revisions and validators should remain separate fields in your downstream model. If a publisher omits a field, absence is preserved rather than replaced with a plausible value.

The HTTP transport validates the URL before connection, checks every DNS answer, rejects private and loopback addresses and pins the approved address to the connection. Redirects are checked again, including redirects that appear to remain on the same host. These safeguards apply to robots requests as well as content requests. A redirect into a forbidden host or route stops that request; the actor does not follow it because the first URL looked acceptable.

Responses are bounded in bytes and time. At most five redirects are followed per attempt. Network failures, 429 and server errors can receive at most two retries with bounded backoff; Retry-After is considered within the remaining deadline. Ordinary client errors and invalid successful-response formats are not retried as if waiting would repair them. A timeout describes what the actor observed; it does not prove that the source is permanently unavailable.

Supported sources are public HTTP(S) websites the buyer owns or is authorized to audit. The buyer is responsible for authorization and downstream use. URLs cannot contain credentials or fragments. Every redirect is checked by the same URL safety and DNS-pinned SSRF policy, and its final URL is recorded. Robots rules, 401/403 restrictions and the one-request-per-second host limit are respected; no access control is bypassed.

Exported text remains untrusted source data. The parser does not execute scripts or follow instructions embedded in content. If you later send the text to an AI workflow, place it in a data field and give that workflow its own instruction boundary. Do not let a publisher-controlled title, description or table cell choose credentials, network destinations or workflow permissions.

### R9. Decision routing

Send linkStatus=broken to an editorial repair queue with referrers. Send redirected results to a canonical-link review if your site policy prefers direct destinations. Keep blocked and unknown results in a separate access-health queue. The top-level found value alone is insufficient to decide whether a link should be repaired.

Create explicit routing branches for complete results, clean absence, partial observations, source errors and fatal delivery problems. The normal success branch should require the documented completed-unit fields, not merely a nonempty dataset. The absence branch can record that no matching content was observed. The partial branch should retain useful complete units while opening a review item for the missing scope. The error branch should avoid overwriting the last good source record with an empty value.

Use a small retry policy in the surrounding workflow. The actor already retries suitable transient HTTP failures. Immediately restarting the entire run several times can multiply start charges and repeat already delivered units. A delayed retry with the same saved input is easier to reconcile. If the run reports delivery uncertainty, compare the dataset and result event counts first; an automatic full retry is especially inappropriate when the previous write may have landed.

Keep decisions proportional to evidence. A machine-readable status is suitable for triage, a review queue or a candidate change list. It is not a substitute for inspecting the underlying source before a consequential decision. Attach the source URL, collection time and relevant evidence fields to any ticket or report your workflow creates. That lets an operator verify the observation without rerunning the entire extraction.

When the result changes between runs, compare the same source identity and the same input limits. A different cap, redirect destination or source revision can explain the difference. Do not label a missing row as a deletion until you have ruled out partial output, a changed source scope and a failed request. Preserve the last successful observation alongside the latest attempt so outages do not look like intentional content removal.

### R10. Commercial playbooks

For a release checklist, preserve the exact start pages, caps and checkExternal setting. Compare unique destination URLs across accepted runs. A reduction in checked URLs may reflect a smaller crawl or earlier limit, so inspect pages, discovered and partial before treating it as an improvement.

For a content cleanup, group broken destinations by referrer page and review the original anchor in the browser. Multiple bad links on one page can be one editorial task, while one shared bad destination can require changes on many pages. The actor retains both sides of that mapping.

For a recurring operational report, start by defining the question the report must answer and the smallest source set that can answer it. Store each run's input, summary and result dataset together. Produce a compact review view for colleagues, while retaining the original JSON for audit. This creates a traceable chain from a source observation to a business action without pretending that extraction alone proves the action was correct.

For a client deliverable, agree on the unit, source scope and completeness rules before collection. Explain whether a capped response is acceptable and who reviews failures. Quote the proposed extraction cost separately from your own analysis and service fee. A small technical result can still require editorial or operational judgment, and the actor does not promise revenue, savings or a particular conversion rate from that judgment.

For a migration or recurring comparison, freeze the input configuration for the comparison period. Assign a batch identifier in your own workflow and save a baseline dataset. Run the same source scope again after the change, then compare stable identities and evidence rather than dataset row positions. Row order is useful for inspection but is generally a poor business key. Mark differences whose underlying run was partial as requiring review.

For an internal acceptance checklist, have someone other than the builder inspect a successful result and a deliberate negative. Verify source identity, the paid-unit count and how the integration handles a free notice. A positive demo alone does not exercise the branches most likely to cause misleading dashboards. Keep this check small enough to repeat when the actor version, source markup or saved input changes.

### R11. Integration recipes

Export url, referrers, linkStatus, httpStatus, finalUrl, redirectChain and checkedAt to your review queue. Keep the original URL as the audit key even if the destination redirects. A URL containing query parameters can represent a distinct resource, so the discovery layer does not remove those parameters to make deduplication look cleaner.

#### Dataset API and typed storage

Use the dataset identifier from the completed run when fetching results. Store the original JSON response before projecting columns into your application database. Preserve booleans as booleans, null as null and arrays as arrays; stringifying everything makes later validation harder. If your warehouse needs flat tables, create parent and child records with an explicit parent key and retain the source URL on the parent.

#### n8n or Make

Create a workflow that starts the saved task, waits for the run to finish, reads the summary and then downloads the dataset. Put the summary-status branch before the action that updates production records. Route partial and fatal runs to a review path. Avoid treating "run finished" as "all expected data arrived". Keep any credentials in the workflow platform's secret storage rather than placing them in source URLs or exported dataset columns.

#### Webhook receiver

A completion notification is a signal to fetch the run artifacts. It is not a complete substitute for those artifacts. Make your receiver idempotent using the run identifier and your own processing ledger, because webhooks can be delivered more than once. Acknowledge the notification promptly and process large exports separately. This prevents a slow database transaction from causing repeated notifications and duplicate downstream actions.

#### Spreadsheet review

Choose a stable set of top-level columns and keep a separate JSON archive for nested fields. Add a visible status or partial column next to the business content rather than hiding warnings on another sheet. When opening CSV in spreadsheet software, import identifiers and source text as text where appropriate. Spreadsheet auto-formatting can alter leading zeros, dates and strings beginning with formula characters. That is a downstream import concern, not evidence that the source changed.

#### Application database

Use a staging table before merging into a canonical dataset. Validate expected types, source identity and completed-unit status in staging. Keep a record of the input settings and actor version with the batch. Commit the merge only after your chosen completeness checks pass. An empty or failed extraction should create an attempt record without deleting the last known good data. This pattern makes source outages visible while protecting previously reviewed information.

#### Human review queue

Create one review record per actionable observation and include the original source URL, collection time, reason and relevant evidence. Group related observations in your own application rather than dropping provenance to reduce row count. The actor supplies evidence; your integration decides who receives it. It does not send emails, create tickets or message third parties by itself.

#### Reproducible exports

Save the input JSON, run ID, default dataset ID and OUTPUT summary together with your export. If a recipient asks why a value was included, you can reconstruct the observation without guessing which run produced the file. When you transform text or flatten nested data, document the transformation in your own export metadata. A derived CSV should not be presented as an untouched source response.

### R12. Operating guide

The crawler reads anchors from received HTML without executing JavaScript. It ignores a base element when resolving relative links so untrusted markup cannot silently move them to another host. It ignores fragment-only and non-HTTP schemes. There is no browser, sitemap expansion, authentication or automatic form submission.

The run caps are20HTML pages,200discovered destinations,200checks and400HTTP requests. Each HTML response is at most1MB. A separate HEAD check can be followed by a crawl GET for an internal HTML destination; that is one paid URL check plus discovery work, not two paid results.

#### Resource envelope

The deployment defaults to a 300-second run timeout and a shorter 240-second working deadline. Per-response byte caps, input caps and request ceilings provide additional bounds. The shorter deadline leaves time to stop source work and write a summary before the platform timeout. It cannot guarantee recovery from every external failure, such as a storage service that remains unavailable during finalization.

Requests are paced per host. The one-request-per-second setting is this actor's own conservative operating limit, not a claim about an upstream service's published quota. Redirects, robots reads and retries consume requests too. Increasing the number of sources can therefore increase elapsed time even when few paid units result. Do not estimate runtime solely from the number of dataset rows. A robots.txt Crawl-delay for the actor's user agent raises that host's interval up to 30 seconds; a larger Crawl-delay does not fit the run budget, so that origin is reported as blocked without requests.

#### Read the summary first

Compare requested, processed and unprocessed to understand how much of the input was handled. Then compare delivered and paid with result events, and inspect free and failed for diagnostic rows. HTTP request count measures transport activity, not billable output. A batch can require several requests for a single unit, and a free source failure still consumes bounded runtime. Preserve these counter meanings in dashboards and support tickets.

#### Change management

Recheck your saved task after an actor version change or a source format change. Run the smallest representative input, inspect the typed output and compare the paid unit count with expectations. If your application depends on a nested field, test that field specifically rather than only checking that a dataset exists. Keep the previous accepted export so you can distinguish a parser regression from a real upstream content change.

#### Reconciliation

When delivery is uncertain, record the run ID before doing anything else. Download the dataset and inspect chargedEventCounts from the run. Compare the results with the OUTPUT counters and log messages. The actor does not claim a transaction spanning external storage, a charge service and your downstream database. A failed charge response can leave useful data present, so never infer non-delivery from a failed run status alone.

#### Troubleshooting workflow

Start with the exact input that failed, the run summary and one affected source URL. Check whether the failure was input validation, URL safety refusal, robots refusal, DNS or transport failure, response format, a resource cap or delivery. Change one factor at a time and rerun a small bounded case. Removing all caps or broadening the source scope to chase a single failure usually obscures the reason rather than resolving it.

#### Acceptance and release

Local tests exercise the actual Apify SDK charging path with controlled persistence and charge transport boundaries. They cover exact budget boundaries, concurrency, missing pricing, priced dataset writes and failures before and after dataset delivery. They are necessary implementation evidence but do not replace a private platform build, real source canary, tariff readback or independent acceptance. The build draft remains a draft until those separate checks are recorded.

### R13. FAQ and limitations

**Is403 a broken link?** No. It proves access was refused under the request conditions. It remains blocked and free.

**Why does404 have found=true?** Here found means a complete HTTP audit result, not a successful destination. linkStatus=broken carries the website-health meaning.

**What does checkExternal=true check?** Public off-origin destinations authorized by the buyer, subject to robots, host pacing and the same URL safety policy at every redirect.

**Are fragments checked?** No. Fragments are removed for URL deduplication; the actor does not verify element IDs inside pages.

**Why does the dataset contain a row with no business content?** The actor returns free status rows to distinguish clean absence, limits and errors. Those rows keep the reason visible in the same export. Filter on the documented completed-unit contract when calculating useful output, and retain status rows in an operational log so the integration does not hide failures.

**Can I remove every limit for a large job?** No. The actor has a bounded product contract, including runtime, input and response limits. Break larger jobs into reviewed batches. A source that needs a browser, authentication or a different endpoint requires a separate capability decision rather than an unlimited flag.

Supported sources are public HTTP(S) websites the buyer owns or is authorized to audit. The buyer is responsible for authorization and downstream use. URLs cannot contain credentials or fragments. Every redirect is checked by the same URL safety and DNS-pinned SSRF policy, and its final URL is recorded. Robots rules, 401/403 restrictions and the one-request-per-second host limit are respected; no access control is bypassed.

**Will a retry produce exactly the same output?** Only if the source and relevant state remain unchanged and the same limits are used. Live content can change between attempts. Keep source identities, revisions or hashes where available and retain the input. Do not promise byte-identical exports from a changing external source.

**What should I send in a bug report?** Include the actor version or build ID, run ID, a minimal redacted input, the expected contract and the actual status or field. Remove tokens and confidential source content. One representative row plus the summary is usually more useful than a large unfiltered export. Explain whether the issue reproduces in a fresh small run.

**Is this page a guarantee of platform cost or availability?** No. The prices above were read back from the deployed tariff on 2026-09-05; check the tariff shown on the Store page before a large run. The source can change independently of the actor. Local parser and SDK tests establish specific behavior under tested conditions; cloud acceptance and ongoing operational monitoring establish different facts.

### R14. Sources, rights and support

The owned fixture files and MANIFEST.sha256 define immutable v1 content. New content requires a new versioned key, not overwriting accepted bytes. The KVS API404 JSON response is intentionally valid broken-link evidence. Primary protocol references include [HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110) for methods and response status classes and [Apify key-value storage](https://docs.apify.com/platform/storage/key-value-store) for record addressing. This draft makes no blanket permission claim for arbitrary external destinations.

Use only sources and content that you are authorized to process for your intended purpose. Preserve publisher attribution and applicable license notices in derived work. A conversion to JSON does not remove the obligations attached to the original content. Where the source has additional notices, keep those notices with the evidence and review them before redistribution.

Supported sources are public HTTP(S) websites the buyer owns or is authorized to audit. The buyer is responsible for authorization and downstream use. URLs cannot contain credentials or fragments. Every redirect is checked by the same URL safety and DNS-pinned SSRF policy, and its final URL is recorded. Robots rules, 401/403 restrictions and the one-request-per-second host limit are respected; no access control is bypassed.

For support, use the issue channel on the actor's Apify page after publication and include a reproducible minimal case. Deployment-specific evidence should include the build ID and run ID so support can compare the code and pricing used by that run. Never include API tokens, private cookies or unrelated personal data in a public issue. For a new capability, describe the input source, expected unit and the decision your integration needs to make.

Related-tool links below point only to published actors of the same author. The output examples above are accepted platform rows, not local fixture output.

Fixture integrity: Apify KVS prepends a 183-byte cookie-shield script to text/html records before the doctype (observed 2026-09-05). MANIFEST.sha256 describes the body after the platform prefix, not the complete HTTP body. This prefix does not change anchor destinations. `unchecked_rights` is the retained enum name for a safety policy refusal (private network, URL credentials or unsupported scheme).

#### Related tools

Related tools for adjacent workflows in AI and search visibility.

| Actor | What it does |
|---|---|
| [AI Crawler Access Checker](https://apify.com/zinin/ai-crawler-access-checker) | Pair it in the AI and search visibility workflow: Audit up to 100 sites for 16 AI crawler policies |
| [Domain Health Checker](https://apify.com/zinin/domain-health-checker) | Pair it in the AI and search visibility workflow: Audit DNS resolution, MX, SPF, DMARC, name servers, and TLS for a list of domains |
| [llms.txt Auditor & AI Crawler Policy Checker](https://apify.com/zinin/llms-txt-auditor) | Pair it in the AI and search visibility workflow: Audit public llms.txt, llms-full.txt, and root robots.txt rules for nine named AI crawlers |
| [Social Preview Checker](https://apify.com/zinin/social-preview-checker) | Pair it in the AI and search visibility workflow: Audit observed Open Graph and Twitter Card metadata for up to 100 authorized public pages |
| [AI Answer & Citation Change Monitor](https://apify.com/zinin/ai-answer-change-alert) | Pair it in the AI and search visibility workflow: Monitor grounded AI answers by query, model, and language; detect rewrites and cited-domain additions or... |

# Actor input Schema

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

One to five absolute public HTTP(S) URLs on websites you own or are authorized to audit. No URL credentials or fragments. Robots, private-network rejection, redirects and host pacing apply. The prefill is our synthetic daily fixture.

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

False reports off-origin links as free unchecked\_external rows without requesting them. True checks public off-origin destinations with HEAD, GET fallback on 405/501, robots per origin and at most one request per second per host. You are responsible for authorization.

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

Maximum HTML crawl pages, including starting pages. Reaching this limit can leave discovery partial while completed URL checks remain billable.

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

Maximum distinct eligible URLs actually checked with HEAD or fallback GET. Redirect hops and fallback requests do not create extra paid URL units.

## Actor input object example

```json
{
  "startUrls": [
    "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html"
  ],
  "checkExternal": false,
  "maxPages": 5,
  "maxLinks": 20
}
```

# 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": [
        "https://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html"
    ],
    "checkExternal": false,
    "maxPages": 5,
    "maxLinks": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/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://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html"],
    "checkExternal": False,
    "maxPages": 5,
    "maxLinks": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("zinin/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://api.apify.com/v2/key-value-stores/b0cgFsJRqVv3XOZyG/records/links-index.html"
  ],
  "checkExternal": false,
  "maxPages": 5,
  "maxLinks": 20
}' |
apify call zinin/broken-link-checker --silent --output-dataset

```

## MCP server setup

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