← Back to blogTutorial

How to Track Amazon Competitor Prices Daily (Export to CSV and Google Sheets)

· 10 min read

You sell on Amazon. You have a list of competitor ASINs you care about — direct rivals in your category, products you benchmark against, sellers who keep stealing your BuyBox. You want a spreadsheet that updates every morning with their current prices, the BuyBox holder, and the trend since last week. This guide is the end-to-end pipeline: scheduled scrape, CSV (or Postgres) storage, and a push to Google Sheets that takes ten lines of code.

Why Daily Competitor Prices Matter

Amazon's BuyBox algorithm reshuffles every few minutes on commoditized listings, but the strategy behind those moves operates on a slower clock. Competitors lower prices for sale events, raise them when they sell out, drop them again when restocked, and quietly drift down over months as new entrants undercut them. Daily snapshots are the right resolution to see those macro shifts; minute-level data is too noisy for a human to act on.

A daily competitor pricing dashboard answers three questions that a one-off scrape cannot:

  1. Is the floor moving? Over 30 days, is the median competitor price trending down (your margin is compressing) or stable?
  2. Who is the repeat undercutter? A specific competitor consistently sitting $0.50 below you is a different problem from a rotating cast of opportunistic resellers.
  3. Did the BuyBox holder change this week? Even if the price did not move, a new seller controlling the BuyBox is a competitive signal.

The pricing data itself is half the value. The history is the other half.

What Data the Amazon Product Page Exposes

For each ASIN you scrape, you get back the fields that matter for pricing intelligence:

  • asin, title, brand
  • price and currency
  • buybox_seller — the "Sold by" value on the listing
  • fulfillment — FBA, FBM, or Amazon-fulfilled
  • availabilityIn Stock, Only 3 left, Currently unavailable
  • other_sellers — an array of additional offers with seller name, price, and fulfillment
  • rating, review_count

Here is what comes back from the LogPose amazon/smart endpoint for a product page (trimmed):

{
  "asin": "B0F2GYMC8H",
  "title": "Anker Power Bank 20000mAh...",
  "price": 39.99,
  "currency": "USD",
  "availability": "In Stock",
  "buybox_seller": "Anker Direct",
  "fulfillment": "FBA",
  "other_sellers": [
    {"seller_name": "PowerHub", "price": 41.50, "fulfillment": "FBA"},
    {"seller_name": "ChargeWorld", "price": 42.99, "fulfillment": "FBM"}
  ],
  "rating": 4.6,
  "review_count": 8421
}

That is enough to populate a useful daily report. Now the pipeline.

The DIY Approach (and Where It Breaks)

For a small list (5-10 ASINs, no scheduling), a Python loop and a requests + BeautifulSoup script is enough to get started:

import csv, time, requests
from bs4 import BeautifulSoup

HEADERS = {"User-Agent": "Mozilla/5.0 ... Chrome/127.0.0.0 ..."}


def fetch(asin: str) -> dict | None:
    r = requests.get(f"https://www.amazon.com/dp/{asin}", headers=HEADERS, timeout=15)
    if r.status_code != 200 or "validateCaptcha" in r.url:
        return None
    soup = BeautifulSoup(r.text, "html.parser")
    price = soup.select_one("span.a-price > span.a-offscreen")
    seller = soup.select_one("#sellerProfileTriggerId")
    if not price:
        return None
    return {
        "asin": asin,
        "price": float(price.get_text(strip=True).replace("
quot;, "").replace(",", "")), "buybox_seller": seller.get_text(strip=True) if seller else None, } with open("competitors.csv") as f: asins = [row["asin"] for row in csv.DictReader(f)] for asin in asins: print(fetch(asin)) time.sleep(5)

The limitations show up fast:

  • CAPTCHA cliff. A clean residential IP gets you 5-30 requests before /errors/validateCaptcha shuts you down. From a datacenter IP, your first request is already the redirect.
  • No BuyBox seller resilience. The seller selector changes every few weeks as Amazon A/B-tests the merchant-info block.
  • Sequential speed. 100 ASINs × 5s pacing = over 8 minutes per run, and that is before anything fails.
  • No retry plumbing. Once a single request fails, you need a queue, exponential backoff, and a dead-letter list. None of that is in the loop above.

For a daily run on 50+ ASINs, the operational cost of running this yourself — proxy rotation, CAPTCHA handling, selector maintenance, retry logic — usually exceeds the cost of a managed endpoint within a month.

The API Approach with Working Python

The canonical helper used across this pipeline. Submit, poll, return typed JSON.

import os, time, requests

API_KEY = os.environ["LOGPOSE_API_KEY"]
BASE = "https://api.logposervices.com/api/v1"
HEADERS = {"X-API-Key": API_KEY}

def amazon_smart(url_or_asin: str, pages: int = 1, timeout_s: int = 120) -> dict:
    r = requests.get(
        f"{BASE}/ecommerce/amazon/smart",
        params={"url": url_or_asin, "pages": pages},
        headers=HEADERS, timeout=30,
    )
    r.raise_for_status()
    job_id = r.json()["job_id"]
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        s = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=15).json()
        if s["status"] == "completed":
            break
        if s["status"] == "failed":
            raise RuntimeError(s.get("error", "unknown failure"))
        time.sleep(2)
    else:
        raise TimeoutError(f"job {job_id} did not finish in {timeout_s}s")
    return requests.get(f"{BASE}/jobs/{job_id}/result", headers=HEADERS, timeout=15).json()

For one ASIN that runs fine. For a daily competitor sweep across 100+, switch to the bulk endpoint.

The Daily Pipeline — Bulk Scrape to CSV

This is the full cron-able script. It reads a list of competitor ASINs from a CSV, submits them as a single bulk job, polls until the aggregate is done, and appends one row per ASIN to a long-format pricing history CSV.

import csv, os, pathlib, time, requests
from datetime import datetime, timezone

API_KEY = os.environ["LOGPOSE_API_KEY"]
BASE = "https://api.logposervices.com/api/v1"
HEADERS = {"X-API-Key": API_KEY}

COMPETITORS_FILE = pathlib.Path("competitors.csv")
HISTORY_FILE = pathlib.Path("price_history.csv")
FIELDS = ["asin", "observed_at", "price", "currency",
          "buybox_seller", "fulfillment", "availability"]


def load_targets() -> list[dict]:
    with COMPETITORS_FILE.open() as f:
        return [{"url": row["asin"], "pages": 1} for row in csv.DictReader(f)]


def submit_bulk(targets: list[dict]) -> str:
    r = requests.post(
        f"{BASE}/ecommerce/amazon/smart/bulk",
        headers=HEADERS,
        json={"targets": targets},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()["bulk_id"]


def wait_bulk(bulk_id: str, timeout_s: int = 1200) -> dict:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        s = requests.get(f"{BASE}/jobs/bulk/{bulk_id}", headers=HEADERS, timeout=15).json()
        if s["status"] in ("completed", "failed"):
            return s
        time.sleep(5)
    raise TimeoutError(f"bulk {bulk_id} did not finish in {timeout_s}s")


def fetch_child(job_id: str) -> dict | None:
    s = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=15).json()
    if s["status"] != "completed":
        return None
    return requests.get(f"{BASE}/jobs/{job_id}/result", headers=HEADERS, timeout=15).json()


def append_history(rows: list[dict]) -> None:
    write_header = not HISTORY_FILE.exists()
    with HISTORY_FILE.open("a", newline="") as f:
        w = csv.DictWriter(f, fieldnames=FIELDS)
        if write_header:
            w.writeheader()
        for row in rows:
            w.writerow({k: row.get(k) for k in FIELDS})


def main() -> None:
    targets = load_targets()
    print(f"submitting {len(targets)} ASINs")
    bulk_id = submit_bulk(targets)
    bulk = wait_bulk(bulk_id)

    now = datetime.now(timezone.utc).isoformat()
    rows = []
    for child_id in bulk["child_job_ids"]:
        data = fetch_child(child_id)
        if not data:
            continue
        rows.append({
            "asin": data.get("asin"),
            "observed_at": now,
            "price": data.get("price"),
            "currency": data.get("currency", "USD"),
            "buybox_seller": data.get("buybox_seller"),
            "fulfillment": data.get("fulfillment"),
            "availability": data.get("availability"),
        })

    append_history(rows)
    print(f"appended {len(rows)} rows to {HISTORY_FILE}")


if __name__ == "__main__":
    main()

Drop it in cron once a day:

30 6 * * * cd /opt/pricing && LOGPOSE_API_KEY=lp_xxx /usr/bin/python3 daily.py >> daily.log 2>&1

Always preview the cost first the first time you run it — the bulk estimate endpoint returns total credits and a per-target breakdown without charging anything:

est = requests.post(
    f"{BASE}/ecommerce/amazon/smart/bulk/estimate",
    headers=HEADERS,
    json={"targets": targets},
).json()
print(est["total_credits"])

The CSV grows by len(competitors) rows per day. For 100 ASINs over a year, that is 36,500 rows — comfortable for a single file. Above that, switch to Postgres.

Optional — Store in Postgres Instead

For multi-year history, dashboards, or anything where multiple people need to read concurrently, a CSV starts hurting. Swap append_history for a small Postgres writer:

import psycopg

DSN = os.environ["PG_DSN"]  # e.g. postgresql://user:pass@host/db

DDL = """
CREATE TABLE IF NOT EXISTS amazon_prices (
    asin TEXT NOT NULL,
    observed_at TIMESTAMPTZ NOT NULL,
    price NUMERIC,
    currency TEXT,
    buybox_seller TEXT,
    fulfillment TEXT,
    availability TEXT,
    PRIMARY KEY (asin, observed_at)
);
CREATE INDEX IF NOT EXISTS idx_amazon_prices_observed
    ON amazon_prices (observed_at DESC);
"""


def append_history_pg(rows: list[dict]) -> None:
    with psycopg.connect(DSN) as conn:
        conn.execute(DDL)
        with conn.cursor() as cur:
            cur.executemany(
                """INSERT INTO amazon_prices
                   (asin, observed_at, price, currency, buybox_seller, fulfillment, availability)
                   VALUES (%(asin)s, %(observed_at)s, %(price)s, %(currency)s,
                           %(buybox_seller)s, %(fulfillment)s, %(availability)s)
                   ON CONFLICT (asin, observed_at) DO NOTHING""",
                rows,
            )
        conn.commit()

Use Postgres as the source of truth and treat the CSV (and the Sheet below) as read-only views.

Push to Google Sheets

For non-engineers consuming the data, a Sheet is the right destination. The gspread library handles the API mechanics; you authenticate once with a Google service account JSON.

import gspread
from google.oauth2.service_account import Credentials

SHEET_ID = os.environ["SHEETS_ID"]
CREDS_FILE = "service-account.json"

SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]


def push_to_sheets(rows: list[dict]) -> None:
    creds = Credentials.from_service_account_file(CREDS_FILE, scopes=SCOPES)
    gc = gspread.authorize(creds)
    ws = gc.open_by_key(SHEET_ID).worksheet("daily")
    header = ["asin", "observed_at", "price", "currency",
              "buybox_seller", "fulfillment", "availability"]
    if not ws.row_values(1):
        ws.append_row(header)
    ws.append_rows(
        [[r.get(k, "") for k in header] for r in rows],
        value_input_option="USER_ENTERED",
    )

Call push_to_sheets(rows) at the end of main() alongside append_history(rows). Share the Sheet with the service account email (service-account@project.iam.gserviceaccount.com) so it has edit access.

A second worksheet with a pivot table — asin rows, last 7 days as columns, prices as values — gives the brand-manager view without writing a Python report. Refresh it manually or via Apps Script.

Feeding a Repricer

LogPose gives you the data; it does not change your prices. For actual repricing, pipe the daily output into one of:

  • Aura — algorithmic repricing for FBA sellers; consumes competitor data via SP-API.
  • BQool — rule-based repricer with BuyBox-aware logic.
  • RepricerExpress — long-tenured, well-documented.
  • Custom SP-API rules — write your own; the Listings/v2021-08-01 endpoints handle price updates.

The pattern: nightly cron writes the CSV + Sheet, your repricer reads its own competitor feed independently, and the two agree on macro trends because they look at the same listings. Test in sandbox before turning on production rules — automated repricing wars accelerate downward.

Scaling Beyond a Single ASIN

A few hundred ASINs at daily cadence works without effort. Things to know as the catalog grows:

  • Bulk endpoint concurrency. The bulk endpoint runs children concurrently on the server side. You do not need to thread the submitter.
  • Stagger schedules across many catalogs. If you run pipelines for 5 brands, do not schedule them all at 0 6 * * *. Spread across the early-morning window.
  • Always preview cost. Run bulk/estimate whenever the target list grows materially. It is free.
  • Cross-marketplace sweeps. If you also sell on eBay or Walmart, the equivalent endpoints exist; this guide stays on Amazon by design.

Common Mistakes

  • pages > 1 on /dp/ product pages. The pages parameter only matters for search and category URLs. On product pages, pages=1 is the only meaningful value.
  • Forgetting the locale. UK (amazon.co.uk), Germany (amazon.de) and other locales work — pass the full URL. Cross-locale price comparisons are a strong use case.
  • Bare ASINs are fine. {"url": "B0F2GYMC8H"} is auto-expanded to https://www.amazon.com/dp/B0F2GYMC8H.
  • Cloudflare 100s edge timeout. api.logposervices.com sits behind Cloudflare. A single scrape that stalls past 90 seconds returns 524 to the client even though the job keeps running server-side. Always poll the job ID, never depend on long-running connections.
  • Monitoring parent ASINs of variation listings. The price you scrape from a parent depends on which child is default-selected. Always target specific child ASINs.
  • Treating yesterday's price as the baseline. Compare against a rolling 30-day median, not a single previous data point, to separate flash sales from structural moves.

Get Started

  1. Sign up at logposervices.com and generate an API key from Tool → API Keys.
  2. export LOGPOSE_API_KEY=lp_xxxxxxx
  3. Drop your competitor ASINs into a competitors.csv (one column, header asin).
  4. Run the bulk pipeline above once interactively, confirm rows hit the CSV, then add the cron entry.

Related: monitor Amazon BuyBox changes, Amazon price tracker walkthrough, scrape Amazon prices in Python, monitor competitor pricing daily, bulk ASIN extraction, best Amazon scraper APIs in 2026.

External: gspread docs, Amazon SP-API docs, psycopg 3.

Frequently asked questions

How accurate is daily competitor pricing data scraped from Amazon?
For private-label and slow-moving products, daily is plenty — most prices move once or twice a week. For commoditized categories with active repricers, daily captures the trend line but misses intraday rotation. If your competitors are repricing every 5-15 minutes, a daily snapshot is the average of dozens of moves and will look noisier than it really is. Pair daily history with an hourly monitor on your top-revenue ASINs and you get the best of both views.
Why use Google Sheets instead of a real database?
Because the people who consume competitor pricing data — brand managers, category leads, owners — already live in spreadsheets. A Postgres table is great for engineers but useless to a non-technical decision-maker. The pattern most teams settle on is: Postgres as the source of truth, Google Sheets as the read view that gets refreshed daily. The gspread snippet later in this guide handles the Sheets side; pair it with the Postgres pattern for durability.
Can this pipeline auto-reprice my listings?
No. LogPose is the data layer — it gives you clean competitor prices and BuyBox holders on a schedule. Actually changing your own listing price requires Amazon's SP-API and either your own pricing logic or a dedicated repricer (Aura, BQool, RepricerExpress, Sellery). The split is intentional: the scraping side is hard to get right, so is the repricing side, and trying to do both in one tool usually means doing both badly. Feed competitor data from this pipeline into your repricer of choice.
What does it cost to track 100 ASINs daily?
That depends on the platform you use and what you actually want — raw scrape, history retention, webhook delivery, parser maintenance. The variables that drive cost are number of ASINs (100 is small), check frequency (daily is 100 calls; hourly is 2,400), and whether you want bulk concurrency. Most teams underestimate frequency cost more than per-call cost — over-polling stable listings is the biggest budget leak. Pick a cadence calibrated to how fast your category actually moves.
How do I detect a competitor's sale event vs a permanent price move?
Compare against the rolling 30-day median, not yesterday's price. A drop of 15% that lasts 24 hours and reverts is a flash sale. A drop of 8% that holds for a week is a structural move worth reacting to. The CSV schema in this guide stores observed_at on every row, which is exactly what you need to compute the rolling median in Pandas: df.groupby('asin').rolling('30D', on='observed_at')['price'].median().

Related posts

Tutorial

How to Monitor Amazon BuyBox Changes (and Get Alerted When You Lose It)

9 min read
Comparison

Best Amazon Scraper APIs in 2026 (Honest Comparison)

10 min read
Tutorial

How to Get Amazon Product Reviews via API

9 min read