Best Open-Source Tools for Scheduled Product Page Screenshots and Price Checks
Compare open-source options for scheduled product screenshots and price checks, with setup steps, a runnable Playwright monitor, and practical reliability advice.
For a ready-made open-source monitor, start with changedetection.io. It supports scheduled page watches, price and restock monitoring, browser-based fetching for JavaScript pages, interactive Browser Steps, and screenshots attached to supported change notifications. If you need complete control over price parsing, screenshot files, alerts, and storage, build a small scheduled workflow with Playwright. A screenshot records what a page looked like; a separate extraction and comparison step determines whether its price changed.
Choose a check interval that fits the product and retailer, and validate the extracted price against the page yourself. Neither the project documentation nor this guide establishes that every retailer, price selector, or anti-bot system will work reliably.
Which tool should you use?
| Tool | Best fit | What it does | What you operate |
|---|---|---|---|
| changedetection.io | A self-hosted monitoring interface | Scheduled watches, product price/restock mode, HTTP and Chrome-based fetching, Browser Steps, and screenshot notifications. | The application, its storage, fetcher configuration, schedules, and notification destination. |
| Playwright with your own scheduler | A tailored workflow or integration | Browser navigation, extraction, screenshots, price comparison, and custom alert logic that you implement. | All scheduling, persistence, retries, alert delivery, and browser maintenance. |
For most people who want to monitor product pages rather than build a monitoring service, changedetection.io is the stronger starting point. Its README describes both a fast HTTP fetcher and a Chrome-based JavaScript fetcher. Its API also exposes price-related modes and a notification_screenshot option, which defaults to false. That option is for screenshots sent with notifications when the destination supports them; do not assume it archives a screenshot from every check. See the API reference.
For pages that need JavaScript or user interaction, configure the Chrome-based fetcher. Browser Steps can perform actions such as signing in, accepting cookies, or refining a search, and the project README says Browser Steps require Playwright to be enabled. These features do not guarantee access to a retailer that blocks automation or personalizes prices by account or region.
Set up a product watch with changedetection.io
- Deploy changedetection.io using the current instructions in its project repository. The README documents Docker and Python installation paths and says Raspberry Pi and ARM devices are supported. A Raspberry Pi is optional; any suitable always-on host can run a self-hosted monitor.
- Add the specific product URL as a watch. Start with the default HTTP fetcher if the price is present in the initial page response.
- Choose the product price or restock monitoring mode for a single product page. The project describes this mode as extracting metadata from product pages. Check the displayed result against the retailer’s visible price, including currency and sale conditions.
- Set the re-check schedule. Use a sensible interval for how quickly price changes matter to you. Where useful, constrain checks by timezone, day, or time; the API documents the weekly
time_schedule_limitsetting. - If the first fetch misses the price or returns an incomplete page, configure the Chrome-based fetcher. If the page requires an interaction, add Browser Steps and verify that the resulting page is the product state you intend to monitor.
- Configure a notification destination and decide whether it supports screenshot attachments. Enable
notification_screenshotif you want a screenshot attached when a change notification is sent. This is not the same as keeping a screenshot archive for every scheduled check. - Let the watch run and inspect several results. Confirm that the extracted amount, currency, availability, and notification behavior match your needs before relying on alerts.
The project describes price threshold triggers and a price-following mode. The API reference lists processor modes including restock_diff and text_json_diff, plus price-following and threshold fields. Consult the current API documentation for exact field names and accepted values when configuring watches programmatically; settings can change between releases.
Build a scheduled monitor with Playwright
Use this route if you want to own the extraction and screenshot pipeline. The example below is a small Node.js process that checks one URL at a fixed interval, saves a full-page PNG on every successful check, extracts a price from a CSS selector, and reports a change to standard output. It stores the last observed value in a local JSON file. It does not send email or run as a durable service by itself.
Install and run
mkdir product-watch
cd product-watch
npm init -y
npm install playwright
npx playwright install chromium
Save as watch.mjs. Change PRODUCT_URL and PRICE_SELECTOR for the retailer. The selector should target the actual price element, not a page-wide text block.
import { chromium } from 'playwright';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
const PRODUCT_URL = process.env.PRODUCT_URL ?? 'https://example.com/product';
const PRICE_SELECTOR = process.env.PRICE_SELECTOR ?? '[data-testid="price"]';
const INTERVAL_MS = Number(process.env.INTERVAL_MS ?? 60 * 60 * 1000);
const STATE_FILE = 'price-state.json';
const SCREENSHOT_DIR = 'screenshots';
async function readPreviousPrice() {
try {
const state = JSON.parse(await readFile(STATE_FILE, 'utf8'));
return typeof state.price === 'string' ? state.price : null;
} catch (error) {
if (error.code === 'ENOENT') return null;
throw error;
}
}
async function checkOnce(browser) {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
try {
const response = await page.goto(PRODUCT_URL, {
waitUntil: 'domcontentloaded',
timeout: 45_000
});
if (!response || !response.ok()) {
throw new Error(`Navigation returned HTTP ${response?.status() ?? 'no response'}`);
}
const price = (await page.locator(PRICE_SELECTOR).first().innerText({ timeout: 15_000 })).trim();
if (!price) throw new Error('Price selector matched empty text');
await mkdir(SCREENSHOT_DIR, { recursive: true });
const timestamp = new Date().toISOString().replaceAll(':', '-');
const screenshotPath = `${SCREENSHOT_DIR}/${timestamp}.png`;
await page.screenshot({ path: screenshotPath, fullPage: true });
const previous = await readPreviousPrice();
await writeFile(STATE_FILE, JSON.stringify({ price, checkedAt: new Date().toISOString() }, null, 2));
if (previous === null) {
console.log(`Initial price: ${price}; screenshot: ${screenshotPath}`);
} else if (previous !== price) {
console.log(`Price changed: ${previous} -> ${price}; screenshot: ${screenshotPath}`);
} else {
console.log(`No price change (${price}); screenshot: ${screenshotPath}`);
}
} finally {
await page.close();
}
}
const browser = await chromium.launch({ headless: true });
let stopping = false;
process.once('SIGINT', () => { stopping = true; });
process.once('SIGTERM', () => { stopping = true; });
try {
while (!stopping) {
try {
await checkOnce(browser);
} catch (error) {
console.error(`Check failed: ${error.message}`);
}
if (!stopping) await new Promise(resolve => setTimeout(resolve, INTERVAL_MS));
}
} finally {
await browser.close();
}
Run it with environment variables to avoid editing the script:
PRODUCT_URL='https://store.example/item' \
PRICE_SELECTOR='.product-price' \
INTERVAL_MS=1800000 \
node watch.mjs
The interval is measured after each check completes, so a slow page makes the actual start-to-start period longer than INTERVAL_MS. For fixed wall-clock times, missed-run handling, or multiple products, run one check as a job under an operating-system scheduler or a queue instead of keeping this loop as the scheduler.
Turn the example into a useful monitor
- Normalize price values. The example compares displayed strings. That can mistake formatting changes such as
$1,299.00versus$1,299for a price change. Parse amount and currency separately, and preserve the original text for debugging. - Track the right offer. Pages can show a list price, sale price, installment amount, or variant-specific price. Select the value for the chosen size, color, seller, and shipping region.
- Keep a history. The example overwrites its state file and keeps screenshots. For an audit trail, append timestamped observations to a database or JSON Lines file, and apply a retention policy to screenshots.
- Add meaningful alerts. Replace the console message with your notification mechanism. Include product URL, old and new amount, currency, check time, and screenshot path or link.
- Handle uncertain observations. Do not replace the last known good price with an empty value, a CAPTCHA page, or a temporary error. Record the failed check separately and retry with a limit.
The browser calls in this example follow Playwright’s documented navigation, locator, and screenshot APIs. See the Page API and Locator guide.
Capture a screenshot without writing browser code
If you only need a screenshot on demand, use ScreenshotNeo, a website screenshot API and MCP server for developers made by Yorker Media. One GET request captures a URL as PNG, JPEG, WebP, or PDF. The parameters used by other screenshot APIs also work, which can make an existing integration easier to switch.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);
See the ScreenshotNeo API documentation for authentication and capture options. For scheduled price checks, pair a screenshot call with your own scheduler and price extraction; the screenshot endpoint captures pages but does not replace price-monitoring logic.
Or skip the browser setup
ScreenshotNeo can capture the page with one API call:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Cookie banners are accepted before capture, and more than 60 known consent platforms, newsletter popups, and chat widgets are removed; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up for 1,000 free screenshots a month, no card required.
Configuration choices that affect results
| Need | Practical choice | Watch for |
|---|---|---|
| Static product page | Try changedetection.io’s HTTP fetcher first. | Some sites return incomplete markup or different content to automated clients. |
| JavaScript-rendered price | Use the Chrome-based fetcher in changedetection.io or a Playwright browser workflow. | Wait for the price element or a clear page state; page load alone may not mean the price is ready. |
| Login, cookie choice, or search interaction | Configure changedetection.io Browser Steps or implement the interaction in your own browser code. | Respect account security and site terms. A session can expire or change the product state. |
| Price threshold or restock event | Use changedetection.io’s product price/restock features or implement explicit numeric and availability rules. | Validate currency, variants, shipping, coupons, and stock labels separately. |
| Screenshot delivery | Use changedetection.io’s notification screenshot option where the destination supports attachments. | It is notification-oriented and defaults to false; it does not promise a screenshot for every scheduled run. |
| Visual record of each check | Save timestamped screenshot files or use a custom archive in your own workflow. | Control disk use, retention, access to account pages, and backup behavior. |
Reliability, performance, and cost
- Schedule responsibly. Every check consumes browser or network resources and adds traffic to the retailer. Choose a cadence based on how quickly you need to notice a change, and avoid unnecessarily frequent polling.
- Expect missed or ambiguous checks. Retailers change markup, deploy experiments, show consent dialogs, throttle requests, and personalize offers. Treat navigation errors and missing selectors as failed observations, not as a zero price or a confirmed change.
- Keep the monitor available. A self-hosted watcher only runs while its host and fetcher are available. Back up its configuration and data, monitor failed runs, and check the current project deployment guidance. The project documents Docker and Python installation and ARM support.
- Budget storage and retention. Full-page screenshots can accumulate quickly when many products are checked. Keep only the history you need, and consider storing screenshots only when the price changes if your own workflow allows it.
- Estimate browser load. Browser rendering usually needs more resources than a simple HTTP fetch. Use the HTTP path where it produces the correct price and reserve browser fetches for sites that require rendering or interaction.
- Separate monitoring cost from hosting. The open-source tool can be self-hosted, but you still provide an always-on machine and maintain it. For ScreenshotNeo, the supplied plan prices are Free: 1,000 shots/month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No price is detected | The price is inserted by JavaScript, the selector is wrong, or the selected mode cannot read the page’s price metadata. | Inspect the fetched page and price result; switch to the Chrome-based fetcher if rendering is needed; confirm the price element or product metadata on the target page. |
| Browser Steps fail or stop early | A step depends on a changed layout, slow load, expired login, or Playwright is not enabled. | Enable the required browser support, revisit each step, add an appropriate wait, and verify the resulting product state. |
| Every check reports a price change | Formatting, selected variants, rotating promotions, or unrelated page text changed. | Compare a normalized amount and currency, narrow the selector, and choose whether the rule should follow a particular variant or seller. |
| Screenshot arrives only with some alerts | Notification screenshots are disabled by default or the destination does not support them. | Enable notification_screenshot and verify support in the selected notification destination. Use a separate archive if every run needs a screenshot. |
| Checks return a block page or inconsistent price | The retailer may restrict automated requests or personalize content by region or account. | Do not treat the response as a verified price. Review the page manually, check whether an authorized authenticated workflow is appropriate, and reduce unnecessary requests. |
| Playwright script times out on the price selector | The selector does not match, the page has not rendered the price, or a consent overlay blocks the expected state. | Inspect the rendered page, correct PRICE_SELECTOR, wait for the relevant element, and handle legitimate consent interactions where appropriate. |
| Screenshot files grow without limit | The sample saves one full-page image on every successful run. | Add a retention policy, archive selectively, or capture only on price changes. |
| The script repeats a check later than expected | Its interval starts after each check finishes. | Use a system scheduler or job queue for fixed-time checks and define what should happen after a missed run. |
Frequently asked questions
Does changedetection.io save a screenshot on every scheduled check?
The documented notification_screenshot option concerns screenshots included with change notifications, when the destination supports them. It should not be read as an automatic archive of every run.
Do I need a Raspberry Pi?
No. The project says Raspberry Pi and ARM devices are supported, but those are deployment options. You need a host that remains available for the schedules you configure.
Can a screenshot alone tell me whether the price changed?
It can show the page visually, but a reliable alert needs a comparison rule. Extract the price and currency, normalize them, and decide how variants, sale prices, and availability should affect the result.
Which should I choose for a custom alert integration?
Use changedetection.io if its watch, schedule, and notification model meets your needs. Use Playwright when you need to control the browser workflow and own the extraction, persistence, and alert pipeline.
Sources
- changedetection.io project README for watches, scheduling, price/restock monitoring, fetchers, Browser Steps, installation, and supported hardware.
- ChangeDetection.io API v1 documentation for processor modes, scheduling fields, and notification screenshots.
- Playwright Page API and Locator guide for browser navigation, locators, and screenshots.
