# Documentation Change Feed (`riprap-tools/docs-change-feed`) Actor

Track documentation changes from public page URLs. Compare saved section snapshots and return added, removed and modified sections with text diffs. Create a baseline on the first successful check. No browser, no AI. Pay per page checked, including unchanged pages. Failed fetches are not charged.

- **URL**: https://apify.com/riprap-tools/docs-change-feed.md
- **Developed by:** [Riprap Tools](https://apify.com/riprap-tools) (community)
- **Categories:** Developer tools, Automation, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 page checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Documentation Change Feed

### What it does

Documentation Change Feed by Riprap Tools checks public documentation pages against saved section snapshots. It returns added, removed and modified sections with text diffs. The first successful check creates a baseline; later checks report `unchanged` or `changed` against the last saved check. Processing is rule-based, with no AI.

It is for developers, SDK maintainers, technical writers and teams watching upstream vendor or API documentation. It detects text changes, not whether an edit is a breaking change. You decide which changes need action.

### What you can use it for

- Watch a documentation page for `changeType: "changed"` before updating an integration. Change detail follows the page's headings and section ids, not individual functions.
- Schedule daily checks and use `changeType: "changed"` in your own alerting workflow.
- Track a vendor changelog or migration-guide page without monitoring an entire site.
- Keep an audit trail of documentation edits by retaining dataset results from successive runs.
- Help technical writers spot upstream edits that may require changes to their own guides.

### How to use it

1. Enter public page URLs in the Input tab. Choose pages containing the documentation itself, not a directory you expect the Actor to crawl.
2. Set `maxPages` and, optionally, a memorable `stateKey`. Start the run and read the default dataset. A new page produces `baseline_created` with empty change arrays.
3. Run again with the same URLs and key. Successful checks update the baseline, so each comparison covers changes since the last saved check—not since the first run.
4. For recurring checks, use Apify Schedules, a platform feature. Connect results to your own alerting or reporting system; this Actor does not send notifications.

You can also submit the same input JSON through Apify's Actor run API. For example:

```json
{
  "pageUrls": ["https://docs.python.org/3/library/colorsys.html"],
  "maxPages": 1,
  "stateKey": "python-colorsys"
}
```

### Input

| Field | Type | Default | Limits and behavior |
|---|---|---|---|
| `pageUrls` | array of strings | `["https://docs.python.org/3/library/colorsys.html"]` | Schema maximum: 500 items. Runtime requires 1–500 public HTTP(S) URLs, without URL credentials. |
| `maxPages` | integer | `1` | Minimum 1, maximum 500. Checks at most this many distinct input URLs, in input order. |
| `ignoreSelectors` | array of strings | `[]` | Remove elements before sectioning; only tag, `.class`, `#id`, no compound selectors/combinators/attributes. At most 50 strings, 1–200 characters each. |
| `ignoreText` | array of strings | `[]` | Remove text lines/fragments containing a case-sensitive literal substring (no regex), before hashing. At most 50 strings, 1–200 characters each. |
| `stateKey` | string | Not set; derived from URLs | Optional baseline name: 1–100 letters, digits, underscores or hyphens. No schema default or numeric bounds. |

Without a `stateKey`, the Actor uses a SHA256 hash of the sorted, distinct submitted URL list, before applying `maxPages`. Reordering URLs does not change this key; changing the URL set does. Invalid names are rejected with a `stateKey` error; omit the field (or use null or an empty string) to derive a key. Duplicate URLs are checked once.

Changing ignore filters between runs may change hashes. Only the first `maxPages` distinct URLs are checked (default **1**), not every submitted URL. Remaining URLs appear in SUMMARY with a warning; they are not charged.

Reuse the same key and page selection for comparisons. To reset, choose a new, unused key or delete the old key's record in the baseline store. After a successful snapshot write, only pages selected by this run remain under that key; excluded pages lose their saved baseline. A failed selected page retains its previous snapshot.

### Output

Each successfully checked page produces one default-dataset item:

| Field | Type | Meaning |
|---|---|---|
| `url` | string | Submitted page URL. |
| `changeType` | string | `baseline_created`, `unchanged` or `changed`. |
| `added` | array | Sections present only in the current snapshot. |
| `removed` | array | Sections present only in the previous snapshot. |
| `modified` | array | Sections whose normalized text hash changed. |
| `checkedAt` | string | ISO UTC time of this successful check. |
| `previousCheckedAt` | string or null | Last saved successful check time; null for baseline or legacy snapshot without time. |
| `stateKey` | string | Resolved baseline key. |
| `addedCount` | integer | Length of `added`. |
| `removedCount` | integer | Length of `removed`. |
| `modifiedCount` | integer | Length of `modified`. |

Each change entry contains `id`, `title` and a unified `diff`. For example, a migration guide changing its supported version could return:

```json
{
  "url": "https://docs.example.org/migration.html",
  "changeType": "changed",
  "checkedAt": "2026-10-18T00:00:00Z",
  "previousCheckedAt": "2026-10-17T00:00:00Z",
  "stateKey": "migration",
  "addedCount": 0,
  "removedCount": 0,
  "modifiedCount": 1,
  "added": [],
  "removed": [],
  "modified": [
    {
      "id": "requirements",
      "title": "Requirements",
      "diff": "--- previous\n+++ current\n@@ -1 +1 @@\n-Requirements Requires version 2.\n+Requirements Requires version 3.",
      "diffTruncated": false,
      "diffUnavailable": false
    }
  ]
}
```

Full normalized section text is hashed with SHA256. Each section stores up to 4,096 characters (also bounded to 6,000 ASCII-escaped JSON bytes) of normalized, privacy-filtered text. Diffs use context-local chunks of 12 words, with two chunks of surrounding context, capped at 2,048 UTF-8 bytes. `diffTruncated` flags section or diff limits, with `diffTruncatedReason` (`section_text_limit` or `diff_size_limit`). `diffUnavailable` and `diffUnavailableReason` identify legacy snapshots (`legacy_snapshot`) or edits outside retained text (`section_text_limit`); a legacy excerpt diff may still be shown, but is incomplete. Older snapshots load and upgrade on the next successful check; full-text hash change detection is unchanged. Section identifier changes can appear as removal and addition.

There are no dataset error rows. Failures emit no item, are uncharged, retain the last-good selected baseline and appear only in SUMMARY `failures`: each entry has `url`, stable `code`, and a short actionable `message`. Codes include `robots_denied`, `robots_unavailable`, `http_NNN` (for example `http_404`), `unsafe_destination`, `response_size_limit`, `no_content`, `section_limit`, `redirect_limit`, `crawl_delay_limit`, `invalid_url`, and fallback `fetch_failed`. Snapshot size rejection is an uncharged `snapshot_size_limit` failure; other pages continue.

The default key-value store's `SUMMARY` includes `checked`, `failed`, resolved `stateKey`, `requestedCount` (including duplicates), `distinctCount`, `selectedCount`, `notSelectedCount`, `notSelectedUrls`, `warnings`, `failures`, and `outcome`. Outcome is `complete` only when all distinct submitted URLs were successfully checkpointed, `partial` when some but not all were, and `no_results` when none were. Spending stops add `billingLimitReached: true`. Delivery/checkpoint errors can still fail the run; inspect run status as well as SUMMARY. Invalid inputs produce field-named validation errors and SUMMARY is written in finally (it may be empty for invalid inputs). Output links expose dataset items and SUMMARY.

#### Recurring checks recipe

Create an Apify Schedule for this Actor with the same URLs, filters, `maxPages` and `stateKey`. Ensure the cap selects every intended URL. Avoid overlapping runs on the same key. The first successful run creates a baseline. In your downstream dataset consumer, filter rows where `changeType == "changed"` and send your own alerts; baseline and unchanged rows are still charged checks. Read SUMMARY on every run: `partial` means incomplete coverage and `no_results` means no successful checks, not “nothing changed.” Inspect failures, not-selected URLs, billing flags and the run status before treating a run as healthy. This Actor does not configure schedules, filters or notifications for you.

### Pricing

Pay per event: one `page-checked` event per successfully checked page, including baseline and unchanged checks. No start fee. Failed fetches and extraction failures are not charged.

| Tier | Per 1,000 page checks | Per page |
|---|---|---|
| No discount | $5.00 | $0.005 |
| Bronze | $4.17 | $0.00417 |
| Silver | $3.33 | $0.00333 |
| Gold | $2.50 | $0.0025 |

Checking 20 pages daily for 30 days = 600 checks = **$3.00 at no-discount pricing**.

A per-run spending cap stops processing cleanly: capacity is checked before fetching and before pushing each item. Only successfully pushed items are charged. Storage and charging are not atomic: a charge failure after a push can leave an uncharged item without an updated baseline. Such failures are not automatically retried.

### Limits and what it does not do

- No link crawling, JavaScript rendering, login or semantic interpretation. Only submitted pages are checked.
- Extracts main-content regions (`main`, `role="main"` or class `body`), including Sphinx-style pages. A section starts at each h1–h4 heading. Its id is the heading `id`, else the id of the enclosing `section` or `div`, else a generated `heading-N`; headings that share an id form one section. On pages without such ids the whole page can be one section. Navigation, footers, scripts, styles and recognized update/source text are filtered; not all layout noise is removed.
- Maximum 500 pages per run and 1,000 extracted sections per page. The snapshot JSON stored under one `stateKey` (all pages that run selects) must not exceed 9,000,000 bytes; it is not silently truncated.
- Responses, including robots.txt, are capped at 2 MiB each. Requests are sequential with a minimum one-second delay, a 10-second socket timeout and at most five redirects.
- robots.txt is respected. Unavailable rules deny access; declared crawl delays above 10 seconds are refused. Redirect destinations are checked too.
- Private/internal and other non-public addresses are refused. Overlapping runs on one `stateKey` are not supported.

### Data and responsibility

You must have the right to monitor submitted pages and comply with source terms and applicable data-protection rules. Snapshots are stored in the named `docs-change-feed-baselines` key-value store in your own Apify account. Delete a key's record to remove that baseline, or delete the named store through Apify storage controls to remove all baselines. Dataset results are separate; delete retained runs/datasets separately when no longer needed.

Email-like text is redacted, and sections with author, contributor, credits or contact identifiers/titles are excluded. This reduces, but does not eliminate, personal data. Avoid submitting pages containing personal information.

The default Python documentation is licensed under the [Python Software Foundation License Version 2](https://docs.python.org/3/license.html). Retain applicable copyright and license notices when redistributing documentation excerpts. No affiliation or endorsement is implied.

### FAQ

**Will it identify breaking changes?** No. It reports text differences for you to assess.

**Why is my first result empty?** A baseline has no previous snapshot to compare against. Its change arrays are empty.

**Why did my page return only one section?** The page has no h1–h4 headings with distinct ids inside its main content, so the whole page is compared as one section. The default Python `colorsys` page is like this: its functions are not headings. Any edit marks that section `changed`; the diff shows bounded context around edits within retained section text. Check the explicit diff quality flags for legacy snapshots or limits.

**Can a page redesign create changes?** Yes. Section identifiers and main-content structure affect extraction and comparison.

**Does `/3/` stay on one Python version?** Use version-specific URLs if you want to monitor a fixed version rather than a moving documentation path.

### Support

Report reproducible problems through the Actor's Issues tab. Riprap Tools replies within 14 days.

# Actor input Schema

## `pageUrls` (type: `array`):

Public HTTP(S) pages you have permission to check; robots and SSRF checks always apply. Default host: docs.python.org. Only the first maxPages distinct URLs are checked; remaining URLs are reported in SUMMARY.

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

Maximum pages checked; each successful check is charged, including unchanged pages. Default 1: only one distinct URL is checked unless increased.

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

Optional 1–100 letters, digits, underscores or hyphens. Omit for a SHA256 of the sorted URLs. Reuse to compare future runs.

## `ignoreSelectors` (type: `array`):

Remove elements before sectioning. Only simple tag, .class or #id selectors; no combinators, attributes or pseudo-selectors. Changing filters may change hashes. At most 50 strings, each 1–200 characters.

## `ignoreText` (type: `array`):

Remove lines/fragments containing any literal substring before hashing. Case-sensitive, no regex. Changing filters may change hashes. At most 50 strings, each 1–200 characters.

## Actor input object example

```json
{
  "pageUrls": [
    "https://docs.python.org/3/library/colorsys.html"
  ],
  "maxPages": 1,
  "ignoreSelectors": [],
  "ignoreText": []
}
```

# Actor output Schema

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

Page checks with added, removed and modified sections

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

Run summary: pages checked, failures and limits

# 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 = {
    "pageUrls": [
        "https://docs.python.org/3/library/colorsys.html"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("riprap-tools/docs-change-feed").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 = { "pageUrls": ["https://docs.python.org/3/library/colorsys.html"] }

# Run the Actor and wait for it to finish
run = client.actor("riprap-tools/docs-change-feed").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 '{
  "pageUrls": [
    "https://docs.python.org/3/library/colorsys.html"
  ]
}' |
apify call riprap-tools/docs-change-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,riprap-tools/docs-change-feed"
        }
    }
}
```

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/UbU8FeblMrPL2ZQOg/builds/4tRywDlTauPeKZS5L/openapi.json
