Website Change Monitor: Diff, Price & Stock, $3/1K
Pricing
from $2.10 / 1,000 url checks
Website Change Monitor: Diff, Price & Stock, $3/1K
Website change monitor for public pages. Each URL is one check that returns a text diff, the changed CSS selector, price and stock status. Respects robots.txt. $0.003 per URL. Schedule it for alerts.
Pricing
from $2.10 / 1,000 url checks
Rating
0.0
(0)
Developer
Don Mangu
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Website Change Monitor checks the public web pages, RSS or Atom feeds and JSON API endpoints you list and tells you what changed since the last check. Each URL is one check at $0.003 ($3 per 1,000 checks) and returns one result row with a text diff, the change inside a CSS selector, the price and the stock status. The Actor reads robots.txt for every host, keeps one baseline per URL in your own Apify account, and works with Apify schedules, webhooks and the API, so a change can start an alert in your own tools. For feeds and JSON endpoints it returns only the new and changed items, and it can send them to a webhook URL.
Website change monitor sample output
The examples use public GOV.UK pages. This Actor is not affiliated with GOV.UK or with any site you monitor. This row comes from the saved task "Track Price Changes on Any Web Page". It reads the first fee in the GOV.UK passport fees table on the first run, which stores the baseline:
{"url": "https://www.gov.uk/passport-fees","status": "baseline","changed": false,"changeTypes": [],"summary": "First check: baseline stored, nothing to compare yet","httpStatus": 200,"title": "Passport fees - GOV.UK","watching": "price-stock","price": 102,"priceCents": 10200,"currency": "GBP","priceText": "£102","priceSource": "selector","priceTargetMet": false,"availability": null,"checkCount": 1,"error": null}
How to monitor website changes in 5 steps
- Open the Actor and add the pages to watch under Page URLs. Each URL is one check.
- Narrow the check if you want. Enter a CSS selector to watch one part of a page, a Price selector and a Stock selector for products, or a Target price to get a yes or no when the price falls to it. Product pages with schema.org JSON-LD or price meta tags are read without any selector.
- Click Start. The first run stores a baseline for every URL and returns the status
baseline. - Open Schedules and run the Actor daily or hourly with the same input. Every later run compares each URL with its baseline and returns
changedorunchanged. - Read the results in the Check results, Text diffs and Price and stock tables, or through the API. The key-value store record
SUMMARYlists the URLs that changed in the run.
To find a CSS selector, right-click the element in your browser, choose Inspect, right-click the highlighted tag and choose Copy selector.
What the website change monitor returns
One row per URL. These are the main fields:
| Field | Meaning |
|---|---|
status | baseline, changed, unchanged, unavailable, robots-disallowed, blocked, error, invalid-url or skipped-host-blocked. A webhook status row uses webhook-sent, webhook-failed or webhook-refused |
sourceType, items, newCount, changedCount, itemsSeen | Feed and JSON sources: the type, the new and changed items, their counts and the number of items in the response |
changed, changeTypes, summary | Whether something changed, what kind (text, selector, file, price, availability, page-removed, page-restored, and for feeds and JSON new-items, changed-items, document) and one sentence about it |
diff, diffText | The changed lines as replace, add and remove entries, and the same as text with - and + lines |
addedLineCount, removedLineCount | Size of the text change |
price, priceCents, currency, previousPrice, priceChangePercent | Current price, price in hundredths of the currency unit, and the change since the last check |
priceTargetMet | True when the price is at or below your target price |
availability, previousAvailability | in_stock, out_of_stock, preorder, backorder or unknown |
checkedAt, previousCheckedAt, checkCount | When this URL was checked now and before |
notes, error | Hints such as a selector that matched nothing, and the reason when a check could not finish |
If a page returns 404 or 410 after a successful check, the row has changed true and the change type page-removed. When the page comes back, the change type is page-restored. PDFs and other files are compared by a hash of their bytes.
Price and stock alert with the website change monitor
Give the Actor a product page and it looks for the price in this order: your Price selector, schema.org JSON-LD offers, then price meta tags such as product:price:amount. Stock comes from your Stock selector, the JSON-LD availability value or the availability meta tag. Prices such as £94.50, 1.299,00 € and $1,299.00 are read into a number, a currency and cents.
When a price or stock selector is set and no CSS selector is set, only the price and stock fields are compared, so banners and counters on the page do not create changes. To get an alert, schedule the Actor, add an Apify webhook to the run and act on the rows where changed is true, or where priceTargetMet is true.
Monitor an RSS feed or a JSON API
Set Source type to choose what the URLs are. The default is page, which works as described above. All URLs of one run use the same type.
| Source type | What it compares | What a changed row holds |
|---|---|---|
page | Page text, a selector, price and stock | diff, diffText, price and stock fields |
feed | Items of an RSS 2.0, RSS 1.0 or Atom feed. An item is new when its guid (or Atom id, or link) was not seen before, and changed when its title, link, text or date changed | items: only the new and changed items |
json with ID field | Records of a JSON response. A record is new when its ID was not seen before, and changed when any field of the record changed | items: only the new and changed records |
json without ID field | The whole JSON document. Key order is ignored | diff and diffText of the pretty printed document |
The first run stores a baseline and returns no items. Every later run returns only what is new or changed since the last run. Items that leave the feed or the response are not reported. The Actor remembers up to 5,000 item IDs per source, so an item that comes back is not reported as new again.
Feed example. The input below uses the Federal Register RSS feed. The first run on 13 October 2026 returned one row with the status baseline and the summary "First check: baseline stored (200 items), nothing to compare yet". The second run returned unchanged with "No new or changed items (200 items checked)".
{"startUrls": [{ "url": "https://www.federalregister.gov/api/v1/documents.rss?conditions%5Btype%5D%5B%5D=RULE" }],"sourceType": "feed","monitorName": "federal-register-rules"}
JSON example. The openFDA drug enforcement endpoint lists recalls. Each recall has a recall_number:
{"startUrls": [{ "url": "https://api.fda.gov/drug/enforcement.json?limit=5&sort=report_date:desc" }],"sourceType": "json","idField": "recall_number","itemsPath": "results","monitorName": "fda-recalls"}
On 13 October 2026 the first run stored 4 records ("First check: baseline stored (4 items)"). The endpoint returned 5 records, and one had an empty recall_number, so it was skipped and the row notes say so. The second run returned unchanged. itemsPath is optional. Without it the Actor takes a top level list, or a field named results, items, data or records.
Changed row. This row comes from a test run against a small local test server that serves a feed. The feed grew from 3 to 5 items and the first item was edited:
{"status": "changed","changed": true,"changeTypes": ["new-items", "changed-items"],"summary": "2 new items, 1 changed item of 5 in the feed.","newCount": 2,"changedCount": 1,"itemsSeen": 5,"items": [{ "change": "changed", "id": "g1", "title": "Post 1 (edited)", "link": "http://x.test/1", "published": "Mon, 01 Oct 2026 10:00:00 GMT", "summary": "Body 1" },{ "change": "new", "id": "g4", "title": "Post 4", "link": "http://x.test/4", "published": "Mon, 04 Oct 2026 10:00:00 GMT", "summary": "Body 4" },{ "change": "new", "id": "g5", "title": "Post 5", "link": "http://x.test/5", "published": "Mon, 05 Oct 2026 10:00:00 GMT", "summary": "Body 5" }]}
For json the entries in items hold the full record instead of title and link. Maximum diff lines limits how many items one row lists, and itemsTruncated is true when more changed. newCount and changedCount always show the full numbers.
Send changes to a webhook
Enter a Webhook URL and the Actor sends one POST with a JSON body at the end of the run. The body holds only the sources that changed. When nothing changed, nothing is sent.
{"monitorName": "fda-recalls","runId": "abc123","finishedAt": "2026-10-13T17:25:54.296Z","changedSources": 1,"truncated": false,"changes": [{"url": "https://api.fda.gov/drug/enforcement.json?limit=5&sort=report_date:desc","sourceType": "json","summary": "2 new items, 1 changed item of 4 in the response.","changeTypes": ["new-items", "changed-items"],"newCount": 2,"changedCount": 1,"itemsTruncated": false,"items": [{ "change": "new", "id": "R-3", "record": { "rid": "R-3", "status": "Ongoing", "name": "Rec 3" } }]}]}
The example body shows the structure. The items entry was captured from a local test run, and the other values are shortened. A page source sends diffText, and the price and stock fields when they changed, in place of items.
- Only
httpsURLs are accepted. URLs with a user name or password, and URLs that point to private or internal addresses, are refused. The address is checked again when the connection opens. - Redirects are not followed. A
2xxanswer counts as delivered. After a network error or a5xxanswer the Actor tries once more after two seconds. - A bad or failing webhook does not fail the run. The results get one extra row with the status
webhook-sent,webhook-failedorwebhook-refused, the reason, and the host of the URL. The path of the URL is not stored, because it can hold a secret. The same information is inSUMMARYunderwebhook. - The body is cut to about 1 MB. Then
truncatedis true.
Website change monitor pricing
The Actor uses pay per event. One event is charged for each URL after its result row is saved:
| Event | Price (no discount) | Bronze | Silver | Gold |
|---|---|---|---|---|
url-checked | $0.003 | $0.0027 | $0.0024 | $0.0021 |
| Actor start (every run) | $0.00005 |
That is $3.00 per 1,000 checks without a discount and $2.10 per 1,000 at Gold.
- 1 URL equals 1 charged check. The first check, an unchanged page, a changed page and a page that answers 404, 403 or an error are all one check.
- A URL that robots.txt does not allow is charged as one check, because the Actor requested robots.txt from that host. No page request is sent for it.
- Feeds and JSON endpoints use the same
url-checkedevent, charged once per source. The first check and a check that finds new or changed items are charged. A check with no change is free and returns a status row. A feed or JSON check that ends in an error, a block or a robots.txt refusal is also free. A webhook is not charged. - URLs that are not valid web addresses, URLs that point to private network addresses and URLs above your Maximum URLs limit are not charged. After a host refuses three requests in a run, its remaining URLs are skipped and not charged.
- The Actor stops when the maximum charge you set for the run is reached.
Worked example. You watch 50 product pages once a day for 30 days. That is 30 runs with 50 checks each, 1,500 checks, which cost 1,500 x $0.003 = $4.50, plus 30 run starts x $0.00005 = $0.0015, so $4.5015 in a month at the no discount price.
Input example
{"startUrls": [{ "url": "https://www.gov.uk/passport-fees" },{ "url": "https://www.gov.uk/bank-holidays.json" }],"priceSelector": "main table tbody tr:first-child td:first-of-type","priceBelow": 100,"monitorName": "price-watch","ignoreSelectors": [".cookie-banner"],"ignorePatterns": ["Updated on .*"],"maxDiffLines": 50,"maxUrls": 100}
The fields are: startUrls (Page URLs), sourceType, idField, itemsPath, webhookUrl, selector, priceSelector, stockSelector, priceBelow (Target price), monitorName, ignoreSelectors (Ignored elements), ignorePatterns (Ignored text patterns), maxDiffLines (Maximum diff lines), maxUrls (Maximum URLs) and resetBaseline. Use a different Monitor name for each watchlist. Runs with the same name share baselines, runs with different names keep them apart.
Output example for a changed page
This is an example of a row after a price change on a product page. The values are illustrative:
{"url": "https://shop.example/widget","status": "changed","changed": true,"changeTypes": ["price", "availability"],"summary": "Price changed from 49.90 EUR to 39.90 EUR (-20.04%). Availability changed from in_stock to out_of_stock.","price": 39.9,"priceCents": 3990,"currency": "EUR","previousPrice": 49.9,"priceChanged": true,"priceChangePercent": -20.04,"priceTargetMet": true,"availability": "out_of_stock","previousAvailability": "in_stock","availabilityChanged": true,"diff": [],"checkedAt": "2026-10-05T08:00:00.000Z","previousCheckedAt": "2026-10-04T08:00:00.000Z"}
A text change returns diff entries such as { "op": "replace", "from": "Opening hours: 9 to 5", "to": "Opening hours: 10 to 4" }, and diffText holds the same change as - Opening hours: 9 to 5 and + Opening hours: 10 to 4.
Run it every day
Use this to get an alert when a page, a price or a stock status changes.
- Put your page URLs in the list.
- Give the watchlist a Monitor name (
monitorName), for exampleprice-watch. Use one name for each watchlist. - Save the input as a task and add a daily schedule. Use an hourly schedule for fast-moving pages.
- Add an Apify webhook to the run to start your alert.
The first run saves a baseline for each URL. Each later run compares every URL with its baseline and returns changed or unchanged. Example: 20 URLs checked once a day cost 20 x $0.003 + $0.00005 = $0.06005 a day, so 30 daily runs cost $1.8015.
Related Actors
- Web Page to Markdown turns a page into clean Markdown text.
- Sitemap Extractor lists all URLs of a site, ready to paste into Page URLs.
- RSS Feed Reader reads all items of a feed on every run, without a baseline.
- Broken Link Checker finds pages that return 404.
- Website Screenshot API saves a picture of a page.
These Actors are built by the same author. Each one does one step, so you can pick the step you need:
| What you need | Actor |
|---|---|
| Named fields as checked JSON (prices, names, dates) from any page | AI Web Scraper |
| Text and tables from PDF files | PDF Text Extractor |
| Word, Excel and PowerPoint files as Markdown | Document to Markdown API |
| Official public data: Federal Register, FDA recalls, Treasury, ECB rates, SEC filings | Federal Register API, FDA Recalls, Treasury Fiscal Data API, Exchange Rates API, SEC EDGAR Filings API |
FAQ
Is it legal to monitor a website for changes?
The Actor requests only public pages that you list. It sends no login, no cookies and no form data, does not solve CAPTCHAs and does not change IP addresses to avoid limits. It reads robots.txt for every host, refuses paths that robots.txt disallows for its user agent WebsiteChangeMonitor, and waits at least one second between two requests to the same host, longer when robots.txt sets a Crawl-delay. You are responsible for following the terms of the sites you monitor and the laws that apply to you. For example, the GOV.UK reuse page says: "You can scrape GOV.UK as long as you respect our robots.txt policy." Check the terms of each site before you add it, and do not monitor pages that contain personal data.
Which pages does it work on?
Pages whose content is in the HTML the server sends. Pages that build their content with JavaScript after loading show little or no text, and the row notes that. Pages that need a login, show a CAPTCHA or return an access challenge are reported as blocked, and the Actor stops requesting a host after three refused requests in one run.
What are the limits?
Up to 500 URLs per run, a response of up to 3 MB and about 100,000 page elements, 10 seconds for a page to answer (one retry) and 24 seconds in total per URL, up to 5 redirects, and 4 URLs in parallel across different hosts. The baseline of a URL keeps up to 400,000 characters of text. Page text is compared line by line.
Where are the baselines stored?
In a named key-value store in your Apify account called website-change-monitor- followed by your Monitor name. Delete that store to forget all baselines of a watchlist, or select Reset baseline to store the current state again. Do not start two runs with the same Monitor name at the same time.
Why does a page show a change on every run?
Something on the page changes on every visit, for example a clock, a visitor counter or an advert. Add its CSS selector to Ignored elements, add a regular expression to Ignored text patterns, or set a CSS selector to watch only the part you care about. A change of settings stores a new baseline and returns baseline once.
Why is a row blocked, robots-disallowed or error?
blocked means the site answered 401, 403, 429, 503 or a challenge page. The Actor retries 429 and 503 once, then reports it and does not try to get around it. robots-disallowed means robots.txt does not allow the path, or robots.txt could not be read. error means a network error, a timeout, a server error, a page above the size limit or a page that is too complex to read. The error field gives the reason.
How do I get notified?
Enter a Webhook URL to get one POST with only the changes after each run. You can also add an Apify webhook on the run and read the dataset rows where changed is true. The SUMMARY record lists the changed URLs of the run with their summary sentences.
Does a feed or JSON check respect robots.txt?
Yes. Feed and JSON URLs go through the same fetcher as pages, with the same robots.txt check, the same delay between requests to one host and the same size limit of 3 MB. A site that refuses the request, such as a feed behind a block, returns the status blocked and is not charged.
Made by Don Mangu. If this saved you time, a rating helps others find it.