# Website Text & DOM Change Monitor (`automation-lab/website-text-dom-change-monitor`) Actor

Compare public static webpages with supplied previous HTML. Export normalized text, text/DOM hashes, added and removed lines, changed element paths and reusable snapshots for recurring audits.

- **URL**: https://apify.com/automation-lab/website-text-dom-change-monitor.md
- **Developed by:** [Automation Lab](https://apify.com/automation-lab) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.48 / 1,000 page snapshots

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

## Website Text & DOM Change Monitor

Compare public static HTML pages with HTML snapshots you supply. Website text DOM change monitoring returns normalized body text, content and structure hashes, added/removed lines, and inserted/deleted/changed element paths. Use it for recurring wording reviews, compliance audits and website content pipelines—not screenshot comparisons or browser automation.

The first observation establishes a baseline. **You own cross-run storage:** save each returned `html`, then supply it as `previousHtml` next time. The Actor never silently retrieves another user's history or shares a global baseline.

### Who is it for?

- Compliance teams reviewing public terms and policy wording.
- Content operations comparing documentation and landing pages.
- Analysts auditing static competitor content with source-attributed changes.
- Developers building a snapshot archive in their own database.

### Why use this Actor?

A changed hash tells you something changed; line and element records show where. This Actor compares the whole accepted page, including wording beyond the first matching field. Attribute-only DOM changes can be detected even when body text stays the same.

Unlike the related [Website HTML & Text Change Monitor](https://apify.com/automation-lab/website-html-text-change-monitor), this workflow accepts an explicit previous HTML document and reports positional DOM element differences. Unlike the [Website Visual Change Monitor](https://apify.com/automation-lab/website-visual-change-monitor), it does not render or compare screenshots.

### Get started

1. Add a public static HTML URL in `pages`.
2. Run the Actor and open the default dataset.
3. Save the successful result's `html` and `observedAt` in your own storage.
4. On the next run, pass that HTML as `previousHtml` for the same URL.
5. Review `status`, `textChanges`, `domChanges` and both truncation flags.

```json
{
  "pages": [{ "url": "https://example.com" }],
  "maxDiffEntries": 200
}
```

### Input parameters

| Field | Meaning |
| --- | --- |
| `pages` | Required array of 1–20 unique URL objects. Sequential, no crawling. |
| `pages[].url` | Anonymous public HTTP(S) URL on standard ports; no credentials. |
| `pages[].previousHtml` | Optional prior HTML, at most 524288 UTF-8 bytes per page and 1048576 bytes combined per run. Omit for a baseline; `""` means an explicitly empty prior snapshot. |
| `maxDiffEntries` | Integer 1–500, default 20, separately applied to each diff array on every page. No unlimited/zero mode. |

Example comparison input:

```json
{
  "pages": [{
    "url": "https://example.com",
    "previousHtml": "<html><body><h1>Previous wording</h1></body></html>"
  }],
  "maxDiffEntries": 100
}
```

Previous HTML must belong to the same intended URL and observation policy. The Actor cannot verify the history you supply. It does not fetch Wayback snapshots or infer a previous version from a timestamp.

### Output fields

| Field | Meaning |
| --- | --- |
| `url`, `finalUrl` | Requested and final public URL. |
| `observedAt` | UTC timestamp for this observation. Keep your own prior timestamp alongside stored HTML. |
| `status` | `baseline`, `unchanged`, `changed`, or `error`. |
| `html` | Original current HTML for your next comparison. |
| `currentText` | Normalized body text with block-separated lines. |
| `textHash`, `domHash` | SHA-256 of normalized current text and element-path representation. |
| `previousTextHash`, `previousDomHash` | Prior hashes; null on baseline. |
| `textChanges` | `{type: "added"|"removed", text}` line records. |
| `domChanges` | `{type, path, previous, current}` records for inserted, deleted or changed elements. |
| `textTruncated`, `domTruncated` | True when the corresponding diff is incomplete. |
| `httpStatus`, `bytes` | Successful response status and fetched byte count. |
| `error` | Page error message; successful snapshot fields are absent on error rows. |

Baseline and unchanged results have empty change arrays. Every successful observation is a useful page result, including unchanged pages. Error rows are diagnostic and have no per-item event charge.

### Output example

A real first-observation excerpt from an example-domain control (HTML, hashes and transport fields omitted here for readability):

```json
{
  "url": "https://example.com",
  "status": "baseline",
  "currentText": "This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.",
  "textChanges": [],
  "domChanges": [],
  "textTruncated": false,
  "domTruncated": false
}
```

Use the prefilled Books to Scrape URL for the public catalogue demonstration. Source content can change; always inspect the current results.

### How comparison works

Scripts, styles, noscript and template contents are removed from both snapshots. Body whitespace is normalized; inline wording stays together and common block elements establish lines. This is text from static HTML, not browser-computed visibility: CSS-hidden text can remain. There is no OCR, JavaScript execution, locale translation or semantic rewriting.

DOM comparison includes normalized element paths, sorted attributes and direct text. Paths such as `/html[1]/body[1]/main[1]/p[2]` use same-tag sibling positions. They are **not stable node IDs or executable CSS selectors**. An insertion can shift subsequent paths and produce multiple changed records. Attribute values, including class and style, are significant; comments and attribute order are not. Head title wording can change the DOM hash without changing body text.

A full accepted snapshot is hashed even if output differences are truncated. After trimming equal leading/trailing lines, changed regions of at most 400 lines use a minimal line diff with a 100 ms computation cap. Larger regions use a bounded replacement hunk (all old lines removed, all new lines added), which can include unchanged interior lines rather than guaranteeing minimal edits. A computation timeout sets `textTruncated` true and details can be empty. A hash mismatch still reports `changed`.

### Limits and supported sources

- Maximum 20 submitted pages per run, no links followed.
- Maximum 524288 bytes for each current or previous HTML document; combined prior HTML is capped at 1048576 bytes per run. Split larger archives into smaller batches.
- Maximum 5000 normalized elements, 64 nesting levels and 5000 normalized text lines per snapshot.
- At most three HTTP redirects, with the same safety checks at each hop.
- Approximately 10-second page deadline; individual requests have at most 5 seconds and DNS lookup at most 2 seconds.
- One retry for transient network reset, temporary DNS failure, HTTP 429 or 5xx; no retries for login/challenge/404 responses.
- Only UTF-8/ASCII uncompressed HTML responses are supported; no files, compressed archives or arbitrary media.

Over-limit snapshots fail instead of pretending a partial hash represents the full page. There is no browser, login, custom proxy, cookie jar, screenshot, automatic alert, arbitrary dynamic-site guarantee or built-in historical archive. JavaScript-only/empty bodies fail; challenge pages and access restrictions must not be interpreted as authoritative business content. Validate source relevance downstream.

### Public URL safety

Private, loopback, link-local, reserved, multicast and private DNS destinations are blocked. Every DNS answer must be public; the connection uses a pinned validated address while retaining the requested TLS hostname. Redirects are revalidated before connecting. Only HTTP/HTTPS on standard ports is accepted, without URL credentials. There are no subresource requests.

This prevents using the Actor as an internal-network fetcher. Do not supply confidential pages or put credentials in URL queries. Your requested URLs and fetched public content remain in your run dataset as part of the product output.

### How much does it cost to compare public webpages?

Billing uses one `start` event per run and one `item` event per successful page snapshot. Baseline, changed and unchanged pages are charged equally; failed pages have no item event. Hashes, text and DOM details are included, not separately charged.

Start fee: **$0.00005 per run**. Successful page snapshot prices:

| Store spend tier | Price per page |
| --- | --- |
| FREE | $0.00092 |
| BRONZE | $0.00080 |
| SILVER | $0.000624 |
| GOLD | $0.00048 |
| PLATINUM | $0.00048 |
| DIAMOND | $0.00048 |

BRONZE examples including the start fee: one page $0.00085; five pages $0.00405; twenty pages $0.01605. At FREE tier, the estimated total for one successful observation including initialization is $0.00097. Spend tiers follow your qualifying aggregate monthly Apify Store spending, not a private per-Actor volume discount. Charges can stop collection at your configured spending limit; error rows or partial runs are not a promise of a complete batch. Estimates are not guaranteed invoices.

### Integrations and recurring workflows

Schedule runs with Apify and use your own orchestration to load previous HTML from a database into each new input. Save successful current snapshots after each observation. Do not replace your last good baseline with an error row.

Send change arrays to a spreadsheet or compliance review queue. Use an Apify webhook or your integration platform after completion if you want notifications; **the Actor itself sends no alerts**. Compare hash values before loading large HTML into downstream language models. Keep a retention policy for your own archive.

### API usage

Start once and retain the returned run ID:

```bash
curl -X POST \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  'https://api.apify.com/v2/acts/automation-lab~website-text-dom-change-monitor/runs' \
  -d '{"pages":[{"url":"https://example.com"}]}'
```

JavaScript with the official Apify client:

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/website-text-dom-change-monitor')
  .call({ pages: [{ url: 'https://example.com' }] });
const { items } = await client.dataset(run.defaultDatasetId).listItems({ limit: 20 });
```

Python:

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/website-text-dom-change-monitor').call(
    run_input={'pages': [{'url': 'https://example.com'}]})
items = client.dataset(run['defaultDatasetId']).list_items(limit=20).items
```

Check terminal status and pagination before treating an export as complete.

### Use through MCP

For Claude Code:

```bash
claude mcp add --transport http apify \
  'https://mcp.apify.com?tools=automation-lab/website-text-dom-change-monitor'
```

Claude Desktop, Cursor and VS Code configuration:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=automation-lab/website-text-dom-change-monitor"
    }
  }
}
```

Configure authorized authentication in your client. Discover actual tools and input schemas using scoped `tools/list`; selection also adds run/data/abort tools, not exactly one tool. Example prompt: “Fetch example.com, establish a baseline and return its hashes; do not invent a prior snapshot.”

For an authorized invocation, start once and retain the run/status/storage IDs. `call-actor` waits at most 45 seconds (default 30). If pending, poll `get-actor-run` for that same run with 2, 4, 8 seconds then capped 10-second backoff and a 120-second total deadline. Set client transport timeout above the server wait plus margin. A timeout is not permission to restart; recover the known run or report uncertainty. At deadline report pending with the run ID. Failed runs may have partial rows, not a successful full result.

After success, use `get-dataset-items` with explicit fields, limit 20 and offset 0. Example consumer budget: at most 100 source rows and 64 KiB serialized UTF-8 result content total, whichever comes first. Enforce bytes host-side before model injection; omit/summarize oversized HTML with disclosure. If the client cannot intercept oversized responses, do not promise a hard byte guarantee. Track source offsets, report returned count, continuation offset and truncation. Store full exports outside model context. These consumer bounds do not change Actor collection limits. No universal token-saving claim is made.

### Troubleshooting and FAQ

**Why is the first run a baseline?** No `previousHtml` was supplied. Save `html` and provide it next time. Scheduling alone does not create persistent history.

**Why is text unchanged but status changed?** Attributes, tags, structure or head title text changed. Inspect `domChanges`.

**Why did many paths change?** Positional paths shift when same-tag siblings are inserted/deleted. Review nearby paths together, not as stable identity events.

**Why did a URL fail?** Check HTTP status, HTML size, encoding, public-address restrictions and whether static content exists. This Actor cannot solve logins or browser challenges. Partial failures produce error rows; if every page fails, the run fails. Start charges can still apply.

**Can I send huge documents or request unlimited differences?** No. Reduce source scope outside the Actor or use another tool. The caps are strict and documented; truncation flags must be respected.

### Legality and responsible use

Only monitor pages you are authorized to access. Respect website terms, robots policies, rate limits, copyright and applicable privacy law. Sequential bounded fetches are not permission to bypass access controls. This Actor is a technical comparison tool, not legal advice or a compliance certification.

Failed operations send sanitized diagnostic input, exceptions and Actor/build/run IDs to our private GlitchTip service for repair. Secret fields and URL queries are removed; reports are retained for 30 days. Previous HTML is redacted from diagnostic reports. Do not put secrets in arbitrary HTML input fields. The runtime uses no AI, paid data provider or proxy. Apify hosts inputs, output datasets and logs under your account's retention settings; delete these through Apify when no longer needed. There is no Actor-global archive, cache or session store. Public targets receive ordinary HTTP requests. GlitchTip receives only scrubbed failure diagnostics, retained for 30 days. Store and delete your own snapshot archives according to your obligations.

For help, open an issue from this Actor's Apify Store page with a safe input and run link. Never include passwords or confidential snapshots.

### Related Actors

- [Website HTML & Text Change Monitor](https://apify.com/automation-lab/website-html-text-change-monitor): persistent snapshot-oriented monitoring workflow.
- [Website Visual Change Monitor](https://apify.com/automation-lab/website-visual-change-monitor): screenshot-oriented comparisons.
- [Public Webpage HTML Downloader](https://apify.com/automation-lab/public-webpage-html-downloader): HTML acquisition rather than caller-supplied structural diffs.

# Changelog

This Actor's version history is a separate document: https://apify.com/automation-lab/website-text-dom-change-monitor/changelog.md

# Actor input Schema

## `pages` (type: `array`):

1–20 unique URL objects: {"url":"https://example.com","previousHtml":"<html>...</html>"}. previousHtml is optional; an empty string means an explicitly empty prior page. Public HTTP/HTTPS standard ports only, no credentials, private networks or private redirects. Combined previousHtml is capped at 1048576 UTF-8 bytes per run. Each fetched/prior HTML is limited to 524288 UTF-8 bytes, 5000 elements, 64 nesting levels and 5000 text lines; over-limit pages produce errors. No browser, login or automatic history fetch.

## `maxDiffEntries` (type: `integer`):

1–500 entries per page in each of textChanges and domChanges; default 20. Not a page or character limit. Overflow flags textTruncated/domTruncated; hashes still cover the full accepted snapshot. Text diff also has a 100 ms computation cap. Zero/unlimited is not supported.

## Actor input object example

```json
{
  "pages": [
    {
      "url": "https://books.toscrape.com/"
    }
  ],
  "maxDiffEntries": 20
}
```

# Actor output Schema

## `dataset` (type: `string`):

Download current text, HTML, hashes and bounded differences.

# 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 = {
    "pages": [
        {
            "url": "https://books.toscrape.com/"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/website-text-dom-change-monitor").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 = { "pages": [{ "url": "https://books.toscrape.com/" }] }

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/website-text-dom-change-monitor").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 '{
  "pages": [
    {
      "url": "https://books.toscrape.com/"
    }
  ]
}' |
apify call automation-lab/website-text-dom-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/website-text-dom-change-monitor"
        }
    }
}
```

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/RpTfuZkDUL3cjkz0I/builds/uq7l4CTJ8ActpPbSp/openapi.json
