Amazon Share of Shelf Tracking by Category, Step by Step
Measure what share of an Amazon category's top 100 your brand owns versus each rival, and get alerted the moment one of your ASINs drops off the chart.

Amazon share of shelf tracking measures what percentage of a category's ranked top 100 belongs to your brand versus each competitor. The Amazon Best Sellers Scraper returns all 100 ranked positions for any category or browse node with rank, ASIN, title, price, rating and review count, so a brand ops team or agency analyst can group those rows by brand, publish a weekly share report, and fire an alert when one of their ASINs falls off the chart entirely.
Why track Amazon share of shelf by category
Share of shelf is the defensive metric a brand owner reports on. It answers a question that a keyword rank tracker cannot: of the finite ranked real estate Amazon publishes for your category, how much is yours this week, how much was yours last week, and who took the difference. Amazon has reported for several years running that third-party sellers account for roughly 60% of paid units sold across its stores (Amazon investor relations), which means the shelf you are defending is crowded with sellers who can undercut, relaunch and re-list far faster than a traditional retail planogram ever changed.
The Best Sellers chart works as a shelf proxy because Amazon computes it from actual order volume and republishes it roughly hourly, per browse node, down into narrow subcategories. It is not a search result skewed by your query, session or ad spend. Every brand is measured on the same 100 slots.
For an agency analyst the deliverable is concrete: brands ranked by percentage of top 100 held, a delta column against last week, and the ASINs that moved. For a brand ops lead the trigger is simpler — an ASIN that was rank 34 last Monday and is absent this Monday is a conversation with the category manager, not a dashboard tile.
How does this compare to the alternatives?
There are three realistic ways to produce a recurring share-of-shelf report. Writing your own fetcher is fine for one chart and painful for forty: Amazon serves the back half of each chart through a follow-up call whose tokens change with every render, and a throttled response often returns HTTP 200 with an empty grid rather than an honest error. A generic scraping API hands you raw HTML and leaves rank extraction, currency parsing and soft-block detection to you. A purpose-built Actor returns the ranked rows already typed.
| Approach | Cost model | Reliability | Setup time | Maintenance |
|---|---|---|---|---|
| DIY Python fetcher | Your own infra plus proxy contract | Silent empty charts; rank drift when the grid lazy-loads | 2-5 days per marketplace | Breaks whenever the chart markup or token flow changes |
| Generic scraping API | Subscription, billed per request | Delivers HTML, not ranks; blocks still surface as 200s | 1-2 days of parser work | You own every parser change |
| Thirdwatch Best Sellers Actor | Transparent pay-per-result | Rank read from Amazon's own rank metadata; soft-block detection and retry built in | Under an hour | Handled upstream |
How to track Amazon share of shelf by category in 4 steps
Which categories and browse nodes define my shelf?
Define the shelf before you write any code, because the denominator is the whole report. A department slug like kitchen measures you against every kitchen product on Amazon, which is usually too broad to be actionable. The slug/nodeId form points at a specific browse node, and each node publishes its own independent top 100 — that is normally the shelf your category manager actually argues about.
# Each entry becomes one chart. Mix department slugs, browse nodes and full URLs.
CATEGORIES = [
"kitchen", # whole department, context view
"kitchen/289913", # a single browse node, the real shelf
"hpc", # health & personal care
"https://www.amazon.com/gp/bestsellers/appliances", # full URL also works
]
# Maintain this once per category. Keys are lowercase brand tokens.
OUR_BRANDS = {"instant", "instant pot"}
RIVAL_BRANDS = {"ninja", "cosori", "keurig", "cuisinart", "hamilton beach"}How do I pull the full top 100 for each category?
Request the maximum for every chart, because share of shelf is meaningless on a partial denominator. Set maxResults to 100, keep listType on bestSellers, and pick the country marketplace you report on. The Actor returns one row per ranked position, tagged with category_slug, category_node_id and rank.
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run_input = {
"categories": CATEGORIES,
"listType": "bestSellers",
"country": "us",
"maxResults": 100,
"proxyConfiguration": {"useApifyProxy": True},
}
run = client.actor("thirdwatch/amazon-bestsellers-scraper").call(run_input=run_input)
rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())
print(len(rows), "ranked rows across", len({r["category_url"] for r in rows}), "charts")How do I turn 100 ranked rows into a share-of-shelf number?
Group the rows by brand and divide. Report two numbers side by side: flat share, which is simply how many of the 100 slots a brand holds, and rank-weighted share, which credits position one far more than position one hundred. Flat share is the headline; rank-weighted share is what catches a rival who holds the same slot count but has moved all of them into the top twenty.
from collections import defaultdict
def brand_of(row):
"""Media charts carry by_line; hard-goods charts do not, so fall back to the title."""
if row.get("by_line"):
return row["by_line"].strip().lower()
head = " ".join(row["title"].split()[:2]).lower().strip(",-")
for known in OUR_BRANDS | RIVAL_BRANDS:
if head.startswith(known):
return known
return row["title"].split()[0].lower().strip(",-")
def share_of_shelf(rows, slug, node_id=None):
chart = [
r for r in rows
if r["category_slug"] == slug and r["category_node_id"] == node_id
]
slots = len(chart)
flat, weighted = defaultdict(int), defaultdict(float)
for r in chart:
b = brand_of(r)
flat[b] += 1
weighted[b] += 1 / r["rank"] # rank 1 is worth 100x rank 100
wsum = sum(weighted.values()) or 1
return sorted(
(
{
"brand": b,
"slots": flat[b],
"flat_share": round(flat[b] / slots, 4),
"weighted_share": round(weighted[b] / wsum, 4),
}
for b in flat
),
key=lambda d: -d["weighted_share"],
)
for row in share_of_shelf(rows, "kitchen", "289913")[:10]:
print(row)How do I detect when one of my ASINs drops off the chart?
Store each run as a snapshot keyed by asin and diff the next run against it. Two events matter: an ASIN that was present and is now absent, which is a full displacement, and an ASIN that slipped more than a threshold number of ranks, which is an early warning. Keeping price and rating in the snapshot means the alert can carry a likely cause rather than just a rank number.
import json, pathlib
snapshot = pathlib.Path("shelf_kitchen_289913.json")
previous = json.loads(snapshot.read_text()) if snapshot.exists() else {}
current = {
r["asin"]: {
"rank": r["rank"],
"title": r["title"],
"price": r["price"],
"rating": r["rating"],
"reviews_count": r["reviews_count"],
"scraped_at": r["scraped_at"],
}
for r in rows
if r["category_slug"] == "kitchen" and r["category_node_id"] == "289913"
}
WATCHLIST = {"B08PJRQXFR", "B0936HFBK7", "B07W55DDFB"}
for asin in WATCHLIST:
was, now = previous.get(asin), current.get(asin)
if was and not now:
print(f"DISPLACED {asin} — was rank {was['rank']}, now off the top 100")
elif was and now and now["rank"] - was["rank"] >= 10:
print(f"SLIPPING {asin} — {was['rank']} to {now['rank']}, price {was['price']} to {now['price']}")
elif not was and now:
print(f"ENTERED {asin} — new at rank {now['rank']}")
snapshot.write_text(json.dumps(current, indent=2))How do I make this a recurring weekly report?
Pin the run to a fixed weekday and hour, then treat every run as an immutable snapshot. Because Amazon recomputes the chart roughly hourly, comparing Monday 09:00 to Monday 09:00 is the only comparison that carries a week-over-week meaning. Use an Apify schedule to trigger the run, or call the API from your own scheduler.
curl -X POST "https://api.apify.com/v2/acts/thirdwatch~amazon-bestsellers-scraper/runs?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"categories": ["kitchen", "kitchen/289913", "hpc"],
"listType": "bestSellers",
"country": "us",
"maxResults": 100
}'Every row carries scraped_at, category_url and marketplace, so appending runs into one warehouse table gives you a time series with no extra bookkeeping.
Sample output
Each dataset item is one ranked position on one chart at one point in time. For share of shelf the load-bearing fields are rank, asin, category_slug, category_node_id and scraped_at; price, rating and reviews_count supply the context that turns a rank change into an explanation. Note that by_line and product_format are empty on hard-goods charts, which is why the brand function above falls back to the title.
[
{
"rank": 3,
"asin": "B08PJRQXFR",
"title": "Ninja AF101 Air Fryer, 4 Quart Capacity, Black/Grey",
"by_line": "",
"product_format": "",
"price": 89.99,
"price_text": "$89.99",
"price_type": "list",
"offers_count": null,
"currency": "USD",
"currency_symbol": "$",
"rating": 4.7,
"rating_text": "4.7 out of 5 stars",
"reviews_count": 118204,
"reviews_count_text": "118,204",
"product_url": "https://www.amazon.com/dp/B08PJRQXFR",
"image_url": "https://images-na.ssl-images-amazon.com/images/I/71+8uTMDRFL._AC_UL300_SR300,200_.jpg",
"reviews_url": "https://www.amazon.com/product-reviews/B08PJRQXFR",
"list_type": "bestSellers",
"category_slug": "kitchen",
"category_node_id": "289913",
"category_url": "https://www.amazon.com/gp/bestsellers/kitchen/289913",
"page": 1,
"marketplace": "www.amazon.com",
"country": "us",
"source_query": "kitchen/289913",
"scraped_at": "2026-07-27T09:00:11+00:00"
},
{
"rank": 47,
"asin": "B0936HFBK7",
"title": "COSORI Air Fryer 5 QT, 9-in-1 Compact Airfryer",
"by_line": "",
"product_format": "",
"price": null,
"price_text": "3 offers from $79.99",
"price_type": "from",
"offers_count": 3,
"currency": "USD",
"currency_symbol": "$",
"rating": 4.6,
"rating_text": "4.6 out of 5 stars",
"reviews_count": 41870,
"reviews_count_text": "41,870",
"product_url": "https://www.amazon.com/dp/B0936HFBK7",
"image_url": "https://images-na.ssl-images-amazon.com/images/I/71pMPXGCRXL._AC_UL300_SR300,200_.jpg",
"reviews_url": "https://www.amazon.com/product-reviews/B0936HFBK7",
"list_type": "bestSellers",
"category_slug": "kitchen",
"category_node_id": "289913",
"category_url": "https://www.amazon.com/gp/bestsellers/kitchen/289913",
"page": 1,
"marketplace": "www.amazon.com",
"country": "us",
"source_query": "kitchen/289913",
"scraped_at": "2026-07-27T09:00:11+00:00"
}
]Common pitfalls
The denominator caps at 100. Amazon publishes only the top 100 positions per chart, so a brand sitting at rank 130 registers as zero share, not as a small share. Go deeper by passing browse nodes rather than departments — each node has its own top 100.
Brand attribution is your job on hard goods. by_line and product_format populate on Books, Music and Video charts at near-complete rates, but hard-goods charts carry no brand line at all. Budget an hour to build a brand alias table per category and re-check it when a rival rebrands.
Not every row has a price. Where Amazon shows "N offers from $X", price holds that lowest-offer figure with price_type set to from; subscriptions and some marketplace items show no price and arrive as price: null. Filter on price_type before averaging.
Charts move hourly. Two runs minutes apart can legitimately disagree. Fix the schedule.
Slugs are marketplace-specific. amazon.de uses computers where amazon.com uses electronics; an unknown slug yields a warning and zero rows, not a failed run. Paste the full chart URL when unsure. The Actor detects truncated or empty responses instead of trusting a 200 status, retries with a fresh session, and keeps whatever ranks it collected.
Related use cases
- Track Amazon New Releases for launch detection — catch a rival SKU entering your category before it accumulates reviews.
- Compare Amazon Best Sellers across marketplaces — run the same share-of-shelf math on the UK, German, Indian and Japanese charts.
- Build an auto-updating Amazon affiliate top list — the content-side use of the same ranked chart.
- Build an Amazon Best Sellers API feed — expose your snapshots as a cached JSON endpoint for internal dashboards.
- Guide to scraping e-commerce data — the category pillar covering marketplaces, pricing and catalogue extraction.
- Amazon Best Sellers Scraper — inputs, outputs and marketplace coverage.
- All Thirdwatch guides — the full library.
Frequently asked questions
What is share of shelf on Amazon?
Share of shelf on Amazon is the percentage of a category's ranked chart positions held by one brand. Because Amazon publishes a top 100 per browse node, you compute it by grouping those 100 rows by brand and dividing each brand's row count by 100.
How often should I re-run a share of shelf report?
Weekly is the right default for reporting and daily for displacement alerts. Amazon recomputes Best Sellers roughly hourly, so hourly runs mostly capture noise. A fixed weekday and hour makes week-over-week comparisons meaningful.
Do I need a list of ASINs to track share of shelf?
No. Share of shelf is a census measure: you pull the whole top 100 for a category and group it by brand. An ASIN watchlist is only needed for the displacement alert, where you check whether specific ASINs of yours are still on the chart.
Can I measure share of shelf below the top 100?
Not directly, because Amazon only publishes 100 ranks per chart. The workaround is to run the specific browse nodes your products sit in. Each subcategory node has its own independent top 100, which is usually a tighter definition of your shelf anyway.
How do I get the brand for each ranked product?
Media charts populate a by_line field with the author or artist. Hard-goods charts do not carry a brand line, so you derive the brand from the leading tokens of the title and map it against a brand alias table you maintain once per category.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.