Install requests and protect your API key
Create a free CompSniper account, copy the key from the dashboard, and keep it in an environment variable. API keys are server-side credentials; never put one in browser JavaScript or commit it to Git.
python -m pip install requests
export COMPSNIPER_API_KEY="cs_live_REPLACEWITHYOURKEY"Request completed eBay sales
Call GET /v1/scrape with a keyword. This example asks for one page of used Shure SM7B sales and prints the summary plus five evidence rows.
import os
import requests
response = requests.get(
"https://api.compsniper.com/v1/scrape",
headers={
"Authorization": f"Bearer {os.environ['COMPSNIPER_API_KEY']}"
},
params={
"keyword": "shure sm7b",
"count": 240,
"itemCondition": "used",
},
timeout=75,
)
response.raise_for_status()
data = response.json()
print("Listings:", data["totalItems"])
print("Median:", data["summary"]["median"], data["summary"]["currency"])
print("Realistic range:", data["summary"]["p25"], "-", data["summary"]["p75"])
for item in data["items"][:5]:
print(item["title"], item["soldPrice"], item["soldCurrency"], item["endedAt"])
Read the sold rows and price summary
Every response includes pagination metadata, a sold-listing array, and deterministic price intelligence over the returned sample. Prices are decimal strings on listing rows and numbers in the summary.
{
"keyword": "shure sm7b",
"page": 1,
"totalItems": 207,
"totalResults": "475",
"rawMedian": 200,
"rawSampleCount": 240,
"summary": {
"count": 207,
"currency": "USD",
"median": 215,
"mean": 227.36,
"min": 90,
"max": 550,
"p25": 175.71,
"p75": 260,
"avgShipping": 9
},
"items": [
{
"title": "Shure SM7B Cardioid Dynamic Vocal Microphone - Complete in Box Excellent",
"soldPrice": "199.99",
"soldCurrency": "USD",
"endedAt": "2026-08-29"
}
]
}What relevance cleanup changed in this sample
Collected August 30, 2026 from ebay.com. The raw page contained 240 priced rows with a $200 median. After accessory, part, and wrong-model cleanup, 207 rows remained with a $215 median and a $175.71–$260 interquartile range. This is one observed sample, not a promise that every keyword changes by the same amount.
Narrow the result set before pricing
Use explicit API filters for condition, price, marketplace, date, category, and listing format. Use eBay minus-sign exclusions in the keyword when a predictable accessory keeps appearing.
params = {
"keyword": "iphone 15 pro -case -screen -charger",
"ebaySite": "ebay.com",
"itemCondition": "used",
"minPrice": 250,
"maxPrice": 1200,
"count": 100,
}Eight marketplaces
Change ebaySite without changing your parser.
Raw evidence available
Set relevance=false when you explicitly need every upstream match.
Handle rate limits without creating a retry storm
A temporary per-minute limit and an exhausted monthly quota both use HTTP 429, but their response codes require opposite behavior. Retry rate_limited after Retry-After. Never loop on quota_exceeded.
import random
import time
import requests
def search_with_safe_retries(url, headers, params):
for attempt in range(5):
response = requests.get(url, headers=headers, params=params, timeout=75)
body = response.json()
if response.ok:
return body
if response.status_code == 429 and body.get("code") == "quota_exceeded":
raise RuntimeError(
f"Quota exhausted. Reset: {body.get('reset_at')}. "
f"Upgrade: {body.get('upgrade_url')}"
) # Never retry monthly quota exhaustion.
temporary = (
response.status_code == 429 and body.get("code") == "rate_limited"
) or response.status_code in (500, 502, 503)
if temporary and attempt < 4:
delay = float(
response.headers.get("Retry-After")
or body.get("retry_after")
or min(2 ** attempt, 30)
)
time.sleep(delay + random.uniform(0, 0.5))
continue
raise RuntimeError(body.get("error", f"HTTP {response.status_code}"))
raise RuntimeError("Request failed after five bounded attempts")Successful responses expose X-Usage-Remaining. Slow down or ask the user to upgrade before an automated job reaches zero.
Paginate with an explicit safety bound
Increment page until hasNextPage is false. Each successful page uses one request, so production jobs should always define a maximum number of pages.
items = []
for page in range(1, 4): # Deliberately bounded: each page uses one request.
response = requests.get(
"https://api.compsniper.com/v1/scrape",
headers={"Authorization": f"Bearer {os.environ['COMPSNIPER_API_KEY']}"},
params={"keyword": "sony wh-1000xm5", "page": page, "count": 240},
timeout=75,
)
response.raise_for_status()
data = response.json()
items.extend(data["items"])
if not data["hasNextPage"]:
break
print("Collected rows:", len(items))Move from example to production
Use the full examples repository
Clone working Python, Node.js, cURL, pagination, retry, and Card Batch examples.
Read the complete API contract
Review every query parameter, response field, quota header, and error code.
Batch card pricing
Submit up to 100 structured sports or trading cards in one asynchronous job.
Compare eBay sold-data providers
See how CompSniper and SoldComps differ on result volume, cookies, cleanup, MCP, and pricing.
Frequently asked questions
Python and eBay sold data
How many sold listings can one Python request return?
Up to 240 sold listings per page. One successfully returned page uses one request from the account quota.
Can I retrieve sold prices from eBay marketplaces outside the US?
Yes. The same response format supports the US, UK, Germany, France, Italy, Spain, Canada, and Australia through the ebaySite parameter.
Should Python retry every HTTP 429 response?
No. Retry rate_limited only after Retry-After. Stop immediately on quota_exceeded and wait for reset_at or direct the user to upgrade_url.
Can I receive raw eBay matches without relevance cleanup?
Yes. Set relevance=false. The default relevance cleanup removes likely accessories, parts, and wrong-model matches before the price summary is calculated.