← Back to blogTutorial

Track Amazon BSR (Best Sellers Rank) Over Time

· 8 min read

You are researching products to launch — or watching one you already sell. Best Sellers Rank (BSR) is Amazon's single best public signal of how a product is actually moving relative to its category. A drop in BSR is an early-warning that demand is shifting; a steady BSR over six months says the product is durable.

Amazon does not publish BSR history. You build it yourself by scraping the rank field daily and persisting it. This guide covers the DIY scrape, where the data lives in the HTML, and how to schedule the pipeline reliably.

Why BSR Tracking Is Different From Price Tracking

Three things make BSR awkward to scrape compared to price:

The number is buried in the page. BSR lives in a specific section of the product page that Amazon changes the location of routinely. Sometimes inside a <table id="productDetails_detailBullets_sections1">, sometimes in a <div id="detailBulletsWrapper_feature_div">, sometimes under "Product information" on mobile pages. Your selector needs to fall back across these locations.

Sub-category BSR matters more than parent. A product might have BSR #142 in Electronics (the parent, with a huge pool) and BSR #3 in "Wireless Earbud Headphones" (a sub-category that shoppers actually browse). For research, the sub-category number is the useful one.

It is a snapshot, not a metric. Unlike price (a single field), BSR is a (rank, category) tuple. A product can be ranked in 3-5 categories simultaneously. Your data model needs to handle a list of rank entries per scrape, not a scalar.

Update timing is uncertain. Amazon refreshes BSR at unspecified intervals. The number you scrape at 9am is up to a few hours stale. Daily granularity is the practical floor.

The DIY Approach

import re
import requests
from bs4 import BeautifulSoup

HEADERS = {
    "User-Agent": "Mozilla/5.0 ... Chrome/127.0.0.0 ...",
    "Accept-Language": "en-US,en;q=0.9",
}

BSR_RE = re.compile(r"#([\d,]+)\s+in\s+([A-Za-z0-9 &']+)")


def parse_bsr(html: str) -> list[dict]:
    soup = BeautifulSoup(html, "html.parser")
    text = soup.get_text(" ", strip=True)
    out = []
    for match in BSR_RE.finditer(text):
        rank = int(match.group(1).replace(",", ""))
        category = match.group(2).strip()
        out.append({"rank": rank, "category": category})
    return out


def scrape_bsr(asin: str) -> list[dict]:
    url = f"https://www.amazon.com/dp/{asin}"
    r = requests.get(url, headers=HEADERS, timeout=15)
    if r.status_code != 200 or "validateCaptcha" in r.url:
        return []
    return parse_bsr(r.text)


if __name__ == "__main__":
    for entry in scrape_bsr("B09V3KXJPB"):
        print(entry)
    # Example output:
    # {"rank": 142, "category": "Electronics"}
    # {"rank": 3, "category": "Wireless Earbud Headphones"}

The regex above catches the common "#142 in Electronics" pattern. It is deliberately permissive — Amazon's category names contain spaces, ampersands, and apostrophes.

Real Limitations

  • Selector instability. The BSR HTML location changes every few months. The text-regex approach above is more durable than a CSS selector, but still requires periodic validation.
  • CAPTCHA threshold same as product scraping. ~5-30 requests per residential IP before challenges.
  • Mobile vs desktop pages differ. Your User-Agent must look like a desktop browser to get the full product-details section.
  • Sub-category granularity varies. Some products show five sub-category ranks; some show only the parent. You cannot force Amazon to expose all of them.

A failure mode that costs you a week of data:

# Yesterday's parser worked fine.
# Today Amazon shipped a new desktop layout where BSR lives inside a new container.
# The regex still finds "#142 in Electronics" in the *header* (a related-products carousel)
# instead of the actual product's BSR. You get garbage data with no error.

Always sanity-check: BSR for a known product should change slowly. If yesterday's BSR was 142 and today's is 8,901, the parser is probably reading a different field.

Scaling Beyond a One-Product Script

For a research pipeline tracking dozens or hundreds of ASINs:

Persist all category ranks per scrape. Do not pick "the" rank — store all of them. A bsr_history table with (asin, observed_at, category, rank) is right.

Run daily, not hourly. BSR updates are noisier than price. Hourly granularity adds noise without signal for most use cases.

Compute relative changes, not absolute. A move from rank 3 to rank 8 in a sub-category is a 167% rise. The absolute number alone does not tell you the move size.

Normalize against category size. Rank 142 in Electronics is excellent. Rank 142 in Books is invisible. Track a category_pool_estimate alongside the rank if you compare across categories.

For the scraping layer, since BSR is not a built-in monitor metric in any platform, the cleanest pattern is to schedule a periodic full-page scrape and parse BSR from the result yourself. The LogPose smart endpoint returns the structured product data including the raw fields BSR lives in:

import os
import time
import json
import requests
from datetime import datetime, timezone
from pathlib import Path

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


def scrape(url: str) -> dict:
    submit = requests.get(
        f"{BASE}/ecommerce/amazon/smart",
        params={"url": url, "pages": 1},
        headers=HEADERS, timeout=30,
    ).json()
    job_id = submit["job_id"]
    while True:
        s = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=15).json()
        if s["status"] in ("completed", "failed"):
            break
        time.sleep(2)
    return requests.get(f"{BASE}/jobs/{job_id}/result", headers=HEADERS, timeout=15).json()


asins = ["B09V3KXJPB", "B0BN93GFMN", "B0BTFKR638"]
out_path = Path(f"bsr_{datetime.now(timezone.utc).date()}.jsonl")

with out_path.open("a") as f:
    for asin in asins:
        data = scrape(f"https://www.amazon.com/dp/{asin}")
        ranks = data.get("best_sellers_rank") or data.get("bsr") or []
        for entry in ranks:
            row = {
                "asin": asin,
                "observed_at": datetime.now(timezone.utc).isoformat(),
                **entry,
            }
            f.write(json.dumps(row) + "\n")

Schedule this script daily via cron, systemd timer, or GitHub Actions. Append to a JSONL file or pipe into Postgres. Plot rank-over-time per (asin, category) in the tool of your choice.

The LogPose monitor endpoint currently tracks price, rating, and availability as scalar metrics on Amazon. BSR is multi-valued (per-category) so it lives outside the scalar-metric model — schedule your own scrape until that changes.

Common Mistakes

  • Treating BSR as a single number. It is a list of (category, rank) pairs. Store the list.
  • Comparing BSR across categories. Rank 142 in Electronics and rank 142 in Books are completely different signals.
  • Storing only the latest value. The whole point is the trend. Append, do not update.
  • Scraping hourly. BSR updates lag and noise dominates; daily is sharper.
  • Trusting a sudden 10× rank change. Verify the parser is still reading the right HTML region before alerting on dramatic moves.

The Landscape

For Amazon-specific BSR tracking:

  • Keepa — strongest BSR history product on the market; charts BSR alongside price for any ASIN.
  • JungleScout / Helium 10 — sales-estimate tools that derive volume from BSR; useful for product research, not raw rank tracking.
  • AMZScout — similar product research; lighter on history, lower price point.
  • DIY + Bright Data — full control, you own the data and the schema.
  • LogPosesmart endpoint returns product details with BSR in the raw fields; useful when BSR is one of several signals you track across multiple sites.

If BSR-only is your need, Keepa is usually the simplest. If you are combining BSR with reviews, prices, and competitor analysis across platforms, a general-purpose API plus your own storage gives you flexibility Keepa does not.

Get Started

  1. Sign up at logposervices.com, generate an API key.
  2. Build the daily-scrape script above and put it on cron.
  3. After a week of data, plot rank-over-time per ASIN in your sub-category of interest.

Related: scrape Amazon prices in Python, bulk ASIN extraction, Amazon reviews API.

External: Amazon BSR explainer (Helium 10), Python requests.

Frequently asked questions

What is a good Amazon BSR?
BSR is relative to the category. In a major parent category (Electronics, Books, Toys), anything under 1,000 is excellent. In a sub-category, the absolute number is lower because the pool is smaller. Compare against similar products, not across categories.
How often does Amazon update BSR?
Amazon refreshes BSR roughly every hour for active categories. For very low-volume products, updates can lag 24 hours. The rank you see on a page is up to a few hours old.
Is BSR the same as sales volume?
No. BSR is a relative ranking, not a sales count. A product at BSR 100 does not sell 100 units — it sells more units than the product at BSR 200 in the same category. Sales-volume estimates derived from BSR (JungleScout, Helium 10) are approximations, not measurements.
Can I track BSR via the Amazon Product Advertising API?
PA API exposes a SalesRank field on some categories but not all, and not consistently across product types. For reliable BSR tracking, scraping the product page is more dependable.
Why is my product's BSR rising and falling without my sales changing?
BSR is recalculated based on rolling-window sales velocity across the whole category. A spike in a competitor's sales pushes your BSR up (rank rises in number = position worsens) even if your own sales are constant.

Related posts

Tutorial

How to Get Amazon Product Reviews via API

9 min read
Tutorial

Extract Amazon ASIN Data in Bulk

9 min read
Strategy

Monitor Amazon Competitor Pricing Daily

9 min read