How to Track Prices on Indian Shopping Websites with Puppeteer Screenshots
Build a cautious Puppeteer job that records a product’s displayed price, capture time, and screenshot, with selector waits, error handling, and storage guidance.
How can I track a product’s price on an Indian shopping site and save a screenshot each time it changes? Use a scheduled Puppeteer script to open a product URL, wait for that site’s price element, read and store its displayed text with a timestamp and product identity, then save a screenshot. The critical detail is to verify the page and price content: navigation completing does not prove that the right product page or a correct price loaded.
This guide shows a configurable Node.js workflow. Selectors, dialogs, and displayed price formats vary by retailer and page, so treat them as site-specific settings. Review the current terms and permissions for the retailer before automating access. The research for this article did not access or test live retailer pages, and the example selectors below are placeholders.
1. Decide what you need to record
A screenshot is useful evidence of what the browser rendered at a particular moment; it is not a guarantee of the amount every shopper would pay. The visible offer can depend on the selected variant, seller, delivery location, account state, discount, and stock. Delivery charges or checkout adjustments may appear later.
Store each observation as a record containing at least:
- A stable product identifier you choose, plus the product URL.
- The capture timestamp, including timezone or UTC convention.
- The displayed price text exactly as read, and optionally a separately parsed numeric amount and currency.
- The screenshot path or object-storage key.
- The site-specific selector/configuration version and a capture outcome such as success, missing price, or HTTP error.
Preserving the raw displayed text alongside any parsed amount helps avoid silently misreading Indian number grouping, currency symbols, discounts, or text such as “Currently unavailable.” Do not treat a missing price as zero.
2. Set up a Puppeteer project
Use a supported Node.js release and install Puppeteer in a project directory:
mkdir price-watch
cd price-watch
npm init -y
npm install puppeteer
Puppeteer downloads a compatible browser as part of its standard installation. If your environment provides a browser separately, follow Puppeteer’s official configuration for connecting to it rather than assuming the executable path. See the Puppeteer getting started guide.
3. Configure the target page
For every supported retailer and page type, define the URL, a selector for the price, and optionally a selector for the product title or selected variant. A CSS selector copied from one product page may not work on another category, variant, or redesigned page.
// config.mjs
export const products = [
{
id: 'replace-with-your-product-id',
url: 'https://example.com/product',
priceSelector: '[data-your-site-price]', // Replace with a verified site-specific selector.
titleSelector: '[data-your-site-title]', // Optional; replace or remove.
screenshot: 'captures/product.webp'
}
];
Do not assume a retailer exposes a public consumer price-monitoring API. The located Flipkart API documentation describes seller-oriented Marketplace APIs, and says production information is governed by Marketplace terms. Its listing APIs include seller listing attributes; that does not establish a consumer API for monitoring public listings. Check the current rules that apply to your use of any retailer’s site.
4. Run a price capture with Puppeteer
Save this as capture.mjs. It takes a product configuration, checks the main navigation response when available, waits for a visible price element, extracts its text, and captures the page. It writes a JSON-lines record after a successful capture. The selector is intentionally a placeholder; replace it with one appropriate to a page you are permitted to access.
import puppeteer from 'puppeteer';
import { mkdir, appendFile } from 'node:fs/promises';
import { dirname } from 'node:path';
const product = {
id: 'replace-with-your-product-id',
url: 'https://example.com/product',
priceSelector: '[data-your-site-price]', // Replace with a site-specific selector.
titleSelector: '[data-your-site-title]', // Optional; replace or remove.
screenshotPath: 'captures/product.png'
};
const timeoutMs = 20_000;
const recordPath = 'captures/prices.jsonl';
async function captureProduct(item) {
const capturedAt = new Date().toISOString();
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1365, height: 900 },
deviceScaleFactor: 1
});
page.setDefaultNavigationTimeout(timeoutMs);
page.setDefaultTimeout(timeoutMs);
const response = await page.goto(item.url, {
waitUntil: 'domcontentloaded',
timeout: timeoutMs
});
// A null response can occur for non-HTTP navigation; handle it explicitly.
if (!response) {
throw new Error('Navigation returned no main-resource response');
}
if (response.status() >= 400) {
throw new Error(`Product page returned HTTP ${response.status()}`);
}
// A locator waits for the configured element. Visibility alone does not
// guarantee that its contents are a valid price; validate the text below.
const priceLocator = page.locator(item.priceSelector);
await priceLocator.wait();
const priceText = await priceLocator.evaluate(el => el.textContent?.trim() ?? '');
if (!priceText) {
throw new Error('Price element appeared but contained no text');
}
let title = null;
if (item.titleSelector) {
const titleLocator = page.locator(item.titleSelector);
try {
await titleLocator.wait({ timeout: 5_000 });
title = await titleLocator.evaluate(el => el.textContent?.trim() ?? '');
} catch {
// The title is optional; retain the configured product ID and URL.
}
}
await mkdir(dirname(item.screenshotPath), { recursive: true });
await page.screenshot({ path: item.screenshotPath, fullPage: true });
const record = {
productId: item.id,
url: item.url,
capturedAt,
title,
displayedPrice: priceText,
screenshotPath: item.screenshotPath,
httpStatus: response.status()
};
await mkdir(dirname(recordPath), { recursive: true });
await appendFile(recordPath, `${JSON.stringify(record)}\n`, 'utf8');
return record;
} finally {
await browser.close();
}
}
captureProduct(product)
.then(record => console.log(JSON.stringify(record, null, 2)))
.catch(error => {
console.error(`Capture failed for ${product.id}:`, error.message);
process.exitCode = 1;
});
Run it with node capture.mjs. The code is an illustrative pattern based on Puppeteer’s documented APIs, not a tested retailer integration. Validate the selector and output against pages you are allowed to access before relying on the records.
Why wait for the price selector?
page.goto() resolves when its chosen navigation condition is met. A JavaScript-rendered product price may appear later, and a page can finish navigation while showing an error, consent screen, location prompt, or incomplete product view. Puppeteer recommends locator APIs for interaction because they wait for elements and readiness. waitForSelector is a lower-level alternative when you need explicit visibility options. See the official page interactions guide and waitForSelector API.
Prefer a selector or application-specific readiness condition over a fixed sleep. General network-idle conditions can be useful for some pages, but analytics, streaming requests, or delayed content can make them unreliable as the only proof that a price is ready. The Puppeteer screenshot guide demonstrates navigation followed by a screenshot; for price tracking, add a check for the actual content you need.
5. Choose screenshot scope and format
The example saves a full-page PNG. Use a full-page capture when the surrounding product details, selected variant, or visible context matter. A compact element capture is useful when you need to keep a price region alongside the record:
const priceElement = await page.$(product.priceSelector);
if (!priceElement) throw new Error('Price element not found');
await priceElement.screenshot({ path: 'captures/price.png' });
An element screenshot scrolls the target into view when necessary. Page screenshots support options such as path, fullPage, and clip; consult the ScreenshotOptions reference for the current API. For example, to capture a fixed viewport instead of the full page:
await page.screenshot({ path: 'captures/viewport.png', fullPage: false });
PNG is lossless and suitable when small visual details matter. JPEG and WebP can reduce storage size if their compression is acceptable for your evidence needs. Keep screenshot naming or metadata linked to the structured record so a file is never mistaken for a different product or capture time.
6. Schedule captures and compare observations
Run the script from a scheduler or job runner at a cadence that is appropriate for your need and allowed by the target site. Avoid excessive polling. A small delay between observations can miss short-lived price changes; very frequent requests add load and maintenance without guaranteeing a complete history.
For a single process, JSON Lines is a simple append-only starting point. For multiple products or workers, use a database or object storage with a unique observation ID and a separate screenshot reference. Make writes atomic where practical: a failed screenshot should not leave a record that looks like a complete capture. Retain failed-attempt metadata separately if you need operational visibility.
Compare normalized values only after preserving the original display text. A robust parser needs to account for the site’s currency, Indian digit grouping, possible sale and list prices, and availability labels. Do not infer currency from a symbol alone when the site or account context can vary. Keep product variant and seller context if they affect which offer is shown.
7. Reliability, performance, and cost
- Readiness and timeouts: Use finite navigation and selector timeouts. Record a timeout or missing selector as a failed observation, not as a price.
- HTTP status: Inspect the main-resource response. Puppeteer documents that valid HTTP error responses such as 404 or 500 may be returned without throwing in headless shell mode, so a resolved navigation is not sufficient evidence of success. See Page.goto.
- Resource use: Launching a browser has setup and memory overhead. Reusing a browser for a controlled batch can reduce repeated startup work, but isolate pages and ensure every page is closed. Measure resource use in your own deployment rather than assuming a fixed rate.
- Screenshot storage: Full-page images use more storage than a small element crop. Set a retention policy and keep records searchable independently of image files.
- Price correctness: Validate that extracted text looks like the expected offer and preserve the original text. A page may show a placeholder, old value, or a different offer than intended.
- Access and policy: The research does not establish that automated consumer collection is permitted on every Indian shopping site. Review applicable terms, access rules, and screenshot reuse permissions. Do not attempt to evade bot checks or access restrictions.
8. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Navigation succeeds but no price is recorded | The price loads after navigation, selector is wrong, or a dialog/error view replaced the product. | Inspect the rendered page you are permitted to access, verify the selector for that page type, and wait on the actual price element. Keep a distinct missing-price outcome. |
| HTTP 404 or 500 appears in the browser but no navigation exception occurs | HTTP error statuses can still produce a navigation response. | Check response.status() and mark the capture unsuccessful for error statuses. |
| Selector timeout | Selector changed, element is in a different page state, or page content did not load. | Confirm the target element and state, raise the timeout only when justified, and handle failure explicitly. Do not replace every selector wait with a long fixed sleep. |
| Recorded price is blank or says unavailable | The element exists but does not contain a purchasable offer, or the selector targets a label. | Preserve the raw text and classify availability separately; do not convert it to zero. |
| Screenshot does not show the extracted value | Content changed between extraction and capture, the element moved, or the crop differs from the intended evidence. | Capture the relevant element or viewport, and consider extracting and screenshotting in a short sequence. Keep the capture timestamp and outcome together. |
| Browser process remains after a failed capture | Cleanup was skipped in an error path. | Keep browser shutdown in finally, as in the example, and monitor the job runner for orphaned processes. |
| Price history has inconsistent values | Variant, seller, delivery location, account state, or currency display changed. | Store these relevant inputs when available and compare like-for-like observations. |
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return an image or PDF; for this workflow it can capture a product page without installing and maintaining a browser in your job. Use the returned screenshot alongside your own product identity, timestamp, and price extraction process. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/product -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/product"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/product'
});
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()))
);
- Cookie banners, popups, and chat widgets are removed before capture; each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers say the page verdict and billing outcome.
- An MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs.
- The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card.
10. Is Amazon’s built-in history enough?
If you only need Amazon, a native price-history feature may require less setup than maintaining browser automation. Amazon Staff reported in an article published May 1, 2026 and updated June 24, 2026 that price history was available to customers in India, with 30-, 90-, and 365-day views and the 365-day insights rolling out at that time. Amazon also reported that over 50 million customers had used the feature; that is Amazon’s company-reported figure, not independent research. The cited feature is an Amazon option and does not establish cross-marketplace coverage. See Amazon’s price-history update.
FAQ
Does a screenshot prove the final checkout price?
No. It records what the browser rendered at capture time. Delivery fees, account-specific offers, taxes, or checkout changes may affect the amount payable.
Can I use one selector across Indian shopping sites?
No. Choose and maintain selectors for each supported site and relevant page layout. A selector that works for one product or variant may not apply elsewhere.
Does Flipkart provide a public consumer API for price history?
The located official documentation describes seller-oriented Marketplace APIs. It does not establish a public consumer price-monitoring API.
Should I use Puppeteer’s network-idle navigation condition?
It can suit some pages, but it is not proof that the displayed price is present and correct. Wait for the relevant content and validate its text.


