Most "eBay scraper" tutorials download eBay's search pages and parse the HTML. Before you write one, read what eBay says about that. Its User Agreement prohibits using "any robot, spider, scraper, data mining tools, data gathering and extraction tools, or other automated means to access our Services for any purpose, except with the prior express permission of eBay". Its robots.txt disallows the search result pages those tutorials start from.
eBay does offer a supported way to get the same listing data: the Browse API. This guide builds an eBay scraper in Python on top of it. The script searches listings, filters them, pages through the results, handles rate limits and writes a CSV, in about 80 lines of code. Nothing in it parses eBay's HTML.
What eBay allows
Two documents set the rules, and they say different things.
robots.txt tells crawlers which paths they may request. Python's standard library can read it. This check ran against eBay's live file on 13 October 2026:
from urllib.robotparser import RobotFileParser
rp = RobotFileParser("https://www.ebay.com/robots.txt")
rp.read()
for path in ["/sch/i.html?_nkw=mechanical+keyboard", "/itm/123456789012", "/b/Keyboards/33963/bn_1648"]:
print(f"{rp.can_fetch('*', 'https://www.ebay.com' + path)!s:5} {path}")False /sch/i.html?_nkw=mechanical+keyboard
True /itm/123456789012
True /b/Keyboards/33963/bn_1648Search results (/sch/) are off limits to generic crawlers, while item and browse pages are not disallowed. robots.txt isn't the whole answer, though. The comment at the top of eBay's file says that using robots or other automated means to access the site without eBay's express permission is strictly prohibited, with an exception for public search engines.
The User Agreement is the contract you accept by using eBay, and it bans automated access without permission, whatever robots.txt allows. So "the path isn't disallowed" doesn't make a page scraper acceptable on eBay. The API is how eBay grants that permission for programmatic access.
This is a summary of eBay's published terms, not legal advice. If eBay data is central to a commercial product, have someone qualified read the agreements that apply to you.
What the Browse API gives you
The item_summary/search method returns the fields a listing scraper usually collects:
Field | What it holds |
|---|---|
| eBay's identifier for the listing |
| The listing title |
|
|
| For example "New" or "Used" |
|
|
| The public listing URL |
| The main image URL |
|
|
| Where the item ships from |
For everything on one listing, such as item specifics, full descriptions and all images, call getItem with the itemId.
Three limits from eBay's documentation shape the code below:
limitcan be 1 to 200 per call (default 50), andoffsetmust be 0 or a multiple oflimit.- One search returns at most 10,000 items. For more, split the query, for example by category or by price band.
- Only listings with a Buy It Now (
FIXED_PRICE) option come back by default. Add abuyingOptionsfilter to include auctions.
Before you start: keys and access
- Create an account in the eBay Developers Program and generate a keyset. You get a client ID and client secret for the sandbox and for production.
- Start against the sandbox (
https://api.sandbox.ebay.com). Anyone with a developer account can use the Buy APIs, Browse included, there. - Read eBay's Buy API requirements before you depend on production. They say production use of the Buy APIs is intended for eBay partners and can require an application and agreements, depending on your business model.
- Note your daily call limit. If you need more, eBay offers higher limits after an Application Growth Check.
Install the one dependency:
python -m pip install requests
export EBAY_CLIENT_ID='your-client-id'
export EBAY_CLIENT_SECRET='your-client-secret'Step 1: Get an application token
Browse API calls for public listing data use an application access token, obtained with the OAuth client credentials grant. You send your client ID and secret as HTTP Basic credentials and ask for the public api_scope:
import base64
import os
import time
import requests
API_ROOT = os.environ.get("EBAY_API_ROOT", "https://api.ebay.com") # sandbox: https://api.sandbox.ebay.com
CLIENT_ID = os.environ["EBAY_CLIENT_ID"]
CLIENT_SECRET = os.environ["EBAY_CLIENT_SECRET"]
SCOPE = "https://api.ebay.com/oauth/api_scope"
def get_token(session):
basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
resp = session.post(
f"{API_ROOT}/identity/v1/oauth2/token",
headers={"Authorization": f"Basic {basic}", "Content-Type": "application/x-www-form-urlencoded"},
data={"grant_type": "client_credentials", "scope": SCOPE},
timeout=30,
)
resp.raise_for_status()
body = resp.json()
return body["access_token"], time.time() + body["expires_in"] - 60The token is valid for expires_in seconds, two hours in eBay's examples. The function returns the expiry time minus a minute, so the search loop can refresh the token before it lapses instead of failing halfway through a long run.
Step 2: Search and paginate
Each call sends the token as a bearer token and names the marketplace. X-EBAY-C-MARKETPLACE-ID falls back to EBAY_US when it is missing, and it is required for every other marketplace:
def search(query, marketplace="EBAY_US", max_items=600, page_size=200, filters=None):
session = requests.Session()
token, expires_at = get_token(session)
offset = 0
while offset < max_items:
if time.time() > expires_at:
token, expires_at = get_token(session)
params = {"q": query, "limit": page_size, "offset": offset}
if filters:
params["filter"] = filters
page = get_with_backoff(
session,
f"{API_ROOT}/buy/browse/v1/item_summary/search",
params,
{"Authorization": f"Bearer {token}", "X-EBAY-C-MARKETPLACE-ID": marketplace},
)
items = page.get("itemSummaries", [])
yield from items
if not page.get("next") or not items:
break
offset += page_size
time.sleep(1) # keep well inside your daily call allowanceThe response carries total, limit, offset and a next URL whenever there is another page. The loop stops when next is missing, when a page comes back empty, or when it reaches max_items. search() is a generator, so you can stream items into a file without holding them all in memory.
Step 3: Filter and sort
The filter parameter takes comma-separated conditions. A few that cover most needs:
Filter | Example |
|---|---|
Price range |
|
Condition |
|
Buying format |
|
Specific sellers |
|
Ships to |
|
Add sort=price (or -price, or newlyListed) to the parameters to change the order. The default is eBay's Best Match. category_ids narrows a search to one category, which is also the cleanest way to split a query that exceeds the 10,000-item cap.
Step 4: Handle rate limits and errors
When you go over a rate limit, the API answers HTTP 429. Transient server errors are 5xx. Retry both with a growing delay, and honour Retry-After when the response includes it:
def get_with_backoff(session, url, params, headers, attempts=5):
for attempt in range(attempts):
resp = session.get(url, params=params, headers=headers, timeout=30)
if resp.status_code not in (429, 500, 502, 503, 504):
resp.raise_for_status()
return resp.json()
wait = int(resp.headers.get("Retry-After", 2 ** attempt))
print(f"HTTP {resp.status_code}, waiting {wait}s")
time.sleep(wait)
resp.raise_for_status()Any other error, such as a 400 for a malformed filter or a 401 for a bad token, is raised immediately, because retrying won't fix it. The one-second pause between pages in search() matters more than the retry logic. Your allowance is counted per day, and a script that burns it in a minute has nothing left for the rest of the day.
Step 5: Export to CSV
The last part writes one row per listing:
import csv
if __name__ == "__main__":
fields = ["itemId", "title", "price", "currency", "condition", "seller", "itemWebUrl"]
with open("ebay_items.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
count = 0
for item in search("mechanical keyboard", filters="conditions:{NEW},buyingOptions:{FIXED_PRICE}"):
writer.writerow({
"itemId": item["itemId"],
"title": item["title"],
"price": item.get("price", {}).get("value"),
"currency": item.get("price", {}).get("currency"),
"condition": item.get("condition"),
"seller": item.get("seller", {}).get("username"),
"itemWebUrl": item.get("itemWebUrl"),
})
count += 1
print(f"Saved {count} items to ebay_items.csv")Put the five blocks in one file, ebay_browse.py, in the order shown, and run it with python ebay_browse.py. We tested the complete script against a local server that implements the documented request rules: the token call, limit up to 200, offset as a multiple of limit, the marketplace header, and a 429 with Retry-After. It paged through 450 items, waited out the 429 and resumed where it stopped. If you'd rather store results as JSON, our guide to working with JSON in Python covers reading and writing it.
Other eBay marketplaces
Set marketplace to the site you want: EBAY_GB, EBAY_DE, EBAY_AU and so on. Prices come back in that marketplace's currency. To see shipping as a buyer in a particular place would, eBay recommends the X-EBAY-C-ENDUSERCTX header with a contextualLocation (country and, where used, postal code), which makes delivery estimates and calculated shipping more accurate. Comparing the same product across marketplaces is a common use. Our walkthrough of cross-country price monitoring covers how to design that kind of comparison.
What about sold listings?
The Browse API returns active listings only. Sold and completed listings sit in a separate API, Marketplace Insights, and eBay restricts access to it. If you sell on eBay, Terapeak product research in Seller Hub is eBay's own tool for sold-listing history. Scraping eBay's "Sold items" search pages runs into the same User Agreement clause and robots.txt rule as any other search page.
Where proxies fit, and where they don't
The Browse API doesn't need a proxy. eBay identifies your application by its token, not by your IP address, and your call limits belong to the application. Routing API calls through different IPs doesn't change any of that. Don't use proxies to get around eBay's limits or terms.
Proxies are useful in two other places. One is checking, in an ordinary browser, how your own listings or your store look to a buyer in another country: the currency, shipping options and the content shown there. Evomi's residential proxies cover 195+ countries for that kind of spot check. The other is collecting data from sites that permit it, where a sensible crawler still follows the rules in our guide to respecting robots.txt and crawl-delay.
FAQ
Is it legal to scrape eBay?
That depends on your jurisdiction and on what you do with the data, and this article can't settle it. What is clear is eBay's own position: its User Agreement prohibits scrapers and other automated access without eBay's express permission, and its robots.txt disallows search pages. Using the Browse API under its license is the route eBay provides.
Does eBay allow web scraping?
Not without permission. The User Agreement bans automated access "for any purpose, except with the prior express permission of eBay", and the robots.txt header carries the same warning. Public search engines are the exception the file names.
How can I scrape eBay sold listings?
eBay doesn't permit scraping its sold-items pages. Sold-listing data is available through the Marketplace Insights API, which is restricted, and to sellers through Terapeak in Seller Hub.
Is there an eBay API for Python?
eBay's REST APIs, including Browse, work from Python with requests, as in this guide. You don't need an SDK. Older tutorials use the Finding API through the ebaysdk package, but eBay decommissioned the Finding API in February 2025, so new code should use Browse.
How many items can I get from one eBay search?
Up to 200 per call and 10,000 per search through the Browse API. For more, split the search by category, price band or condition, and run each part separately.
Do I need proxies to use the eBay API?
No. API access is tied to your application keys and call limits, so proxies add nothing to an API client.