How to Scrape eBay Listings and Sold Data for Pricing Research
eBay's sold-listings history is the closest thing to a free market-price database — if you know the two URL parameters that unlock it. Here's how to scrape listings, variants and real transaction prices at scale.
eBay is one of the largest peer-to-peer marketplaces on the web, which makes it a goldmine for pricing research, competitor tracking and resale valuation. It has an official developer SDK, but the most useful data — what items actually sold for — is easiest to collect straight from the public search pages. This guide shows how to scrape eBay listings, variant data and real sold prices, plus the proxy setup that keeps a scraper alive past the first few hundred requests.
The search URL is the API
When you search eBay, the query is encoded entirely in the URL, and a handful of parameters control exactly what comes back. Master these five and you can drive eBay's catalogue programmatically without touching a browser:
_nkw— the search keyword (URL-encode spaces as +)._sacat— category restriction (0 = all categories)._sop— sort order (e.g. newly listed, price + shipping)._pgn— page number for pagination._ipg— items per page (default 60; the practical maximum per request).
The sort parameter _sop deserves a mention: its values order results by newly listed, lowest total price including shipping, or ending soonest, which lets you shape targeted feeds — newest listings for a change monitor, cheapest total for a deal finder. Restricting with _sacat narrows a broad keyword to the right department, stripping noise before you parse a single row. Keyword, category and sort together let you express most research queries as a single URL, which is exactly what makes eBay pleasant to automate.
import requests
from urllib.parse import urlencode
proxy = "http://USER:PASS@gate.quantumproxies.io:8000"
proxies = {"http": proxy, "https": proxy}
def search_url(keyword, page=1, per_page=60, category=0):
params = {"_nkw": keyword, "_sacat": category,
"_ipg": per_page, "_pgn": page}
return "https://www.ebay.com/sch/i.html?" + urlencode(params)
r = requests.get(search_url("vintage seiko watch"),
proxies=proxies, timeout=20)
print(r.status_code) # parse r.text with your HTML library of choice

Sold and completed listings: the real market price
This is the endpoint that matters for pricing. Active listings only tell you what sellers hope to get; plenty never sell. eBay exposes actual transaction history on the same search page through two extra parameters: LH_Complete=1 shows all completed listings, and LH_Sold=1 narrows to items that genuinely sold. On a sold listing, the price is the final sale value, not an asking price — which makes this the cleanest free source of what things are really worth:
def sold_url(keyword, page=1):
# append the completed + sold filters to any search
return ("https://www.ebay.com/sch/i.html?"
f"_nkw={keyword.replace(' ', '+')}"
"&LH_Complete=1&LH_Sold=1"
f"&_ipg=60&_pgn={page}")
# every sold row = a real transaction: use median, not mean,
# to shrug off outliers and shill bids
r = requests.get(sold_url("nike dunk low panda 10"),
proxies=proxies, timeout=20)
Because the sold page uses the same markup as a standard search, your list parser handles it unchanged — you just append the two filters. Take the median of the last 30-60 sold prices rather than the average, and you get a robust market value that resale and competitor-price monitoring workflows can trust.
Parsing a listing: price, specifics and variants
A single-variant listing is straightforward: the price sits in the primary price element, the item-specifics table (condition, brand, model, size) is a set of label/value pairs, and the description lives in an iframe you can fetch separately. Watch for eBay's regional price conversion — some regions get a converted price alongside the original, so capture both.
Multi-variant products (a phone with model, storage and colour options) are trickier because the per-option prices are updated by JavaScript. That data is not in the visible HTML — it is hidden web data, stored in a JavaScript variable called MSKU inside a <script> tag. Extract that JSON and you get every variant's price and stock in one shot, without rendering the page:
import re, json
def extract_msku(html):
# the variant matrix lives in a JS object keyed 'MSKU'
m = re.search(r'"MSKU"\s*:\s*(\{.*?\})\s*[,}]', html, re.S)
if not m:
return {} # single-variant listing, no matrix
data = json.loads(m.group(1))
# data now holds per-variant price, currency and stock flags
return data
variants = extract_msku(r.text)

Staying unblocked at scale
One listing is trivial. Scale is where eBay pushes back — with rate limiting, CAPTCHA challenges and dynamic content that flags automated patterns. Datacenter IPs get throttled fast on marketplaces; the reliable fix is residential proxies that present as ordinary shoppers, rotated so no single IP racks up an obvious burst. Pace requests, honour any 429 with backoff, and localise your exit when you need region-specific pricing, since eBay tailors results and currency by geography.
When a target adds heavier bot management or you would rather not maintain the parsing and rotation stack yourself, a Scraper API handles IP rotation, JS rendering and retries behind one endpoint and returns clean HTML, JSON or markdown. For the broader marketplace playbook, our guide on marketplace automation across Amazon, eBay and Etsy covers account safety and geo-targeting.
Scrape eBay with clean residential proxies
From scrape to dataset
Once rows are flowing, the discipline is deduplication and history. Key every record by its eBay item id so a re-scrape updates the existing row instead of duplicating it, and store the price with a timestamp and the item condition on every pull — a sold price only means something next to its date and whether the item was new or used. Keep a rolling window of recent sold data per product and you have a live valuation that refreshes itself, which is the foundation of any resale, repricing or arbitrage tool. Pair that with alerting on sudden price moves and the dataset starts doing the watching for you.
Is scraping eBay legal?
This is general information, not legal advice. Collecting publicly available listing data — prices, conditions, sold history — at respectful rates is widely treated as legitimate. The line to watch is personal data: seller names, locations and contact details are protected under GDPR in the EU and similar laws elsewhere, so avoid storing them without a lawful basis. Scrape the market signal, not the people.
Frequently asked questions
How do I scrape eBay sold listings?
Take any eBay search URL and append LH_Complete=1&LH_Sold=1. That switches the results to completed listings that actually sold, where the displayed price is the final transaction value. The page uses the same markup as a normal search, so the same parser extracts title, price and sale date for pricing research.
Can I scrape eBay prices without the official API?
Yes. The public search and listing pages return everything a buyer sees, with no quota or OAuth. The search parameters (_nkw, _sacat, _pgn, _ipg) drive queries and pagination, and product pages carry price, item specifics and variant data. You will need rotating residential proxies to sustain volume without being rate-limited.
How do I get eBay variant prices?
Multi-option listings store their per-variant price and stock in a JavaScript object named MSKU embedded in a script tag, not in the visible HTML. Extract that JSON block and you get every combination's price without rendering the page or clicking each option — far faster and more reliable than driving a browser.
Why does my eBay scraper get blocked?
eBay applies rate limiting and CAPTCHA challenges to traffic that looks automated, and datacenter IP ranges are throttled quickly. Spread requests across rotating residential IPs, pace them with delays, back off on any 429, and keep headers coherent. If challenges persist, a Scraper API that rotates IPs and renders JavaScript removes the maintenance burden.
Scrape the search page like an API, unlock sold history with two parameters, mine variant matrices from the embedded JSON, and route it all through clean residential IPs. Do that and eBay becomes a live pricing dataset you control.