# eBay Sold Listings Scraper: 3 Years Price History, Best Offer (`compsapi/ebay-sold-listings-history`) Actor

Get confirmed eBay sold prices with up to 3 years of history and the real accepted Best Offer price, not the asking price. Search 8 eBay marketplaces, filter by condition, category and price range, export JSON or CSV for comps, resale pricing and price analytics.

- **URL**: https://apify.com/compsapi/ebay-sold-listings-history.md
- **Developed by:** [Sam Smith](https://apify.com/compsapi) (community)
- **Categories:** E-commerce, Developer tools
- **Stats:** 15 total users, 4 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $2.95 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## eBay Sold Listings API: 3 Years of Price History, Real Sold Prices Only

Get eBay sold listings and completed listings as clean JSON, with **up to 3 years of sales history** and the **real accepted Best Offer price**, not the crossed out asking price. Sold comps for resale pricing, price analytics and market research, across 8 eBay marketplaces, no eBay login or developer approval needed.

Most eBay sold scrapers stop at 90 days, because that is all the public sold search shows. And when a sale goes through as an accepted Best Offer, they return the listing's asking price with a flag at best, an eBay page limitation they state themselves. This Actor fixes both: full multi year history and the price the buyer actually paid.

### Why this one

| | This Actor | Typical eBay sold scraper |
|---|---|---|
| History | up to 3 years (1095 days) | 90 days |
| Best Offer sales | real accepted price + `best_offer: true` | asking price, flag at best |
| Speed | about 2 seconds per page of 240 sales | 5 to 10 seconds |
| Sold time | date and time | date only |
| Results per page | up to 240 | up to 240 |
| Marketplaces | 8 (US, UK, DE, FR, IT, ES, CA, AU) | usually US only |

In a side by side test on 4 popular searches, all 49 Best Offer sales that both tools returned were priced too high by a 90 day scraper. A Rolex Submariner listed at $24,995 actually sold for $15,500.

We then measured it properly across **5,859 Best Offer sales**: 99.0% of them read high on the asking price, by a median of 14.9%. Full method and data: [the Best Offer gap study](https://compsapi.com/best-offer-gap?utm_source=apify\&utm_medium=actor\&utm_campaign=readme-study).

### What you can do with it

- Price inventory from real sales, including last year's prices for seasonal items.
- Track how resale prices move over months, not weeks.
- Build sold comps for trading cards, sneakers, watches, phones, consoles and collectibles.
- Feed pricing models, repricers, dashboards and spreadsheets with clean data.

### Who this is for

Resellers and flippers pricing inventory, liquidation and estate sale buyers valuing manifests, card and sneaker tools showing live comps, repricing and cross listing software, analysts tracking secondary market prices, and anyone who lost access to sold data when eBay retired findCompletedItems and put sold listings behind a login.

### Input

| Field | Description |
|---|---|
| `keywords` | One search per line, for example `iphone 15 pro 256gb`. Minus words exclude: `iphone 15 -pro -plus`. |
| `days` | How far back to search, 1 to 1095 (3 years). Default 90. |
| `maxResults` | Max sales per keyword, newest first. Default 240. |
| `condition` | `any`, `new`, `open_box`, `refurbished`, `used` or `parts`. |
| `bestOffer` | `mark` (default) flags every sale and gives the real accepted price, `only` returns accepted Best Offer sales only, `off` skips the check. |
| `site` | eBay marketplace: `US` (default, includes matching sales from other sites), `UK`, `DE`, `FR`, `IT`, `ES`, `CA` or `AU`. |
| `category` | Optional eBay category id, comma separate for several, for example `31387` for wristwatches. |
| `minPrice` / `maxPrice` | Only sales inside this sold price range. |
| `nativePrice` | Also return the price in the currency the sale happened in, read from the listing. Off by default, see below. |
| `nativePriceBudget` | Caps listing reads per run for the above. Default 200, max 1000. |

Example:

```json
{
  "keywords": ["charizard psa 10", "rolex submariner 126610"],
  "days": 365,
  "maxResults": 500,
  "condition": "any",
  "bestOffer": "mark"
}
```

### Output

One row per sold listing:

```json
{
  "keyword": "nintendo switch oled",
  "item_id": "227424612273",
  "title": "Nintendo Switch OLED Model Console 64GB Blue/Neon Yellow Joy-Con *EXCELLANT Cond",
  "price": 190.61,
  "best_offer": true,
  "us_sale": true,
  "native_price": 717.94,
  "native_currency": "GBP",
  "last_sold": "2026-10-07T22:05:00Z",
  "units_sold": 1,
  "total_sales": 190.61,
  "format": "FIXED_PRICE",
  "bids": 0,
  "shipping_cost": 5.7,
  "free_shipping_pct": 0,
  "condition": "used",
  "url": "https://www.ebay.com/itm/227424612273",
  "image": "https://i.ebayimg.com/00/s/OTY5WDE1OTA=/z/jBYAAeSwuTdqh6QE/$_1.JPG?set_id=8800005007"
}
```

- `price` is the price the item actually sold for. For an accepted Best Offer it is the accepted offer.
- `best_offer` is `true` for accepted Best Offer sales, `false` for sales at the listed price or at auction, `null` when it could not be confirmed.
- `us_sale` is `true` when the sale was made on eBay US and `false` when it was made on another eBay site. On the default search, which merges US with international coverage, this lets you separate the two without running a search per marketplace. Useful if you are building a US only price guide and the default's international sales would skew it.
- `native_price` and `native_currency` appear when you switch on **Original currency price**. `price` stays USD, so nothing else changes. They are `null` with a `native_reason` on two kinds of row: a listing that sold several units, where the price is an average across those sales and so matches no single amount the page ever showed, and a listing that is still running, where the seller can change the price after the sale. On a UK search of single collectibles we measured 35 of 40 rows filled.
- `condition` appears when you filter by one condition.
- `units_sold` and `total_sales` cover multi quantity listings.

Export as JSON, CSV, Excel or connect to Google Sheets, Make, Zapier and n8n through Apify integrations.

### Card comps: why the filters matter

Keyword search alone will not give you a card price. Measured on 17 October 2026, over three years of sales:

| Query | Median | Sales | Range |
|---|---|---|---|
| `charizard base set psa 10`, raw sold feed | **$14.73** | 239 listings | $1.30 to $30,000 |
| same words, junk filtered and outliers removed | **$545.00** | 60 sales | p25 $236.77, p75 $2,861.08 |

The first number is not wrong, it is the honest median of what eBay returns: sleeves titled "fits PSA 10 Charizard", bulk lots, raw cards described as a "PSA 10 contender", and fan art proxies all match those words, and all of them are cheap.

Adding the card number to the query does not fix it. `4/102` is Base Set Charizard and it is also a later reprint, and a longer query makes eBay widen the match rather than narrow it.

What fixes it is filtering on parsed structure: `graded`, `grader`, `grade` and the junk flags (`lot`, `mystery`, `accessory`, `replica`, `keyword_stuffing`, `not_graded_claim`). Every sale carries a parsed `card` object with grade, grader, card number, year, language and first edition, so you can filter after retrieval instead of guessing in the query.

Full write up: [why your card comps are wrong](https://compsapi.com/blog/why-your-card-comps-are-wrong?utm_source=apify\&utm_medium=actor\&utm_campaign=readme-cards).

### Need an API instead?

The same data is available as a REST API with monthly plans at [compsapi.com](https://compsapi.com?utm_source=apify\&utm_medium=actor\&utm_campaign=readme): one GET request, about 2 seconds, up to 240 sales per page.

### MCP server

The same data is available as a hosted MCP server, so an assistant can request comps directly. Details at [compsapi.com](https://compsapi.com?utm_source=apify\&utm_medium=actor\&utm_campaign=readme-mcp).

### FAQ

**Is this a Terapeak alternative?** For sold price research, yes: comparable history depth as JSON you can export and build on, without a Seller Hub subscription.

**Is this the eBay Marketplace Insights API?** No. Marketplace Insights is closed to new applicants and only covers 90 days. This Actor gives you up to 3 years without an eBay developer approval, a direct Marketplace Insights alternative.

**Is this a findCompletedItems replacement?** Yes. eBay retired the findCompletedItems API; this returns the same completed and sold listings data with more history.

**Which eBay site?** eBay US by default, with matching sales from other eBay sites included. Set `site` to UK, DE, FR, IT, ES, CA or AU to search one marketplace on its own.

**What currency are the prices in?** Always USD, whichever site you pick. eBay's research backend converts every amount to USD before we receive it, so a UK search returns USD too. A marketplace pin changes which sales you get, never the currency. If you need the original GBP or EUR amount, switch on **Original currency price** and each single, settled sale also carries `native_price` and `native_currency`, read from the listing rather than converted.

**Why is original currency off by default, and what is the budget for?** Every non US sale needs its own listing read, so it is the one input here with a real cost behind it. The budget caps those reads per run, 200 by default. Rows past the budget keep every other field with `native_price` null, and the run log tells you what it used. Prices already read are cached permanently, because a settled sale never changes price, so running the same search again is effectively free. Start with the default and raise it only if you need deeper coverage in one go.

**How fresh is the data?** Sales usually appear within minutes of the sale.

**Why do some searches return accessories?** eBay matches keywords in titles. Make searches specific (model number, size, grade) and use minus words, for example `rolex submariner -bezel -strap -insert`.

If this Actor saves you time, a rating helps other resellers find it. Found a problem or missing a field? Open an issue and it usually ships within days.

Not affiliated with eBay Inc. eBay is a trademark of eBay Inc.

**How far back do eBay sold listings go?** eBay's own public sold filter shows about 90 days, and the Marketplace Insights API is capped at 90 days and closed to new applicants. This Actor reaches 1,095 days, which is three years.

**Why do my card searches return junk?** eBay matches any arrangement of your words anywhere in the title, so accessories, lots and proxies all match. Filter on the parsed `card` fields and the junk flags rather than adding words to the query. See the card comps section above.

# Actor input Schema

## `keywords` (type: `array`):

One search per line, for example iphone 15 pro 256gb or charizard psa 10. Minus words exclude: iphone 15 -pro -plus.

## `days` (type: `integer`):

How far back to search, 1 to 1095 days (3 years).

## `maxResults` (type: `integer`):

Newest sales first. Up to 240 per page, pages are fetched until this number is reached.

## `condition` (type: `string`):

Only sales in this condition.

## `bestOffer` (type: `string`):

Mark: every sale gets best_offer true or false, and price is the real accepted offer. Only: return accepted best offer sales only.

## `site` (type: `string`):

Which eBay site to search. US includes matching sales from other eBay sites; every other choice searches only sales made on that site. Prices are always USD, because eBay's research data converts before we receive it.

## `category` (type: `string`):

Optional eBay category id, comma separate for several, for example 31387 for wristwatches.

## `minPrice` (type: `integer`):

Only sales at or above this price.

## `maxPrice` (type: `integer`):

Only sales at or below this price.

## `nativePrice` (type: `boolean`):

Also return the price in the currency the sale actually happened in, read from the listing itself. Prices are otherwise always USD. Only filled for single, settled sales: a listing that sold several units has an averaged price that matches no single amount, and a listing still running can change price, so both come back null with a reason. Off by default because each one costs a listing read.

## `nativePriceBudget` (type: `integer`):

Caps how many listings one run will read for original currency prices. Rows past the budget keep every other field with native_price null. Already read prices are cached and cost nothing on later runs, so a repeat run of the same search is effectively free.

## Actor input object example

```json
{
  "keywords": [
    "iphone 15 pro 256gb"
  ],
  "days": 90,
  "maxResults": 240,
  "condition": "any",
  "bestOffer": "mark",
  "site": "US",
  "nativePrice": false,
  "nativePriceBudget": 200
}
```

# Actor output Schema

## `soldListings` (type: `string`):

Every sold listing from the run as JSON, one row per sale.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "iphone 15 pro 256gb"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("compsapi/ebay-sold-listings-history").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "keywords": ["iphone 15 pro 256gb"] }

# Run the Actor and wait for it to finish
run = client.actor("compsapi/ebay-sold-listings-history").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "iphone 15 pro 256gb"
  ]
}' |
apify call compsapi/ebay-sold-listings-history --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,compsapi/ebay-sold-listings-history"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/nBK4Tsl5GRZ2jLwlc/builds/IuxwIHWipNMqTmA6b/openapi.json
