> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Build a price monitor

> Track product prices on Amazon, Walmart, eBay, Best Buy or any store with Zenrows, keep the history, and get alerted on every price drop.

Check a list of products every day, store each price with a timestamp, and get an alert when one drops. Prepared retailers such as amazon.com, walmart.com, ebay.com and bestbuy.com need no selectors: Extract returns the price as JSON. Any other store takes a two-line `css_extractor`.

**Uses:** [Extract](/extract/introduction) (`extract=auto`), [Fetch](/fetch/api-reference) with [`css_extractor`](/fetch/features/css-extractor), [Batch scheduled jobs](/batch/developer-guide-restapi#scheduled-jobs)

## How it works

1. **Extract or Fetch reads each price.** Prepared retailers go through `extract=auto`, which returns the product and its pricing as JSON. Other stores go through Fetch with `mode=auto` and a `css_extractor` that names the title and price elements.
2. **SQLite stores the history.** Every check adds one row per product: URL, name, price and time.
3. **Your code detects drops.** Each new price is compared with the last stored one. A lower price triggers an alert.
4. **The alert goes out.** The script posts to a Slack incoming webhook when `SLACK_WEBHOOK_URL` is set, and prints to the console otherwise.
5. **A schedule runs it daily.** Run the script from cron, or let Zenrows run the requests as a scheduled Batch job and read the results.

## Ask your AI agent

With the [Zenrows MCP server](/mcp/overview) connected to Claude, Cursor or any MCP client, paste this prompt. Your agent picks the right Zenrows tools and settings on its own.

```text theme={"dark"}
Use Zenrows to get the current price of https://www.amazon.com/dp/B0DRFGNWV1 and
https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/. Show a table with
product name, current price and list price, and tell me which one is discounted.
```

## Read one price

Try the request on its own first. On a prepared retailer, `extract=auto` returns the product in `parsed.product` and its prices in `parsed.pricing`. `proxy_country=us` shows the prices a US shopper sees:

<CodeGroup>
  ```python Python (Requests) theme={"dark"}
  import requests

  params = {
      "apikey": "YOUR_ZENROWS_API_KEY",
      "url": "https://www.amazon.com/dp/B0DRFGNWV1",
      "mode": "auto",
      "proxy_country": "us",
      "extract": "auto",
  }
  response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
  response.raise_for_status()

  parsed = response.json()["parsed"]
  print(parsed["product"]["title"], parsed["pricing"])
  ```

  ```bash cURL theme={"dark"}
  curl "https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fwww.amazon.com%2Fdp%2FB0DRFGNWV1&mode=auto&proxy_country=us&extract=auto"
  ```
</CodeGroup>

```json Response (trimmed) theme={"dark"}
{
  "parsed": {
    "product": {
      "asin": "B0DRFGNWV1",
      "title": "Redragon K521 Upgrade Rainbow LED Gaming Keyboard, 104 Keys Wired Mechanical Feeling Keyboard ..."
    },
    "pricing": {
      "currency": "$",
      "current_price": 14,
      "deal_badge": null,
      "discount_percent": null,
      "list_price": 15.99
    }
  }
}
```

`GET https://api.zenrows.com/v1/extract/domains` lists every domain Extract covers. To add a new one, see [Extract endpoints](/extract/endpoints).

## Run the monitor

Save this as `monitor.py`, set your key, and run `python monitor.py`. Each run checks every product, stores the prices in `prices.db` and alerts on drops.

```python Python (Requests) theme={"dark"}
import json
import os
import re
import sqlite3
from datetime import datetime, timezone

import requests

ZENROWS_API_KEY = "YOUR_ZENROWS_API_KEY"
SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL")  # optional

# Retailers Zenrows has prepared (Amazon, Walmart, eBay, Best Buy...) need no selectors.
# For any other store, give a css_extractor with the name and price selectors.
STORE_SELECTORS = {"name": "h1.product_title", "price": "p.price", "sku": ".sku"}
PRODUCTS = [
    {"url": "https://www.amazon.com/dp/B0DRFGNWV1"},
    {"url": "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/", "css_extractor": STORE_SELECTORS},
    {"url": "https://www.scrapingcourse.com/ecommerce/product/aero-daily-fitness-tee/", "css_extractor": STORE_SELECTORS},
]

db = sqlite3.connect("prices.db")
db.execute("CREATE TABLE IF NOT EXISTS prices (url TEXT, name TEXT, price REAL, checked_at TEXT)")

def zenrows_params(item: dict) -> dict:
    """Extract for prepared retailers, css_extractor for any other store."""
    if "css_extractor" in item:
        return {"mode": "auto", "css_extractor": json.dumps(item["css_extractor"])}
    return {"mode": "auto", "proxy_country": "us", "extract": "auto"}

def read_product(body: dict) -> dict:
    """Normalize either response to a name and a numeric price."""
    if "parsed" in body:  # extract=auto
        parsed = body["parsed"]
        product = parsed.get("product") or {}
        pricing = parsed.get("pricing") or {}
        return {
            "name": product.get("name") or product.get("title"),
            "price": pricing.get("current_price", product.get("price")),
        }
    match = re.search(r"[\d.,]+", body.get("price") or "")  # css_extractor: "$57.00"
    return {"name": body.get("name"), "price": float(match.group().replace(",", "")) if match else None}

def get_product(item: dict) -> dict:
    response = requests.get(
        "https://api.zenrows.com/v1/",
        params={"apikey": ZENROWS_API_KEY, "url": item["url"], **zenrows_params(item)},
        timeout=180,
    )
    response.raise_for_status()
    return read_product(response.json())

def alert(message: str) -> None:
    if SLACK_WEBHOOK_URL:
        requests.post(SLACK_WEBHOOK_URL, json={"text": message}, timeout=30)
    else:
        print(message)

def record(url: str, product: dict) -> None:
    """Store today's price, then compare it with the last one stored for this URL."""
    if product["price"] is None:
        print(f"No price found today: {url}")
        return
    previous = db.execute(
        "SELECT price FROM prices WHERE url = ? ORDER BY checked_at DESC LIMIT 1", (url,)
    ).fetchone()
    price = float(product["price"])
    db.execute(
        "INSERT INTO prices VALUES (?, ?, ?, ?)",
        (url, product["name"], price, datetime.now(timezone.utc).isoformat()),
    )
    db.commit()

    if previous and price < previous[0]:
        drop = (previous[0] - price) / previous[0] * 100
        alert(f"Price drop: {product['name']} {previous[0]:.2f} -> {price:.2f} (-{drop:.0f}%)\n{url}")
    else:
        print(f"{product['name'][:40]}: {price:.2f}")

if __name__ == "__main__":
    for item in PRODUCTS:
        record(item["url"], get_product(item))
```

<Tip>
  Keep your keys out of the code in production: read `ZENROWS_API_KEY` and `SLACK_WEBHOOK_URL` from environment variables or a secrets manager. To send alerts to Slack, create an <a href="https://api.slack.com/messaging/webhooks" rel="nofollow">incoming webhook</a> and export its URL as `SLACK_WEBHOOK_URL`.
</Tip>

## Run it every day

**With cron.** Run the script on your own server once a day, at 08:00:

```bash theme={"dark"}
0 8 * * * cd /path/to/monitor && /usr/bin/python3 monitor.py >> monitor.log 2>&1
```

**With a scheduled Batch job.** Zenrows runs the requests on its side every day, and your code only reads the results. Install the SDK (`pip install zenrows`), save this as `batch_monitor.py` next to `monitor.py`, and run `python batch_monitor.py schedule` once:

```python Python (SDK) theme={"dark"}
import json
import sys

from zenrows import ZenRowsBatchClient
from zenrows.batch import Calendar, Daily

from monitor import PRODUCTS, read_product, record, zenrows_params

client = ZenRowsBatchClient(api_key="YOUR_ZENROWS_API_KEY")
TASKS = [{"url": item["url"], "zenrows_params": zenrows_params(item)} for item in PRODUCTS]

def schedule() -> None:
    """Create the daily job once. Zenrows runs it every day at 08:00."""
    job = client.submit_scheduled(
        Calendar(times_of_day=["08:00"], cadence=Daily(), timezone="America/New_York"),
        TASKS,
    )
    print("Scheduled job:", job.job_id)

def check(job_id: str) -> None:
    """Read the job's latest run and record every price."""
    run = client.job(job_id).run
    for task in run.results(status="successful"):
        body = json.loads(run.download_task_to_memory(task))
        record(str(task.url), read_product(body))

if __name__ == "__main__":
    if sys.argv[1] == "schedule":
        schedule()
    else:
        check(sys.argv[2])
```

After each daily run, call `python batch_monitor.py check <job-id>`. A check after setting the tee's stored price to 30.00 to simulate a drop:

```text theme={"dark"}
Adrienne Trek Jacket: 57.00
Price drop: Aero Daily Fitness Tee 30.00 -> 24.00 (-20%)
https://www.scrapingcourse.com/ecommerce/product/aero-daily-fitness-tee/
Redragon K521 Upgrade Rainbow LED Gaming: 14.00
```

To run the check the moment a run ends, attach a webhook to the job and call `check` from your webhook handler, see [Batch webhooks](/batch/developer-guide-restapi#set-up-webhooks).

## Example output

Output of `monitor.py`. The first run stores a baseline for each product:

```text theme={"dark"}
Redragon K521 Upgrade Rainbow LED Gaming: 14.00
Adrienne Trek Jacket: 57.00
Aero Daily Fitness Tee: 24.00
```

A later run, after setting the jacket's stored price to 65.00 to simulate a drop:

```text theme={"dark"}
Redragon K521 Upgrade Rainbow LED Gaming: 14.00
Price drop: Adrienne Trek Jacket 65.00 -> 57.00 (-12%)
https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/
Aero Daily Fitness Tee: 24.00
```

The history in `prices.db` (`sqlite3 prices.db "SELECT name, price, checked_at FROM prices"`):

```text theme={"dark"}
Adrienne Trek Jacket|65.0|2026-10-14T23:02:18.071495+00:00
Adrienne Trek Jacket|57.0|2026-10-14T23:02:31.082772+00:00
```

## Customize it

| Change | How |
| - | - |
| Monitor Walmart, eBay or Best Buy | Add the product URL with no `css_extractor`. Extract covers them, see [Extract](/extract/introduction) |
| Monitor another store | Add the URL with a `css_extractor` for its title and price elements, see [CSS extractor](/fetch/features/css-extractor) |
| Prices from another country | Set `"proxy_country"` to that country's ISO code, with `"mode": "auto"`, in `zenrows_params`, see [Proxy country](/fetch/features/proxy-country) |
| Alert only on big drops | Change the condition to `price < previous[0] * 0.9` for drops over 10% |
| Track hundreds of products | Use the scheduled Batch job, which runs them in parallel, see [Scrape a list of URLs](/academy/scrape-from-a-list-of-urls) |
| Store in Postgres or BigQuery | Replace the two `db.execute` calls with your database client |

## Troubleshooting

* **`No price found today`**: the price element moved or the selector doesn't match. Open the page, inspect the price, update the `css_extractor`, and test it with one request.
* **A request returns `422`**: the page wasn't retrieved. Keep `mode=auto` on store requests so Zenrows escalates to JavaScript rendering and premium proxies when a page needs them, see [error codes](/api-error-codes).
* **The script stops with a timeout**: set the HTTP client timeout to 180 seconds or more, as in the samples.
* **Every run alerts on the same product**: the store shows a different price per visitor or region. Pin the region with `proxy_country`.

## Related recipes

<CardGroup cols={2}>
  <Card title="Detect changes on any page and get alerted" icon="bell" href="/academy/pipelines/change-detection">
    Watch any page or field, not only prices.
  </Card>

  <Card title="Turn a whole catalog into a structured dataset" icon="table" href="/academy/pipelines/catalog-dataset">
    Collect every product of a store to choose what to monitor.
  </Card>

  <Card title="Run a Batch job on a schedule" icon="calendar" href="/academy/batch/scheduled-jobs">
    Every schedule option for Batch jobs.
  </Card>

  <Card title="Move an existing scraper to Zenrows" icon="arrows-exchange" href="/academy/integrate-price-monitoring">
    Keep your parser and swap the request layer.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.