Home / Blog / Amazon Offers API: Why len(offers) Is Not the Seller Count

Amazon Offers API: Why len(offers) Is Not the Seller Count

/amazon-product-offers returns one row per offer, not one row per seller: a seller selling the same ASIN via FBA and FBM, or in two conditions, gets two rows. Dedupe offers on sellerId, deliveryDate, condition and fulfillmentType, then count the distinct sellerId values, to get the real seller count for a listing.

October 26, 2026
6 min read
SellerMagnet Team
Share & Bookmark
Terminal window with the Python function that dedupes Amazon offer rows before counting sellers

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-offers returns 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}), never len(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.

Four ways a listing's offers[] can disagree with its real seller count (October 2026).
CaseWhat happensEffect on len(offers)
Legit 2nd row, different fulfillmentThe same seller sells New via FBA and New via FBM: 2 rows, 1 sellerlen(offers) over-counts by 1 per such seller
Legit 2nd row, different conditionThe same seller lists New and Used - Like New: 2 rows, 1 sellerlen(offers) over-counts by 1 per such seller
Exact-duplicate rowA scrape retry re-adds the identical (sellerId, deliveryDate, condition, fulfillmentType) rowthe API already drops these; a stale cached copy from before a refresh may not
Amazon's own offersellerId is the literal "Amazon", fulfillmentType "FBA"counts as exactly 1 seller like any other, never merged with a third party's FBA row
Table of four cases where counting offer rows gives the wrong seller count
Two of these cases are the same seller appearing twice; only an exact-duplicate row is caught by the API itself.

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
}
Flow diagram: offers array goes through the dedupe key, then a set of sellerId, to the real seller count
8 rows in, 7 after the dedupe key removes one exact duplicate, 5 distinct sellers after the set (illustrative run).

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 by sellerId for this reason.
  • A Buy-Box-lost alert that compares buyBox.sellerId against 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: inventory is 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.

Ready to Extract Amazon Data at Scale?

Start building with SellerMagnet API today. Real-time product data, competitive pricing, and review analytics at your fingertips.

150 free API credits • No credit card required • Cancel anytime