Regulatory Page Change Monitor – Diffs & Alerts avatar

Regulatory Page Change Monitor – Diffs & Alerts

Pricing

from $8.50 / 1,000 change detecteds

Go to Apify Store
Regulatory Page Change Monitor – Diffs & Alerts

Regulatory Page Change Monitor – Diffs & Alerts

Regulatory page change monitor for legal, policy and government pages: get only what was added, changed or removed, with a readable line-level diff, webhook alerts and audit-ready records. Filters cookie banners, ads, timestamps and counters; works for price and competitor pages too.

Pricing

from $8.50 / 1,000 change detecteds

Rating

0.0

(0)

Developer

Leyten Data

Leyten Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

25 days ago

Last modified

Security

Limited permissions

Share

What does Regulatory Page Change Monitor do?

Regulatory Page Change Monitor is a page change monitor for legal, policy and government pages: it tracks any web page on a schedule and returns only what is new, changed or removed, with a readable line-level diff, a webhook alert and a dataset record you can keep as the audit trail of what the page said on which day. Terms of service, privacy policies, regulator guidance, tariff pages, tender portals and official publications are its home turf; it works just as well for price change detection and competitor pages. It ignores cookie banners, ads, timestamps, counters and session tokens, so you only hear about real changes.

Three ways to monitor a page

Full page text. Any change to the visible text of a page is reported, with a line-by-line diff. Good for terms of service, regulator guidance, documentation and competitor landing pages. Combine it with crawl depth to monitor a whole section: pages that are added to or disappear from the section are reported too.

Specific elements with CSS or XPath selectors. Monitor only what matters, like a version number, an effective date, a price or a stock label. Selectors can be CSS (.product-price) or XPath (//span[@itemprop="price"]), and you can monitor an attribute such as a link's href instead of the text.

Lists and tables: every item tracked separately. Point the monitor at a list or table (publications table, product grid, search results) and every item is tracked on its own. You get "Circular 2026/14 added", "Steel kettle price changed from € 19,99 to € 17,99" and "Glass kettle removed" instead of one big blob of text. Items are identified by their link (tracking parameters removed), by a key field you choose, or by their text.

Noise suppression: why this page change monitor stays quiet

Most pages change on every load without any real change: a new session token, a rotating banner, "updated 3 minutes ago", a view counter. A monitor that reports those trains you to ignore it. These filters are applied before anything is compared:

FilterDefaultWhat it ignores
cookie-bannersonRemoves consent managers (OneTrust, Cookiebot, Usercentrics, Didomi, Quantcast, iubenda, …) and dialogs whose id or class mentions cookies or consent.
adsonRemoves ad slots (Google Ads/GPT, AdSense), sponsored widgets (Taboola, Outbrain) and elements marked as advertisements.
hidden-elementsonRemoves elements that are not visible: hidden, aria-hidden="true", inline display:none / visibility:hidden, and closed dialogs.
timestampsonMasks times of day (14:05, 2:05:33 PM, 14.05 uur) and ISO timestamps (2026-09-14T08:00:00Z). Plain dates are kept; see dates.
relative-timesonMasks phrases like "5 minutes ago", "2 uur geleden", "vor 3 Tagen", "il y a 2 jours" and "just now".
countersonMasks numbers next to words like views, likes, shares, followers, comments, reviews, weergaven, volgers, reacties, and "12 people are viewing this".
tokensonMasks UUIDs and long random-looking strings (32+ characters mixing letters and digits, long hex). Form fields, meta tags and scripts where CSRF tokens usually live are never part of the extracted text.
tracking-paramsonStrips utm_*, fbclid, gclid, msclkid, session ids (sid, PHPSESSID, jsessionid) and CSRF/cache-busting parameters from page URLs and links.
unordered-carouselsonSorts the slides of carousels, sliders and marquees by their text, so a different start slide or shuffled order is not a change.
datesoffMasks dates like 2026-09-14, 14-09-2026, 14 sep 2026 and Sep 14, 2026. Off by default because a changed deadline or publication date is usually a real change.
navigationoffRemoves nav, header, footer and [role=navigation]. Useful for full-page monitoring when menus change often.
live-regionsoffRemoves [aria-live], [role=timer], [role=marquee] and marquee elements, such as countdowns and stock tickers.

Scripts, styles, form fields and meta tags (where CSRF tokens usually live) are never part of the monitored text. You can switch any filter off, turn on the optional ones, and add your own rules:

  • Ignore elements: CSS selectors such as .stock-counter or #recently-viewed
  • Ignore text patterns: regular expressions such as Order #\d+
  • Minimum changed characters: only report changes above a size threshold; smaller edits accumulate until they pass it

Changing what is extracted (mode, selectors, filters, crawl settings) starts a fresh baseline for the affected URLs instead of reporting a wave of changes.

Reliability you can build on

  • Baseline first: the first run for a URL records its state as baseline records (free, never a change, no webhook); changes are reported from the second run. Turn off reportBaseline if you only want real changes in your dataset.
  • Redesigns are not deletions: when a selector matches nothing, a list is empty or a field is empty in every item, the run fails with a message naming the URL and selector. Nothing is charged for changes and the previous state is kept, so fixing the selector continues where you left off. Set onStructureChange to skipUrl to skip only the broken URL.
  • Removals only from complete checks: if a page failed to load or a crawl limit was reached, removals are not reported for that URL in that run.
  • Nothing lost at your limits: changes above maxChangesPerRun or your spending limit are reported in the next run. A run stops starting new pages at maxPagesPerRun, at your maximum cost per run and close to its timeout, and delivers what it found instead of timing out. URLs to monitor that it did not reach, or whose page it could not load and read in the time left, are left as they were, never reported as removed or charged, and counted as not processed in the run's status message. A first check that the timeout cuts short is not saved at all (see Limitations), so the pages it missed are never reported as added later. Later runs start with the URLs checked longest ago. The time of a check is saved at once when a run reports a change or records a new baseline; otherwise it can take a day, so runs more often than daily may check the same URLs again until then. The default run timeout is 22 hours, enough for the largest run the input allows with one record per page: 100,000 pages at 2 per second to one site, and their delivery. A first check of list pages delivers a record per list item and can need longer. You can set another timeout in the run options.
  • Charged once: every change and every checked page is charged at most once, even if the platform restarts the run.

Features

  • Tracks website changes on a schedule and remembers what each page looked like last time
  • Returns only the differences: added, changed and removed pages, sections or list items
  • Line-level diffs two ways: structured (field path, old value, new value) and as a readable unified diff
  • Audit-ready records: every change carries the content hash, the previous hash, the detection time and the current content
  • Noise suppression out of the box: cookie banners, ads, clock times, "5 minutes ago", view counters, CSRF and session tokens, tracking parameters, shuffled carousels
  • Baseline on the first run is never charged as changes, so you can check your selectors cheaply
  • Never reports a redesign as "everything removed": a selector that stops matching fails the run with a clear message
  • Webhook alerts with a summary and a dataset link when something changes
  • Plain HTML by default (fast and cheap), with optional JavaScript rendering
  • Runs through the Apify API, on a schedule, or from Make, Zapier and n8n

Use cases

Compliance teams: track regulatory and policy pages

Follow the pages that change your obligations: a regulator's guidance page, a tax authority's rate table, a supplier's terms of service, a data protection authority's FAQ. Full-text mode with crawl depth 1 also catches newly linked documents, and every change lands in the dataset with the old and new text.

{
"startUrls": [{ "url": "https://www.regulator.example.gov/guidance/reporting" }],
"monitorMode": "fullText",
"crawlDepth": 1,
"includeUrlPatterns": ["https://www.regulator.example.gov/guidance/**"],
"webhookUrl": "https://hooks.example.com/regulatory-changes"
}

Get notified when a ministry, regulator or municipality publishes a new document or corrects an existing one. Each row of the publications table is tracked separately and keyed by its link.

{
"startUrls": [{ "url": "https://www.voorbeeldzaken.example.nl/publicaties" }],
"monitorMode": "list",
"listSelector": "table.publications tbody tr",
"listFields": [
{ "name": "title", "selector": "td.title" },
{ "name": "type", "selector": "td.type" },
{ "name": "date", "selector": "td.date" }
],
"webhookUrl": "https://hooks.example.com/publications"
}

E-commerce: price change detection

Track prices, discounts and stock on a competitor's category page. Each product is monitored separately, so you see exactly which prices moved.

{
"startUrls": [{ "url": "https://shop.example.com/c/electric-kettles" }],
"monitorMode": "list",
"listSelector": ".product-card",
"listFields": [
{ "name": "title", "selector": ".product-title" },
{ "name": "price", "selector": ".price" },
{ "name": "stock", "selector": ".stock", "optional": true }
],
"webhookUrl": "https://hooks.example.com/price-changes"
}

Product marketing: competitor pages

Follow a competitor's pricing and plan pages to catch new plans, price increases and feature changes the day they go live. You can run it daily or hourly; runs without changes cost very little.

How to use it

  1. Put the pages you care about into URLs to monitor and press Start. The first run records a baseline for each page and delivers it as a free baseline record, so you can confirm you are watching the right thing before anything is charged.
  2. Narrow what counts as a change. Leave What to monitor on full text for a policy page; use Elements to monitor with a CSS selector when only one part matters, or List item selector with Item fields when the page is a register or a table and you want a record per row rather than per page. Ignore elements and Noise filters remove the cookie banners, counters and timestamps that would otherwise wake you up every night.
  3. Give it a State key, schedule it, and point Webhook URL at your own endpoint. From the second run onward you only receive what was added, changed or removed, with a line level diff you can read without opening the page.

The changes view is the diff feed and fieldChanges breaks a list page down to the individual field that moved. Every run ends with a count of added, changed, removed and unchanged, so an uneventful day is visibly uneventful. Turn on Render JavaScript only when a page truly needs it: it needs more memory and makes runs slower and more expensive.

Two honest limits. A page behind a login or a bot wall cannot be read, and if the site changes its structure the selectors you configured stop matching, which is what When a page structure changes decides how to handle. Set it deliberately rather than leaving it to chance.

{
"startUrls": [{ "url": "https://eur-lex.europa.eu/homepage.html" }],
"minChangedCharacters": 40,
"stateKey": "eu-legal-watch"
}

Input

FieldWhat it doesExample
startUrlsPages to watch; with a crawl depth above 0, the start pages of the crawl.[{ "url": "https://en.wikipedia.org/wiki/Portal:Current_events" }]
monitorModefullText (any visible text change), selectors (specific elements) or list (every item tracked separately)."fullText"
selectorsFor selectors mode: CSS selectors or XPath, optionally with an attribute to monitor.[{ "selector": ".product-price" }]
listSelectorFor list mode: the selector that matches one item..product-card
listFieldsFields inside each item; without fields, the whole item text is monitored.[{ "name": "price", "selector": ".price" }]
listKeyFieldThe item field that identifies an item; by default the first link in the item, or its text.sku
noiseFiltersWhat never counts as a change (see the table above).["cookie-banners", "ads", "timestamps"]
ignoreSelectorsCSS selectors of extra elements to ignore.[".stock-counter"]
ignoreTextPatternsRegular expressions for text to ignore.["Order #\\d+"]
minChangedCharactersOnly report a change of at least this many characters; smaller edits accumulate.0
crawlDepth0 monitors only the start URLs; 1 also the pages they link to, and so on.0
maxPagesPerStartUrlCrawl limit per start URL.50
includeUrlPatternsGlob patterns of links to follow.["https://example.com/blog/**"]
excludeUrlPatternsGlob patterns of links never to follow.["**/tag/**"]
renderJavaScriptLoad pages in a headless browser (slower, 2 GB of memory by default).false
waitForSelectorWith JavaScript rendering: wait until this selector exists, for at most requestTimeoutSecs; a selector that does not appear counts as a structure change. Close to the run's timeout the wait gets only the time that is left, at least a second, and a page whose selector did not appear in that time is not checked..price
waitForMillisWith JavaScript rendering: extra time to wait after the page loaded.0
reportBaselineDeliver one free baseline record per monitored page or item on the first check.true
webhookUrlReceives a JSON summary whenever a run finds changes; stored encrypted, like a password.https://hooks.example.com/changes
maxChangesPerRunHard cap on reported (and charged) changes in one run; the rest follows next run.500
maxPagesPerRunHard cap on checked (and charged) pages in one run; later runs start with the URLs to monitor checked longest ago (see Nothing lost at your limits).1000
onStructureChangefailRun or skipUrl when a selector no longer matches."failRun"
stateKeySeparates independent monitors. Defaults to the task ID.regulator-guidance
maxConcurrencyPages loaded in parallel. Without JavaScript rendering, requests to the same host are also limited to 2 per second on average, in bursts of up to this many.5
requestTimeoutSecsTimeout for one attempt to load a page; a page that times out is tried again. Close to the run's timeout an attempt gets only the time that is left, at least 5 seconds, and a page is tried again as long as that much is left.30
proxyConfigurationOnly for sites that block data-centre traffic.{ "useApifyProxy": false }

To track website changes from your own code, save the input as a task, run it once for the baseline, add a schedule, and read the results in one call:

$curl -X POST "https://api.apify.com/v2/actor-tasks/YOUR_TASK_ID/run-sync-get-dataset-items?token=YOUR_API_TOKEN"

Output

Every change is one dataset item. All our monitoring Actors use this same format:

FieldMeaning
idStable identity of the page or list item
scopeThe start URL the item belongs to
changeTypeadded, changed, removed, or baseline for the first check of an item (free)
sourceUrlPage where the item was found
scrapedAt / firstSeenAtWhen the change was detected / when the item was first seen
hash / previousHashFingerprints of the new and previous content
changesStructured changes: path, change, oldValue, newValue, and line for text
summaryOne readable line, ready for a notification
diffTextReadable diff (unified diff for long text)
payloadCurrent content (for removed: the last known content)

When webhookUrl is set, each run that finds changes POSTs a summary with counts, one line per change and the datasetUrl. A summary of every run (pages checked, changes, estimated cost) is saved as RUN_SUMMARY in the run's key-value store and shown as the run's status message. The history itself lives in a named key-value store called regulatory-page-change-monitor-state in your own Apify account: the last known content of each monitored page or item, kept until you delete it; URLs you no longer monitor are cleaned up after 90 days.

{
"envelopeVersion": 1,
"id": "https://shop.example.com/c/electric-kettles#https://shop.example.com/p/steel-kettle-17",
"scope": "https://shop.example.com/c/electric-kettles",
"changeType": "changed",
"sourceUrl": "https://shop.example.com/c/electric-kettles",
"scrapedAt": "2026-09-16T08:00:00.000Z",
"firstSeenAt": "2026-09-14T08:00:00.000Z",
"hash": "39186ce17ce5e68219abe266e8354a8d4bb6d0758ef1ce3e02e0a3236e31cdb0",
"previousHash": "bf9d404fa36d9030c6d1ed4b7f80e4f479e600180f5f619e94e8957c179bccc8",
"changes": [
{
"path": "fields.price",
"change": "modified",
"oldValue": "€ 19,99",
"newValue": "€ 17,99"
}
],
"summary": "Steel kettle 1.7 L: fields.price \"€ 19,99\" → \"€ 17,99\"",
"diffText": "~ fields.price: \"€ 19,99\" → \"€ 17,99\"",
"payload": {
"kind": "row",
"key": "https://shop.example.com/p/steel-kettle-17",
"link": "https://shop.example.com/p/steel-kettle-17",
"fields": {
"title": "Steel kettle 1.7 L",
"price": "€ 17,99",
"stock": "In stock"
}
}
}

Pricing

Pay-per-event pricing: a run without changes is almost free, so checking every day stays cheap.

EventNameWhat it meansPrice
apify-actor-startActor startCharged by Apify when a run starts: once for a run of up to 1 GB of memory, and once more for every extra GB. It covers the first five seconds of compute.$0.00005 per run up to 1 GB of memory ($0.05 per 1,000)
page-checkedPage checkedA page was loaded successfully and compared with the previous run. Pages that fail to load are not charged.$0.0005 per page ($0.50 per 1,000)
change-detected (primary)Change detectedA page, section or list item was added, changed or removed since the previous run. Baseline runs never charge this.$0.01 per change ($10.00 per 1,000)

These are list prices, which the Free plan pays. Paid Apify plans pay less for every event except the Actor start: 5% on Apify's Bronze tier, 10% on Silver and 15% on Gold and higher.

Example: monitoring 20 pages every day with no changes costs 20 × $0.0005 + $0.00005 ≈ $0.01 per day (about $0.30 per month). If 10 changes are found that month, that adds $0.10. JavaScript rendering uses 2 GB of memory by default, which counts as 2 start events per run.

Worst case per run: the default limits stop a run at 1,000 checked pages (maxPagesPerRun) and 500 changes (maxChangesPerRun). At list price that is 1,000 × $0.0005 = $0.50, plus 500 × $0.01 = $5.00, plus one Actor start at the default 512 MB of memory ($0.00005), so at most $5.50005 per run. With Render JavaScript on, the default memory is 2 GB and the start is charged twice, so the ceiling is $5.5001. A first run charges no changes, so it costs at most $0.50005 ($0.5001 with Render JavaScript). Lower the two limits to lower the ceiling.

Tip: to cap what one run can cost, set Max cost per run in the run options in Apify Console: the Actor then checks no page and reports no change that would take the run over that amount, and reports the changes that did not fit in the next run.

You are never charged for: change events on the first run for a URL (the baseline), pages that failed to load, the pages of a first check that the run's timeout cut short, the pages and changes of a run that stopped because a site's structure changed (Apify's start event is still charged, as for every run), or anything above your limits (maxChangesPerRun, maxPagesPerRun and your run's maximum cost).

FAQ

How is a page change monitor different from a scraper, and what do I get on the first run?

A scraper returns everything on every run. This monitor keeps the previous state and returns only what changed, with old and new values, so you can alert on it or feed it into a pipeline directly. The first run delivers one baseline record per monitored page or list item: the reference that later runs are compared with, free, never a change and never a webhook. Baseline records have no event of their own: a first run is priced through page-checked, once per page loaded. Use it to verify that your selectors pick up the right content.

What happens when the website is redesigned?

If your selectors stop matching, the run fails with a clear message such as list item selector ".product-card" matched no items. It never reports every item as removed, and it charges for no change. Update the selectors and the next run continues from your previous state.

Can I monitor pages that need JavaScript, and how do I get notified?

Yes. Turn on Render JavaScript to load pages in a headless browser, optionally waiting for a selector; leave it off for normal pages, where plain HTML is faster and cheaper. For notifications, set a webhook URL to receive a JSON summary, or use Apify integrations to send the dataset to Make, Zapier or n8n after each run. Create one task per monitor; each task keeps its own history, and runs started directly through the API can use a different stateKey per monitor.

Monitoring publicly available pages is generally allowed, but you are responsible for respecting each site's terms and applicable law. Avoid monitoring personal data; if you do, you need a legal basis under the GDPR. This Actor does not log in or bypass access controls, sends a Chrome browser user agent rather than identifying itself as a bot, and stores the monitored content only in your own Apify account. Without JavaScript rendering it also limits requests to the same host to 2 per second on average.

What are the limitations?

  • Pages behind a login are not supported.
  • PDF and other non-HTML documents are not compared; in list mode, new document links are still detected.
  • The default noise filters can hide a real change. timestamps masks anything written like a clock time, so a deadline moved from 17:00 to 12:00, or an article reference such as 3:40 changed to 3:41, is not reported. counters masks a number in front of words such as votes, comments, downloads or reads, so "412 votes in favour" becoming "415 votes in favour" is not reported. Leave timestamps and counters out of noiseFilters on pages where such numbers matter.
  • In crawl mode, a page that stops loading (for example with a 404) is not reported as removed; only pages that are no longer linked are.
  • A page in URLs to monitor that stops loading (for example it now returns 404 or 410, or times out) is not reported as removed, and no webhook reports it. The run's status message counts it as failed, and the run itself fails only when every URL to monitor that it checked failed. A URL it left out near its timeout, including one whose first check it dropped, does not count as checked, so a run in which all the other URLs failed still fails, with their errors; its log says how many URLs were left out. When none could be read because of a temporary error, such as a timeout, a refused or dropped connection, or HTTP 429, 500, 502, 503 or 504, the run keeps trying the first of them for up to 10 minutes, once per run and never past its timeout; if it loads, the others are tried once more. If a page being taken down matters to you, watch the run's status message as well as the webhook.
  • When the first check of a URL stops early at maxPagesPerRun or at your maximum cost per run, the pages it did not reach are reported (and charged) as added when a later run reaches them. A first check that the run's timeout cuts short is dropped instead: nothing of it is saved, delivered or charged, and later runs start with that URL, as with every URL not checked yet. A first check that never fits in the timeout, such as a large crawl with a short timeout, is therefore never recorded, and every run spends its time on it for nothing: raise the timeout in the run options.
  • Only public web addresses are loaded: localhost, private-network and cloud metadata addresses are refused, also after a redirect and for anything a rendered page requests, and links to them are not followed.
  • Pages larger than 5 MB, with elements nested more than 2,000 levels deep, with an element of more than 1,000 attributes, or with markup too malformed to parse in reasonable time are treated as pages that failed to load (and are not charged), as is a rendered page that keeps navigating to other addresses (more than 10 hosts or 50 URLs).
  • XPath selectors are much slower than CSS selectors on large pages and need far more memory. A page whose XPath extraction takes longer than 30 seconds, or whose memory need is estimated at more than half of the run's memory, also counts as a page that failed to load. The estimate is made before extraction from the page's elements, text, comments and attributes, and errs on the safe side. As a guide, XPath handles typical pages of up to about 500 KB of HTML with 256 MB of run memory, 1.2 MB with 512 MB (the default without JavaScript rendering), 2.8 MB with 1 GB, and up to the 5 MB page limit with 2 GB; pages with dense markup need more. For larger pages, use CSS selectors or more memory. Close to the run's timeout an extraction gets only the time that is left, at least a second, and a page whose extraction did not finish in that time is not checked rather than failed.
  • The selectors of one page may read at most 5 MB of text and links (5,242,880 characters) and 20 million elements in total. A selector that matches elements nested inside each other, such as div on a page of nested div elements, reads the inner elements again for each one around them; a page where that goes over the limits counts as a page that failed to load. With XPath, text beyond the page's own size is further limited by the memory left above its estimate. Sorting carousels that are nested inside each other has the same limit on elements.
  • JavaScript rendering works with HTTP proxies; SOCKS proxies are not supported.

Support

See the changelog tab for release notes. Found a page that is reported as changed while nothing changed? Open an issue with the URL. Noise reports are our top priority.