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:
- 8 URL routes, including eBay's own "Sold items" rail link copied verbatim from a working results page, category- and seller-scoped variants, and the RSS feed
- 7 marketplaces
- Datacenter and residential proxies, with 8 pinned geographies
- 5 TLS fingerprints
- Cold, warm and full-navigation session chains
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:
units_sold— has anyone paid this price, and how many timeswatchers— how many shoppers are tracking it right nowbidsandtime_left— for auctions, live demandlist_priceanddiscount_pct— how far below the seller's own asking price the listing sits
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
- No sold or completed listings, for the reasons above. An actor that offers the toggle is either signed in or returning nothing.
- Roughly 2,000 items per search, whatever the result total says — which is why page depth is capped rather than pretending to be unlimited.
- Auction cards carry no seller line on the results page; those rows get the seller from the item page only.
- No sponsored column, because it would be true on every row.
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.