Sold Comps API Docs
v1 · base URL https://api.soldcomps.io/v1 · JSON over HTTPS · Bearer auth
Quickstart
- Create a free account and copy your key (
sh_live_…). - Send one GET request to
/v1/soldwith a keyword. That's the whole API. - Read
stats.medianfor a price, or loop overitemsfor the raw sales.
curl "https://api.soldcomps.io/v1/sold?q=rolex+submariner&condition=used" \ -H "Authorization: Bearer sh_live_xxxxxxxx"
GET/v1/sold
The only endpoint in the sold comps API. Returns up to 200 completed sales matching a keyword, newest first, plus summary stats. One call counts as one request whether it returns 3 sales or 200.
| Parameter | Type | Default | Notes |
|---|---|---|---|
q | string | required | What you'd type in eBay's search box. Put exact phrases in quotes ("psa 10") and exclude words with a minus (-lot). |
site | enum | EBAY_US | EBAY_US (ebay.com), EBAY_GB (ebay.co.uk) or EBAY_DE (ebay.de). |
condition | enum | any | new, open_box, refurbished, used or for_parts. |
min_price / max_price | number | none | Only sales in this price range, in the site's currency. |
buying_format | enum | any | auction or fixed_price (Buy It Now and Best Offer). |
days | integer | 90 | Look-back window, 1 to 90 days. |
Response
{
"query": "rolex submariner",
"site": "EBAY_US",
"window_days": 90,
"count": 162,
"stats": {
"median": 9450.00, "p25": 8100.00, "p75": 11200.00,
"min": 5200.00, "max": 16950.00, "excluded": 21, "currency": "USD"
},
"items": [
{
"item_id": "306123456789",
"title": "Rolex Submariner Date 126610LN Black Ceramic 41mm Box & Papers 2023",
"sold_price": { "value": 11450.00, "currency": "USD" },
"shipping": { "value": 0, "currency": "USD" },
"sold_at": "2026-10-22",
"condition": "Pre-Owned",
"buying_format": "fixed_price",
"best_offer_accepted": false,
"best_offer_price_known": true,
"url": "https://www.ebay.com/itm/306123456789",
"image": "https://i.ebayimg.com/images/g/.../s-l225.jpg",
"flags": []
}
]
}Item fields
| Field | Type | Description |
|---|---|---|
item_id | string | eBay item number |
title | string | Listing title at the time of sale |
sold_price.value / .currency | number / string | Sale price shown by eBay, e.g. 42.50 / "USD" |
shipping.value | number | null | Shipping the buyer paid. 0 = free shipping, null = not shown |
sold_at | string (date) | Day the item sold, YYYY-MM-DD |
condition | string | eBay's condition label, e.g. "Pre-Owned", "Brand New", "New (Other)", "Parts Only" |
buying_format | string | auction or fixed_price |
best_offer_accepted | boolean | true when the sale was an accepted Best Offer |
best_offer_price_known | boolean | false when the accepted offer amount isn't shown; these sales are left out of stats |
url | string | Link to the eBay listing |
image | string | null | Listing photo URL |
flags | string[] | Why a sale is left out of stats: lot, for_parts, box_only, replica, accessory, outlier. Empty = counted |
How stats are computed
stats is computed only from sales with an empty flags array and a known price. excluded tells you how many were left out, and every sale is still in items so you can apply your own rules. More in median vs. average sold price.
| Flag | Means | Example title |
|---|---|---|
lot | Several items sold together | N64 Console Bundle 8 Games · Pokemon cards x10 |
for_parts | Broken, untested or for repair | Super Nintendo SNES ***For Parts or Repair*** |
box_only | Packaging without the item | Empty box only, no console |
replica | Described as fake, replica or homage | 2 fake Rolex watches, props |
accessory | A part or add-on far below the typical price | Watch box · bezel gasket for Rolex · card sleeves |
outlier | More than 1.5× the interquartile range from the middle | A $2,039 typo among $40 sales |
Matching
eBay pads sold results with loose matches. We keep a sale only if every number in your query appears in the title as a whole number (“N64” matches64, “64GB” doesn't), quoted phrases appear exactly, and at least 80% of the other words appear. Minus words exclude a sale. Sales that don't match aren't returned or counted.
Examples
Python
import requests
r = requests.get(
"https://api.soldcomps.io/v1/sold",
params={"q": "rolex submariner 126610ln", "condition": "used"},
headers={"Authorization": "Bearer sh_live_xxxxxxxx"},
timeout=30,
)
r.raise_for_status()
data = r.json()
print(data["count"], "sales, median", data["stats"]["median"])JavaScript (Node 18+ / Deno / Bun)
const url = new URL("https://api.soldcomps.io/v1/sold");
url.searchParams.set("q", "charizard 4/102 \"psa 9\"");
url.searchParams.set("buying_format", "auction");
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SALEHISTORY_KEY}` } });
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error.code}`);
const { count, stats, items } = await res.json();Walkthroughs: eBay sold prices in Python · eBay sold comps in Google Sheets
Errors
| Status | error.code | Meaning | Counts? |
|---|---|---|---|
| 400 | invalid_param | Bad or missing parameter; error.param names it | No |
| 401 | unauthorized | Missing or revoked key | No |
| 402 | quota_exceeded | Monthly requests used up and no credits left | No |
| 429 | rate_limited | Over your per-minute limit; honor Retry-After | No |
| 502 | upstream_unavailable | eBay didn't answer cleanly; safe to retry with backoff | No |
Failed requests are never billed. Every response carries X-Request-Id, X-Quota-Remaining and X-RateLimit-Remaining headers.
Rate limits & quotas
Limits are per key: Free 30/min, Starter 60/min, Growth 120/min, Scale 300/min. Monthly quotas reset on the 1st (UTC). When the quota runs out, prepaid credits are used automatically if you have them. See pricing.
Data notes
- Each request returns up to 200 of the most recent matching sales from the last 90 days, newest first. For busy searches (like “pokemon card”) that may cover only the last day or two; add detail to the query to go further back for one item.
- Prices are in the site's currency: USD on EBAY_US, GBP on EBAY_GB, EUR on EBAY_DE.
- Some accepted Best Offer prices are hidden by eBay. Those items carry
best_offer_price_known: false, show the list price, and are left out ofstats. - You never send us an eBay login or cookie. If eBay blocks a fetch we return
502 upstream_unavailable, never an empty 200. - SaleHistory is independent and not affiliated with eBay.