---
title: "SoldComps Sold Comps API reference"
url: https://www.soldcomps.io/docs
updated: 2026-10-23
answer: "The SoldComps sold comps API has one endpoint, GET /v1/sold, which returns up to 200 completed eBay sales for a keyword as JSON, with median and quartiles. v1 · base URL https://www.soldcomps.io/v1 · Bearer auth · OpenAPI 3.1 spec: https://www.soldcomps.io/openapi.json"
publisher: SoldComps (https://www.soldcomps.io)
---

# SoldComps Sold Comps API reference

> The SoldComps sold comps API has one endpoint, GET /v1/sold, which returns up to 200 completed eBay sales for a keyword as JSON, with median and quartiles. v1 · base URL https://www.soldcomps.io/v1 · Bearer auth · OpenAPI 3.1 spec: https://www.soldcomps.io/openapi.json

## Quickstart

1. Create a free account at https://www.soldcomps.io/signup and copy your key (`sh_live_…`).
2. Send one GET request to `/v1/sold` with a keyword. That's the whole API.
3. Read `stats.median` for a price, or loop over `items` for the raw sales.

```bash
curl "https://www.soldcomps.io/v1/sold?q=rolex+submariner&condition=used" \
  -H "Authorization: Bearer sh_live_xxxxxxxx"
```

## GET /v1/sold

The only endpoint. 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

```json
{
  "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.

| 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" matches 64, "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

```python
import requests

r = requests.get(
    "https://www.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)

```js
const url = new URL("https://www.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.SOLDCOMPS_KEY}` } });
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error.code}`);
const { count, stats, items } = await res.json();
```

## Errors

Error bodies look like `{"error": {"code": "invalid_param", "param": "q"}}`.

| Status | error.code | Meaning | Billed? |
|---|---|---|---|
| 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`, `X-RateLimit-Remaining` headers.

## Rate limits and 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. Pay-as-you-go keys have no monthly quota and draw from credits ($5 per 1,000). Every successful search counts as one request; failed requests are never counted. GET /v1/usage returns your plan, requests used and remaining, and when the quota resets (never billed). Every /v1/sold response also carries X-Quota-Remaining. See [pricing](https://www.soldcomps.io/pricing.md).

## 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 of stats.
- You never send us an eBay login or cookie. If eBay blocks a fetch we return 502 upstream_unavailable, never an empty 200.
- SoldComps is independent and not affiliated with eBay.

## MCP server

AI agents can call the same data through the SoldComps MCP server at https://www.soldcomps.io/mcp. Setup: https://www.soldcomps.io/mcp-server.
