kestrel actors docs

kestrel actorsGuides

How to scrape eBay listings, and why sold listings are not available

Half the demand for eBay scraping is for sold and completed listings, and that view is behind a sign-in gate that no proxy, fingerprint or headless browser gets past. Here is the evidence, the honest substitute that answers most of the same question, and the parsing traps on the pages that are open.

Sold listings: the evidence

eBay's completed and sold view — LH_Sold=1&LH_Complete=1 — answers a logged-out client with a 302 to the sign-in gate: https://signin.ebay.<tld>/ws/eBayISAPI.dll?SignIn&siteid=<n>&ru=<the sold URL>&sgfl=srch. The sgfl=srch marker is eBay's own "sign-in gate for search".

The test, on 2026-08-29, was deliberately exhaustive:

Zero successes in roughly 115 attempts. No route, no country, no fingerprint, no session warm-up.

A headless browser does not change the answer, because the block is an HTTP redirect rather than a JavaScript challenge — the browser follows it to the same login page. Only a signed-in account cookie passes.

There is a market signal here worth reading: one widely-used sold-listings actor on the Apify Store logged 20,901 failed runs out of 117,071 in thirty days — a 17.9% failure rate. That is what building on an account-gated path looks like in production, quite apart from what it does to the accounts involved.

What to use instead: the demand columns

Most people asking for sold listings want to answer one question — what does this actually sell for — and there is a public column that speaks to it directly.

units_sold is the number of units a live listing has already sold at its current price. eBay prints it on the public results page. Combined with three more public numbers it gets you most of the way:

A price that 40 people have paid is a price. A price nobody has paid, with 200 watchers, is an ask. That distinction is what sold-listings data is usually bought for, and it survives the gate.

Working code

from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/ebay-search-scraper").call(run_input={
    "queries": ["mechanical keyboard"],
    "pages": 2,               # 60 listings per page
    "minPrice": 30, "maxPrice": 200,   # filters before billing
    "domain": "com",
})
sold = []
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["type"] == "item" and row["units_sold"]:
        sold.append(row)
sold.sort(key=lambda r: -r["units_sold"])
for r in sold[:10]:
    print(r["units_sold"], "sold @", r["price"], r["currency"], "|", r["watchers"], "watching |", r["title"][:60])

The price filters run before billing, so a "keyboards between 30 and 200" run pays only for rows in the band. That is the cheapest way to study a price segment rather than a whole category.

Three parsing traps on the pages that are open

If you are writing your own parser rather than buying one, these will cost you an afternoon each:

1. Sixty-two cards, sixty listings. Every results page renders two extra cards inside an aria-hidden clipped container, titled "Shop on eBay" and linking to a placeholder item. They look like listings to a selector that matches on the card class. Count 62, ship 60.

2. A zero-result search still renders about 21 cards. They are related items, not matches. "We got rows" is not the same as "the search matched", so a job that keys on row count will report a healthy run for a query that found nothing.

3. Prices are geolocated to the egress IP. The currency comes from the price itself, never from the marketplace domain, because a com run through a European exit can return rows priced in euros. Read the currency per row, and pin your proxy country if you are comparing across days.

Also worth knowing: fetching eBay's home page first usually 403s but sets the Akamai cookies the next request needs, and active listing pages want a Chrome-shaped TLS fingerprint — plain HTTP/2 and a Firefox profile were both refused ten times out of ten.

What the rows carry

Item id, title, canonical URL, the query and exact search URL, page and position in eBay's own order, marketplace, price and the printed price string with currency, the struck-through list price with discount percentage, shipping cost and free-delivery flag, condition, buying format, bids and time left, best-offer flag, units sold, watchers, returns line, seller username with feedback percentage and score (expanded from eBay's 40.6K shorthand to 40600), item location, image, and the catalogue star rating where the listing is matched to a product.

Item-page rows add availability text, breadcrumbs, leaf category, the seller's item specifics table and the store URL.

The honest limitations

Run it

eBay Search Scraper — $0.003 per delivered listing row, with status rows, empty searches and filtered listings free, across 18 marketplaces. Related: tracking competitor prices daily with n8n, and which review sites can be scraped without a browser for the same style of evidence on review sources.

FAQ

Why can't a headless browser reach eBay sold listings?

Because the block is an HTTP 302 to signin.ebay.<tld>, not a JavaScript challenge. A browser follows the redirect to the same login page a plain client gets. Only an authenticated account cookie passes, which means renting or burning eBay accounts.

What is the honest substitute for sold prices?

The units_sold column on live listings: how many units that listing has already sold at its current price, published by eBay on the public results page. For pricing decisions it answers most of the same question — has anyone actually paid this — with data that is genuinely available. Watchers, bid counts and time left fill in the demand picture.

How many listings does one eBay page hold?

Sixty, which is eBay's own ceiling. Note that the page renders 62 cards: the extra two are decoys in an aria-hidden container titled "Shop on eBay". eBay also stops paginating any single search at roughly 2,000 items however many it reports as the result total.

Is there a sponsored column?

Not a useful one. eBay prints the Sponsored disclosure on every card — 60 of 60 on four searches across two marketplaces and both buying formats — so a column that is always true was left out and the finding documented instead. The marker itself is a transparent span containing "derosnopS": "Sponsored" reversed, then flipped back with CSS.

Run eBay Scraper on Apify →

More guides