How to Track Amazon Competitor Prices Daily (Export to CSV and Google Sheets)
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:
- Is the floor moving? Over 30 days, is the median competitor price trending down (your margin is compressing) or stable?
- 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.
- 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,brandpriceandcurrencybuybox_seller— the "Sold by" value on the listingfulfillment— FBA, FBM, or Amazon-fulfilledavailability—In Stock,Only 3 left,Currently unavailableother_sellers— an array of additional offers with seller name, price, and fulfillmentrating,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/validateCaptchashuts 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-01endpoints 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/estimatewhenever 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 > 1on/dp/product pages. Thepagesparameter only matters for search and category URLs. On product pages,pages=1is 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 tohttps://www.amazon.com/dp/B0F2GYMC8H. - Cloudflare 100s edge timeout.
api.logposervices.comsits 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
- Sign up at logposervices.com and generate an API key from Tool → API Keys.
export LOGPOSE_API_KEY=lp_xxxxxxx- Drop your competitor ASINs into a
competitors.csv(one column, headerasin). - 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.