len(offers) from /amazon-product-offers is not the number of sellers competing for an ASIN. The endpoint returns one row per OFFER: a seller running both FBA and FBM for the same listing, or selling it in two conditions, gets two rows for one seller. Dedupe the offers on (sellerId, deliveryDate, condition, fulfillmentType) first, then count the distinct sellerId values with a set, not the row count.
Key Takeaways
/amazon-product-offersreturns one row per offer, not one row per seller.- A seller can legitimately appear on 2+ rows: FBA and FBM for the same listing, or two conditions (New and Used).
- The API already drops exact-duplicate rows on
(sellerId, deliveryDate, condition, fulfillmentType), but a row that differs on any one of those fields is not a duplicate and stays. - Real seller count =
len({o['sellerId'] for o in deduped_offers}), neverlen(offers). - Amazon's own offer carries the literal
sellerId"Amazon" and counts as exactly one seller, in its own group.
Why does len(offers) overcount sellers?
/amazon-product-offers reads one page of Amazon's live offer listing for one ASIN and returns offers[], each row carrying sellerId, sellerName, condition, fulfillmentType (FBA or FBM), priceWithoutShipping, shippingPrice, totalPrice, deliveryDate and more. The response already deduplicates exact-duplicate rows on sellerId, deliveryDate, condition and fulfillmentType together, so a scrape retry that re-reads the same offer twice does not double it in the response you get. What survives that server-side dedupe is every row that differs on at least one of those four fields, and a single seller can produce several of those: the same business selling New via FBA and New via FBM, or New and Used - Like New, is one seller with two or more rows. len(offers) counts the rows, so it counts that one seller twice (or more). Field reference: Offer Listing API guide.
| Case | What happens | Effect on len(offers) |
|---|---|---|
| Legit 2nd row, different fulfillment | The same seller sells New via FBA and New via FBM: 2 rows, 1 seller | len(offers) over-counts by 1 per such seller |
| Legit 2nd row, different condition | The same seller lists New and Used - Like New: 2 rows, 1 seller | len(offers) over-counts by 1 per such seller |
| Exact-duplicate row | A scrape retry re-adds the identical (sellerId, deliveryDate, condition, fulfillmentType) row | the API already drops these; a stale cached copy from before a refresh may not |
| Amazon's own offer | sellerId is the literal "Amazon", fulfillmentType "FBA" | counts as exactly 1 seller like any other, never merged with a third party's FBA row |

The dedupe key and the real seller count
Dedupe on (sellerId, deliveryDate, condition, fulfillmentType) first (defensive: it also protects a consumer that caches pages or merges more than one read), then take the set of sellerId from the deduped rows. dedupe_offers() keeps the first row for each key and drops the rest; report() builds the seller set from the result and also lists which sellers kept more than one row after the dedupe, so you can see which sellers run multiple fulfillment types or conditions instead of losing that detail.
Install requests, set the key, run the check
pip install requests
export SELLERMAGNET_API_KEY="your-key-here"
python3 count_sellers.py
count_sellers.py: dedupe /amazon-product-offers and count distinct sellers for one ASIN
"""How many sellers actually compete for one ASIN: dedupe /amazon-product-offers before you count.
len(offers) counts OFFER ROWS, not sellers: one seller can carry more than one row (New FBA and New FBM,
or two conditions), so a naive seller count from len(offers) is too high. Dedupe each offer on
(sellerId, deliveryDate, condition, fulfillmentType) first - the API already drops exact duplicates of that
key, but a seller with a second row that differs on any one of those four fields survives - then take the
set of sellerId from the deduped rows for the seller count.
Helpers: sellermagnet.py and retry.py from https://sellermagnet-api.com/blog/amazon-product-api-python
"""
from retry import get_with_retry
from sellermagnet import ApiError, new_session
MARKETPLACE = "ATVPDKIKX0DER" # amazon.com
ASIN = "B0CL61F39H" # documented example pair for this marketplace
def dedupe_key(offer):
return (offer.get("sellerId"), offer.get("deliveryDate"), offer.get("condition"), offer.get("fulfillmentType"))
def dedupe_offers(offers):
"""Drop exact-duplicate rows (same seller, same delivery date, same condition, same fulfillment type).
A seller with a second row that differs on any one of those four fields is NOT a duplicate and stays."""
seen, kept = set(), []
for offer in offers:
key = dedupe_key(offer)
if key in seen:
continue
seen.add(key)
kept.append(offer)
return kept
def report(asin, offers):
raw = len(offers)
deduped = dedupe_offers(offers)
sellers = {o.get("sellerId") for o in deduped}
by_seller = {}
for offer in deduped:
by_seller.setdefault(offer.get("sellerId"), []).append(offer.get("fulfillmentType"))
multi_row = {seller: rows for seller, rows in by_seller.items() if len(rows) > 1}
print(f"asin {asin}: {raw} offer row(s), {len(deduped)} after dedupe, {len(sellers)} distinct seller(s)")
for seller, rows in sorted(multi_row.items()):
print(f" {seller}: {len(rows)} rows ({', '.join(rows)}) - one seller, counted once")
return len(sellers)
def main():
session = new_session()
try:
offers = get_with_retry(session, "amazon-product-offers", asin=ASIN, marketplaceId=MARKETPLACE)["offers"]
except ApiError as err:
raise SystemExit(f"stopped: {err}")
report(ASIN, offers)
if __name__ == "__main__":
main()
Output of count_sellers.py on amazon.com (illustrative data)
asin B0CL61F39H: 8 offer row(s), 7 after dedupe, 5 distinct seller(s)
A1DUPSELLER: 2 rows (FBA, FBM) - one seller, counted once
A2TWOCOND: 2 rows (FBM, FBM) - one seller, counted once
Eight rows came back; one was an exact-duplicate row the dedupe key removed, leaving seven; two of those seven belong to sellers with a second row (one runs FBA and FBM, the other lists two conditions), so the real seller count is five, not eight. The trimmed response below shows the shape: the FBA/FBM pair for one seller and the exact-duplicate pair for another, side by side.
Trimmed /amazon-product-offers response: a seller with FBA and FBM rows, plus an exact-duplicate pair (illustrative values)
{
"data": {
"asin": "B0CL61F39H",
"offers": [
{
"condition": "New",
"deliveryDate": "2026-10-29",
"fulfillmentType": "FBA",
"sellerId": "A1DUPSELLER",
"totalPrice": 41.5
},
{
"condition": "New",
"deliveryDate": "2026-11-02",
"fulfillmentType": "FBM",
"sellerId": "A1DUPSELLER",
"totalPrice": 45.98
},
{
"condition": "New",
"deliveryDate": "2026-10-30",
"fulfillmentType": "FBM",
"sellerId": "A3PLAINSELLER",
"totalPrice": 44.0
},
{
"condition": "New",
"deliveryDate": "2026-10-30",
"fulfillmentType": "FBM",
"sellerId": "A3PLAINSELLER",
"totalPrice": 44.0
}
]
},
"success": true
}

What this means for seller-count tooling
- A hijacker/new-seller alert that treats
len(offers)as "competitor count" will over-alert on multi-channel sellers; the hijacker alert build already dedupes bysellerIdfor this reason. - A Buy-Box-lost alert that compares
buyBox.sellerIdagainst your own ID is unaffected: it reads one field, not the row count (see the Buy Box lost alert). - A "how many sellers" dashboard metric should store the deduped seller count, not
len(offers), or it will drift upward as multi-channel sellers add fulfillment types over time.
Do not store len(offers) as the seller count
A job that writes len(offers) into a "number of sellers" column overcounts every listing where a seller runs more than one fulfillment type or condition, and the error grows as those sellers add more offer rows. Store the deduped seller count instead.
Cost and error handling
One seller-count check is one /amazon-product-offers call, 1 credit, regardless of how many rows or sellers come back. /amazon-product-offers takes any of the 23 supported marketplace IDs (unlike statistics, estimated sales and the ASIN/EAN converter, gated to 11): an unknown marketplaceId is a free gate 400 "Invalid marketplaceId", an unknown ASIN is a billed 404 "Invalid product" (never retried), and a 500 without Retry-After is refunded and retried once by get_with_retry from the Python quickstart. A listing with no offers at all answers 200 with offers: [], zero sellers, not an error.
What the data cannot show
- Page one of the offer listing only: the response is the pinned offer plus the offers Amazon loads first on that page, with no paging parameter.
- Stock level:
inventoryis the add-to-cart maximum for that offer (often capped at 1), not the seller's real stock. - Coupons or promotions: offer rows have no coupon field.
- A snapshot, not a stream: one call is one moment; sellers can list or delist between checks.
Frequently Asked Questions
Does /amazon-product-offers return one row per seller?
No. It returns one row per OFFER. A seller selling both New-FBA and New-FBM, or two conditions, gets 2 rows for 1 seller, so len(offers) is the number of offer rows, not the number of sellers.
How do I count sellers correctly?
Dedupe offers on (sellerId, deliveryDate, condition, fulfillmentType), then take the length of the set of sellerId values from the deduped rows: len({o['sellerId'] for o in deduped}).
Does the API already remove duplicate rows?
It dedupes exact duplicates of the same 4-field key before returning the response, but a row that differs on even one of those fields (a second condition or fulfillment type) is not a duplicate and stays, which is exactly what makes len(offers) unsafe as a seller count.
Does Amazon's own offer count as a seller?
Yes, exactly one: sellerId is the literal "Amazon" and fulfillmentType "FBA", and it is its own group, never merged with a third-party seller's FBA row.
What does one seller-count check cost?
1 credit: a single /amazon-product-offers call, regardless of how many rows or sellers come back in the response.
Bottom line: /amazon-product-offers gives you offer rows, not sellers. Dedupe on (sellerId, deliveryDate, condition, fulfillmentType), then count the distinct sellerId values, and the number you report will stop drifting upward every time a seller adds a second fulfillment type or condition.