ScreenshotNeo

BlogHow-to

How to capture product page screenshots with Playwright after a price drops

Use Playwright to detect a product price drop, preserve the page as evidence, and handle changing prices, variants, and capture failures.

By the ScreenshotNeo team4 October 202610 min read

To capture a product page after its price drops, use Playwright to open the page, wait for the displayed price, parse it, compare it with a saved observation or threshold, and take a screenshot only when the comparison indicates a drop. Playwright captures the page; your application defines what counts as a drop and stores the price history.

The example below uses Node.js and Playwright. Replace the sample price locator and parsing logic with ones that match the retailer and locale you monitor. A selector such as data-testid="product-price" is only an example; it is not a standard shared by retailers.

1. Install Playwright and choose a price source

In a new Node.js project, install Playwright and its browser:

npm init -y
npm install playwright
npx playwright install chromium

Choose a locator that targets the price shown to the user. Playwright recommends resilient, user-facing locator strategies where possible; selectors tied to a page’s DOM structure can break when the site changes. Its documentation describes locators as the core of auto-waiting and retrying. Playwright locators

Inspect the actual page to determine how it represents the price, currency, selected variant, and availability. A page may show several prices, such as a sale price, crossed-out list price, financing amount, or price for a different size. Ensure the locator identifies the price relevant to the product configuration you intend to track.

2. Runnable example: compare saved prices and capture a drop

This script stores the last observed price in a local JSON file. It captures a full-page PNG when the current price is lower than the saved price. It also saves the current observation after checking, so the next run has a value to compare.

const { chromium } = require('playwright');
const fs = require('node:fs/promises');
const path = require('node:path');

const productUrl = process.env.PRODUCT_URL || 'https://example.com/product';
const productKey = process.env.PRODUCT_KEY || productUrl;
const historyPath = path.resolve('price-history.json');
const priceSelector = process.env.PRICE_SELECTOR || '[data-testid="product-price"]';

function parsePrice(text) {
  // Adapt this to the retailer's locale and currency format.
  // This example accepts a number with an optional decimal point and comma grouping.
  const normalized = text.replace(/\s/g, '').replace(/,/g, '');
  const match = normalized.match(/\d+(?:\.\d{1,2})?/);
  if (!match) throw new Error(`Could not parse a price from: ${text}`);
  return Number(match[0]);
}

async function readHistory() {
  try {
    return JSON.parse(await fs.readFile(historyPath, 'utf8'));
  } catch (error) {
    if (error.code === 'ENOENT') return {};
    throw error;
  }
}

async function writeHistory(history) {
  await fs.writeFile(historyPath, JSON.stringify(history, null, 2));
}

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });

  try {
    await page.goto(productUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
    const priceLocator = page.locator(priceSelector).first();
    await priceLocator.waitFor({ state: 'visible', timeout: 15000 });

    const displayedPrice = (await priceLocator.innerText()).trim();
    const currentPrice = parsePrice(displayedPrice);
    const history = await readHistory();
    const previous = history[productKey] || null;

    if (previous && previous.currency === 'USD' && currentPrice < previous.amount) {
      const filename = `price-drop-${Date.now()}.png`;
      await page.screenshot({ path: filename, fullPage: true });
      console.log(`Price dropped from $${previous.amount} to $${currentPrice}. Saved ${filename}`);
    } else {
      console.log(`Observed price: $${currentPrice}; previous observation: ${previous?.amount ?? 'none'}`);
    }

    history[productKey] = {
      amount: currentPrice,
      currency: 'USD',
      displayedText: displayedPrice,
      observedAt: new Date().toISOString(),
    };
    await writeHistory(history);
  } finally {
    await browser.close();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run it with a real product URL and a selector verified for that site:

PRODUCT_URL='https://example.com/product' \
PRODUCT_KEY='example-item-blue-large-usd' \
PRICE_SELECTOR='[data-testid="product-price"]' \
node price-watch.js

The sample explicitly compares USD observations. Change the currency and parsing rules together for another locale; never compare amounts across currencies as if they were the same unit. The key should distinguish product variants and regions when those affect price.

3. Define a reliable price-drop condition

A raw number comparison can mistake a variant change, currency change, unavailable product, or transient placeholder for a real reduction. Before taking a screenshot, define the observation you mean to compare.

  • Product identity: include the product and selected variant, such as size or color, in the history key.
  • Currency and region: store the currency and market with the amount; compare only matching observations.
  • Availability: decide whether to ignore unavailable or out-of-stock states rather than treating a missing price as zero.
  • Price type: consistently track the current purchase price, excluding crossed-out prices, installments, shipping, or discounts that require a code unless those are your intended measure.
  • Noise tolerance: optionally require a minimum absolute or percentage drop to avoid captures for tiny rounding changes.
  • First observation: save a baseline without calling it a drop; there is no prior value to compare on the first run.

For a threshold alert instead of comparison with history, replace the condition with an explicit rule such as currentPrice <= targetPrice. For both rules, record the observation time and the displayed text so you can audit why a capture happened.

4. Wait for the price, not an arbitrary delay

Retail pages often render the product shell before the price, or update the price after a variant selection. Waiting for the relevant locator to become visible is a better readiness condition than relying on a fixed sleep. If the retailer updates a visible price asynchronously, wait for the expected value or poll the price state with a bounded timeout.

const price = page.locator('[data-testid="product-price"]');
await price.waitFor({ state: 'visible', timeout: 15000 });

// If an action should change the price, wait for a meaningful state change.
const before = (await price.innerText()).trim();
await page.getByLabel('Color').selectOption('blue');
await page.waitForFunction(
  ({ selector, previous }) => {
    const node = document.querySelector(selector);
    return node && node.textContent.trim() !== previous;
  },
  { selector: '[data-testid="product-price"]', previous: before },
  { timeout: 10000 }
);

Use the wait that matches the page behavior. A selector becoming visible does not prove that every other page component has finished loading, but it is a relevant condition for reading that price. Avoid making network-idle or long fixed delays the default without a reason; pages with analytics, polling, or streaming requests may not become idle.

5. Choose what to capture

Playwright can capture the visible viewport, a full page, a screenshot buffer, or one element. Choose based on what the evidence needs to show. Playwright screenshots

Evidence needed Capture Trade-off
Price and nearby product details await priceLocator.screenshot({ path: 'price.png' }) Focused evidence, but may omit surrounding page context.
Visible page as a shopper sees it await page.screenshot({ path: 'price-drop.png' }) Captures the current viewport only.
Full product page context await page.screenshot({ path: 'price-drop.png', fullPage: true }) Captures the full scrollable page and may be tall or include dynamic sections.
Send bytes to storage or an API const png = await page.screenshot() Returns a buffer instead of writing a file.

Example element capture:

await page.locator('[data-testid="product-info"]').screenshot({ path: 'product-evidence.png' });

Full-page captures can trigger lazy-loaded content as the page is scrolled. If below-the-fold details matter, inspect the resulting image and verify that the retailer actually rendered those sections before capture.

6. Reduce visual noise when needed

Price badges, carousels, clocks, and animated elements can change during a capture. Playwright screenshot options can disable animations or apply screenshot styling; check the option support for the Playwright version installed in your project. Page screenshot API

await page.screenshot({
  path: 'price-drop.png',
  fullPage: true,
  animations: 'disabled',
  style: '[data-testid="recommendations"] { visibility: hidden !important; }',
});

Use styling only to suppress known irrelevant movement. Do not hide the price, selected variant, or state needed to interpret the evidence. If screenshots are for visual regression testing, Playwright Test’s toHaveScreenshot waits for two consecutive screenshots to stabilize before comparison. That assertion belongs to the Playwright Test runner; it does not detect price drops or replace your price history logic. Playwright visual comparisons

7. Run it on a schedule and keep history

A script detects a drop only when it runs. Schedule it with your existing job runner or CI system at a cadence appropriate for the site and your use case. Keep a durable record of amount, currency, product variant, observation time, and screenshot location. A local JSON file works for a small demonstration; concurrent jobs or multiple products need storage that handles locking and durable updates, such as a database or object store.

Prevent overlapping runs for the same product or use a storage operation that makes comparison and update atomic. Otherwise two jobs can read the same old value, both trigger captures, and race to write history. Retain captures only as long as needed, and use descriptive keys or metadata to connect each image to the corresponding observation.

8. cURL, Python, and Node.js with ScreenshotNeo

If you already have a price-drop signal and only need the screenshot, ScreenshotNeo accepts a URL in one GET request and returns an image or PDF. The call does not perform price history comparison; keep that logic in your monitor, then request the capture when its drop condition is true. See the ScreenshotNeo API documentation for capture options.

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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Replace the example URL with the product URL that triggered your condition. In a real monitor, check the HTTP response and preserve the response headers alongside the image when useful; ScreenshotNeo identifies page verdict and billing status with response headers. Its capture options include selector and full-page capture, custom waiting, cookies and headers, and caching.

Or skip the browser setup

Cookie banners are accepted and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed, and responses say which page verdict and billing status applied. An MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Your price comparison still decides when to capture.

Sign up for 1,000 free screenshots a month, with no card.

9. Troubleshooting

Symptom Likely cause Fix
Locator timeout The selector is wrong, the price is not visible yet, or a consent dialog blocks the page. Inspect the rendered page, choose a user-facing or stable locator, and wait for the actual price element with a bounded timeout.
Parser returns the wrong amount Thousands and decimal separators, currency symbols, or multiple prices are interpreted incorrectly. Parse according to the page locale and target currency. Log the raw displayed text and add cases for the site’s formats.
False price drop The selected variant, region, or price type changed between observations. Include variant and market in the history key and compare only matching currency and price types.
No screenshot on the first run No baseline exists yet. Save the first valid observation; compare on a later run or use an explicit target threshold.
Screenshot is blank or incomplete The page has not rendered the relevant content, navigation was interrupted, or a site check blocked the browser. Wait for the price and product context, inspect the page state before capture, and treat challenge or failure pages as invalid observations rather than price changes.
Full-page image misses lazy content The page only loads sections when scrolled into view. Scroll the relevant sections into view and wait for their content before capturing, then verify that the image includes them.
Capture differs between runs Animations, rotating content, recommendations, or personalized content changed. Disable animations or hide known irrelevant selectors using screenshot styling; keep evidence-bearing content visible.
Two alerts for one drop Overlapping scheduled jobs both compared against the same previous observation. Serialize jobs per product or make compare-and-update atomic in durable storage.

10. Performance, reliability, and cost

Each Playwright run launches or connects to a browser, navigates to the retailer, waits for the price, and may capture a large image. Reuse a browser process across multiple product checks when appropriate, while keeping page contexts isolated. Limit concurrency to what your environment and the retailer can handle, and use bounded navigation and locator timeouts so one slow page does not stall a monitoring batch.

For reliability, distinguish navigation errors, missing price elements, parse failures, unavailable products, and genuine price changes in logs and stored status. Retry transient navigation failures with a limit and backoff; do not turn a failed read into a numeric value. Keep the previous valid observation if the current page is unusable. Follow the retailer’s access rules and avoid aggressive polling.

Self-hosted Playwright costs depend on the compute, browser runtime, storage, and schedule you choose; the research sources do not establish a universal benchmark or cost. A screenshot API can avoid maintaining browser infrastructure, but price tracking and the trigger still belong to your application. ScreenshotNeo’s stated plans are Free with 1,000 shots per month, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free, and every feature is on every plan.

Frequently asked questions

Does Playwright know when a price has dropped?

No. It can read page content and take screenshots. Your code must define the comparison and retain prior observations or a target threshold.

Should I use a full-page screenshot or an element screenshot?

Capture the price element for compact evidence; capture the full page when surrounding product context matters. You can save both if the distinction is useful.

Can toHaveScreenshot replace price history?

No. It stabilizes and compares rendered screenshots in Playwright Test. It does not determine whether the displayed numeric price is lower than a previous observation.

Can ScreenshotNeo detect the drop for me?

No. It captures a supplied URL. Your monitor decides when the price condition is met and then calls the API.