# Catawiki Sold Prices Scraper (`phantom_coder/catawiki-sold-prices-scraper`) Actor

Get Catawiki sold prices and past auction results by keyword: final price, sold or unsold, reserve status, and optional expert estimates. History back to 2020, a tracker for new sales, lot ID lookups, and live bids. No Catawiki account needed.

- **URL**: https://apify.com/phantom_coder/catawiki-sold-prices-scraper.md
- **Developed by:** [Phantom Coder](https://apify.com/phantom_coder) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.25 / 1,000 lot results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Catawiki Sold Prices Scraper

Get **closing prices from Catawiki's closed auction lots** by keyword or by lot ID, with sold/unsold status, reserve info, and optional specs, expert estimates, and seller data. No Catawiki account or API key required.

***

### Who is this for

- **Collectors and dealers** (trading cards, coins, watches, comics) checking what a specific item actually sold for on Catawiki before bidding on a similar one
- **Pricing and market research teams** building a sold-price history for a category or keyword that Catawiki's own site does not expose in bulk
- **Arbitrage buyers** comparing a live listing's current bid against recent closed-lot sold prices for the same keyword
- **Appraisers and insurers** pulling expert estimates and specifications for comparable closed lots

***

### Why it's different

Most Catawiki tools track **live** auctions: current bids, estimates, and end-of-auction alerts. This Actor is built for **closing and sold prices**. Catawiki keeps final prices for closed lots for years (in testing, lots that closed in 2020 still returned prices), but nothing on the site lets you browse that history by keyword - you need the exact lot ID.

This Actor closes that gap: give it a keyword like `"rolex submariner"`, and it scans Catawiki's closed-lot index, prices every candidate, and returns only the lots that actually sold (or, optionally, closed without a sale). For example, from a real run:

```
title: "Rolex - Submariner Date - Ref. 16610 - Men - 2006"
status: "sold"   finalPrice: 7900   currency: "EUR"   biddingEndTime: "2026-08-30T12:07:30.000Z"
```

***

### What you get

- **Sold prices by keyword** - scans Catawiki's closed-lot index and returns final prices for matching lots
- **Unsold closed lots** (optional) - lots that closed without meeting reserve, clearly separated from real sales
- **Specific lot lookups** - price and describe exact lot IDs or URLs you already have, any status
- **Live/open lot prices** - current highest bid for lots still being bid on, by keyword
- **Track new sales** - remembers open lots matching your keywords and reports each one's final price once it closes, filling the gap the closed-lot index leaves for recent sales
- **Lot details add-on** - specifications, expert estimate, seller ID/country/score, and a plain-text description, for lots young enough that Catawiki still serves them

***

### How to use

<ol>
<li>Pick a <strong>mode</strong>: "Sold prices" to search by keyword, "Specific lot IDs" if you already have lot IDs or URLs, "Live/open lots" for current auctions, or "Track new sales" to catch sales the closed-lot index hasn't indexed yet.</li>
<li>For "Sold prices", "Live/open lots", or "Track new sales", add one or more <strong>keywords</strong> - every word must appear as a whole word in the lot's title (so "psa 10" never matches "psa 100").</li>
<li>Set <strong>max results per keyword</strong>, and turn on <strong>include unsold</strong> if you also want closed-but-not-sold lots.</li>
<li>Turn on <strong>include lot details ($)</strong> if you want specifications, expert estimates, and seller info - it costs one extra page load per lot.</li>
<li>Click <strong>Start</strong>, then open the dataset.</li>
</ol>

> ⚠️ "Sold prices" mode finds lots through Catawiki's closed-lot index, which doesn't include lots from the last 6 or so weeks (see Limitations). A keyword with 0 results most often means nothing matching has closed yet in the indexed window, not that the Actor failed - check the run log, which always says when a keyword matched nothing. For recent sales, use "Track new sales" instead.

### Tracking new sales

"Track new sales" mode closes the gap left by the closed-lot index: it remembers every open lot matching your keywords and reports each one's final price as soon as it closes, without waiting for the index to catch up.

- **Schedule it** to run regularly (daily is best) - each run prices the lots it's already tracking, drops any that closed, and starts tracking new open lots it finds.
- **Run "Sold prices" mode once first** for history, then switch to "Track new sales" to keep it current going forward.
- **One tracker per keyword and exclude-words combination** - changing the exclude words starts a new tracker, and the lots the old one was watching are no longer checked. Pick your exclude words before you schedule.
- The first run for a new keyword only starts tracking open lots - it won't have anything to report until a later run, once those lots close.

### Input configuration

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `sold` | `sold`, `lotIds`, `live`, or `track` |
| `keywords` | array | `["charizard psa 10"]` | Search terms (sold/live/track modes); every word must match as a whole word |
| `lotIds` | array | none | Lot IDs or lot URLs (lotIds mode) |
| `currency` | string | `EUR` | `EUR`, `USD`, or `GBP` - which currency `highestBid`/`finalPrice` are shown in |
| `maxResultsPerKeyword` | integer | `100` | Max qualifying lots per keyword (1-5000); in track mode, the max open lots tracked per keyword |
| `excludeWords` | array | none | Skip lots whose title contains any of these whole words (sold/live/track modes) |
| `includeUnsold` | boolean | `false` | Also return closed-but-unsold lots (not applicable to lotIds mode, which always returns every status) |
| `soldAfter` | string | none | Only lots whose bidding ended on/after this date (`YYYY-MM-DD`); sold mode only |
| `soldBefore` | string | none | Only lots whose bidding ended on/before this date (`YYYY-MM-DD`); sold mode only |
| `includeDetails` | boolean | `false` | Add-on ($): specs, expert estimate, seller info, description |
| `proxyConfiguration` | object | `{ "useApifyProxy": true }` | Apify datacenter proxy (default, verified working) or residential as a fallback |

```json
{
  "mode": "sold",
  "keywords": ["charizard psa 10"],
  "currency": "EUR",
  "maxResultsPerKeyword": 50,
  "includeUnsold": false,
  "includeDetails": false
}
```

***

### Output

One flat row per lot. Every row has the same fields regardless of status; add-on fields appear only when `includeDetails` is on.

**A sold lot:**

```json
{
  "lotId": 106222753,
  "url": "https://www.catawiki.com/en/l/106222753-rolex-submariner-date-ref-16610-men-2006",
  "title": "Rolex - Submariner Date - Ref. 16610 - Men - 2006",
  "status": "sold",
  "sold": true,
  "closed": true,
  "finalPrice": 7900,
  "highestBid": 7900,
  "currency": "EUR",
  "priceEUR": 7900,
  "priceUSD": 9168,
  "priceGBP": 6763,
  "reservePriceMet": true,
  "biddingStartTime": "2026-08-21T10:00:00.000Z",
  "biddingEndTime": "2026-08-30T12:07:30.000Z",
  "auctionId": 1272898,
  "favoriteCount": 37,
  "imageUrl": "https://assets.catawiki.nl/assets/2026/8/6/4/0/4/4046bab3-5b39-42f5-ad3f-21d65f6e41c7.jpg",
  "thumbnailUrl": "https://assets.catawiki.nl/assets/2026/8/6/4/0/4/thumb2_4046bab3-5b39-42f5-ad3f-21d65f6e41c7.jpg",
  "keyword": "rolex submariner",
  "scrapedAt": "2026-10-25T17:03:20.404Z"
}
```

**A closed, unsold lot** (`includeUnsold: true`, key fields only). Bidding reached €13,500, but the reserve wasn't met, so there was no sale:

```json
{
  "lotId": 106724125,
  "title": "Rolex - Day-Date - 18308 - Unisex - 1991",
  "status": "unsold",
  "sold": false,
  "closed": true,
  "finalPrice": null,
  "highestBid": 13500,
  "currency": "EUR",
  "reservePriceMet": false,
  "biddingEndTime": "2026-09-15T16:11:50.000Z"
}
```

**An open lot** (`live` mode, `currency: "USD"`, key fields only):

```json
{
  "lotId": 107907234,
  "title": "Pokémon - 1 Graded card - Charizard 009/049 Foil - CGC 10 - Various sets",
  "status": "open",
  "sold": null,
  "closed": false,
  "finalPrice": null,
  "highestBid": 7,
  "currency": "USD",
  "reservePriceMet": null,
  "biddingEndTime": "2026-11-01T20:51:55.000Z"
}
```

**With `includeDetails: true`, a lot also carries** (from a real run; seller fields `sellerId`, `sellerCountry`, and `sellerScore` left out here):

```json
{
  "detailsAvailable": true,
  "subtitle": "Japanese",
  "categoryId": 1465,
  "categoryName": "Pokemon Cards",
  "expertEstimateMin": 380,
  "expertEstimateMax": 450,
  "expertEstimateCurrency": "EUR",
  "specifications": [{ "name": "Grade", "value": "10" }, { "name": "Type", "value": "Graded card" }],
  "description": "2022 Pokémon GO Radiant Pokémon Set – Radiant Venusaur, Charizard, Blastoise & Eevee – PSA Gem Mint 10 (4 Cards) Offered..."
}
```

**A lot too old for details** (`includeDetails: true`, but the detail page has redirected away):

```json
{
  "detailsAvailable": false
}
```

**No match for a keyword** (not an error - the run still succeeds):

The dataset simply has no row for that keyword, and the run log reads `No closed lots matched "<keyword>".`

***

### Understanding the data

- **`status` and `sold` always travel together**: `status: "sold"` means `sold: true`; `status: "unsold"` means `sold: false` (closed, but no winner - the reserve wasn't met); `status: "open"` means `sold: null` (not decided yet).
- **`finalPrice` is only ever set for a sold lot.** `highestBid` is set for every status - it is the current bid for an open lot, and the winning bid for a sold or unsold lot. Read `highestBid` together with `reservePriceMet` for an unsold lot: a high `highestBid` with `reservePriceMet: false` tells you the seller's reserve was set above what bidders were willing to pay.
- **`priceEUR`/`priceUSD`/`priceGBP` are always all three present**, regardless of the `currency` input - `highestBid`/`finalPrice` just point at whichever one you asked for, so you don't need to re-run the Actor to compare currencies.
- **`keyword` is `null`** in `lotIds` mode. In `sold` mode, a lot that matches two keywords is returned once, credited to the first keyword in your list, and billed once.

### Common use cases

- **Price-check before bidding**: run `lotIds` mode on the open lot's ID plus a handful of past sold lots found via a keyword search, and compare `highestBid` on the open lot against the median `finalPrice` of the last 10 sold matches.
- **Build a niche's sold trend**: run `sold` mode once for a keyword like `"rolex submariner"` with `maxResultsPerKeyword: 200` and chart `finalPrice` over `biddingEndTime`, then schedule `track` mode daily to keep the chart current without paying for the same lots twice.
- **Spot overpriced reserves**: run with `includeUnsold: true` and filter for `status == "unsold"` - a cluster of unsold lots for the same keyword suggests sellers are pricing above what the market pays.
- **Comparable-sales research with context**: turn on `includeDetails` and filter sold lots where `finalPrice` is more than 20% below `expertEstimateMin`, to find items that sold for less than Catawiki's own expert expected.
- **Cover recent sales the index misses**: schedule `track` mode daily for a keyword, so new sold prices show up as soon as a lot closes, instead of waiting 6-10 weeks for `sold` mode's closed-lot index to catch up.

### FAQ

**Do I need a Catawiki account?** No - everything here is scraped logged out.

**Why did my keyword return 0 results?** Either no matching lot closed early enough to be in Catawiki's closed-lot index yet (see Limitations), or the keyword's tokens are too specific - remember matching is whole-word AND, so `"psa 10"` only matches a lot with both `"psa"` and `"10"` as separate words in its title, not `"psa 100"`.

**How recent can "sold" results be?** Catawiki adds lots to its closed-lot index gradually. In an October 2026 check of about 2,000 lots, the index held nearly all lots closed 10 or more weeks earlier, part of those closed 6-10 weeks earlier, and almost none from the last 6 weeks. Sold lots appear later than unsold ones. For a lot that closed recently and whose ID you know, use `lotIds` mode: prices are available right after a lot closes.

**Why is `detailsAvailable: false` on some lots?** Catawiki's lot detail page redirects to a category page once a lot has been closed for roughly 6 months or more. The price and status fields stay available indefinitely; only the specs/estimate/seller/description add-on fields are affected.

**Why are some results far cheaper than the rest?** Matching is on the lot title, so anything titled with your keywords counts. In a real `"rolex submariner"` run, art prints, an enamel advertising sign, and a LEGO piece sold for €17-50 next to watches at €7,000+. Add `excludeWords` to keep comps clean. In that run, the non-watch lots had titles with words like `art`, `sign`, `lego`, `banksy`, `plate`, and `box`. Check the cheapest rows of your first run to see which words your keyword needs.

**Can I get bid history?** No - Catawiki's bid-history endpoint returns nothing when logged out, even for lots that clearly had bids, so this is not implemented.

**Why did my first track run return nothing?** That's expected, not a failure. The first run for a keyword only has open lots to find - it starts tracking them and the run log says so, with a status message like "Now tracking 40 open lot(s)...". Final prices appear on the next scheduled run, once those lots actually close.

**How are results ordered and picked?** Within a keyword, the Actor picks the lots with the highest lot IDs first, because Catawiki assigns IDs roughly in order, so higher IDs are usually more recent. The picked lots are then output sorted by `biddingEndTime`, newest first. With a large keyword (for example `"pokemon"`, 70,000+ closed lots) and a small `maxResultsPerKeyword`, you get a recent slice, not every match.

### Limitations

1. **Recent sales are missing from keyword search**: "Sold prices" mode finds lots through Catawiki's closed-lot index, which is near-complete only for lots closed about 10 or more weeks ago, partial for 6-10 weeks, and nearly empty for the last 6 weeks (measured October 2026; sold lots lag more than unsold ones). History goes back to 2020. Use `lotIds` mode to price a specific recently closed lot.
2. **Approximate recency ordering**: "newest first" within a keyword is based on lot ID, not the exact `biddingEndTime` - treat it as roughly, not exactly, ordered.
3. **No bid history**: only the final/current price is available, not the sequence of bids.
4. **Lot details age out**: specifications, expert estimate, seller info, and description stop being available once a lot's page redirects away (roughly 6+ months after closing). Price and status fields are not affected by this.
5. **No category filter yet**: matching is keyword/title-based only; there is no category ID filter in this version.
6. **Datacenter proxy dependency**: this Actor is verified against the Apify datacenter proxy. If Catawiki changes its blocking behavior, switch `proxyConfiguration` to residential.
7. **"Track new sales" mode's run spacing matters**: a lot that opens and closes entirely between two runs can be missed if runs are more than about 5 days apart. Running two instances of the same tracker (same keyword and exclude words) at the same time can also overwrite each other's state - schedule one run at a time per tracker.

### Pricing

Pay-per-event pricing. You pay for the lots you get, never for lots that weren't returned. Prices per 1,000 events:

| Event | When it's charged | Free | Starter | Scale | Business and above |
|---|---|---|---|---|---|
| Lot result | Once per lot returned (sold, unsold, or open). In track mode, once per tracked lot whose final result is returned | $3.00 | $2.75 | $2.50 | $2.25 |
| Add-on: Lot details | Once per lot whose detail fields were returned | $6.00 | $5.50 | $5.00 | $4.50 |

On top of that, each run has a fixed Actor start fee of $0.00005 on every plan.

**Typical costs on the Free plan:**

- 50 sold Rolex Submariners: about $0.15
- 200 sold Pokémon lots with details: about $1.80
- A daily `track` run for one keyword that returns 5 newly closed lots: about $0.015 per day, or about $0.45 per month

A run that can't reach Catawiki at all (every session blocked) is charged only the Actor start fee, never for results.

### Output field reference

| Field | Description |
|---|---|
| `lotId` | Numeric Catawiki lot ID |
| `url` | Lot page URL |
| `title` | Lot title (falls back to a title built from the URL slug if unavailable) |
| `status` | `sold`, `unsold`, or `open` |
| `sold` | `true`/`false` for a decided lot, `null` while still open |
| `closed` | Whether bidding has ended |
| `finalPrice` | Sale price in the chosen currency; set only when `status` is `sold` |
| `highestBid` | Current/final highest bid in the chosen currency, for every status |
| `currency` | The currency `highestBid`/`finalPrice` are shown in |
| `priceEUR` / `priceUSD` / `priceGBP` | Highest/final bid amount in each currency, always present |
| `reservePriceMet` | Whether the reserve price was met. `null` means the lot had no reserve (common for "no reserve price" lots) or is still open |
| `biddingStartTime` / `biddingEndTime` | ISO 8601 timestamps |
| `auctionId` | Numeric Catawiki auction ID |
| `favoriteCount` | Number of users who favorited the lot |
| `imageUrl` / `thumbnailUrl` | Lot images |
| `keyword` | The keyword that found this lot (`null` in `lotIds` mode) |
| `scrapedAt` | When this row was scraped, ISO 8601 |
| `detailsAvailable` | Add-on: whether detail fields below were returned |
| `subtitle` | Add-on: lot subtitle |
| `categoryId` / `categoryName` | Add-on: Catawiki category |
| `expertEstimateMin` / `expertEstimateMax` | Add-on: Catawiki expert's estimate range, in `expertEstimateCurrency` |
| `expertEstimateCurrency` | Add-on: currency of the estimate. Catawiki often sets the estimate in one currency only, so this falls back to EUR when the chosen currency has none |
| `sellerId` / `sellerCountry` / `sellerScore` | Add-on: seller info - ID, country, and feedback score only |
| `specifications` | Add-on: array of `{ name, value }` spec pairs |
| `description` | Add-on: plain-text description, HTML stripped, truncated to about 5,000 characters |

### Support

Questions or issues? Open a ticket in the **Issues** tab on the Actor page.

# Actor input Schema

## `mode` (type: `string`):

What to collect. See the section description for what each mode returns.

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

Used in the sold, live, and track modes. Every word must appear as a whole word in the lot title, so "psa 10" won't match "psa 100".

## `lotIds` (type: `array`):

Used in the specific lot IDs mode. Accepts IDs like <code>107000000</code> or lot page URLs.

## `maxResultsPerKeyword` (type: `integer`):

Used in the sold, live, and track modes. In sold/live modes, the newest matching lots are picked first. In track mode, it's the max number of open lots tracked per keyword at once.

## `excludeWords` (type: `array`):

Skip lots whose title contains any of these words. Useful for clean comps, for example <code>art</code>, <code>sign</code>, and <code>lego</code> when pricing real watches. Used in the sold, live, and track modes.

## `includeUnsold` (type: `boolean`):

Also return lots that closed without a sale, with <code>status: "unsold"</code> and <code>finalPrice: null</code>. The specific lot IDs mode always returns every status.

## `soldAfter` (type: `string`):

Only return lots whose auction ended on or after this date. Used in the sold mode.

## `soldBefore` (type: `string`):

Only return lots whose auction ended on or before this date. Used in the sold mode.

## `currency` (type: `string`):

Currency for <code>highestBid</code> and <code>finalPrice</code>. Every row also includes <code>priceEUR</code>, <code>priceUSD</code>, and <code>priceGBP</code>.

## `includeDetails` (type: `boolean`):

Lots closed more than a few months ago return <code>detailsAvailable: false</code> and aren't charged.

## `proxyConfiguration` (type: `object`):

Apify datacenter proxy is the default and works in testing. If runs keep failing to reach Catawiki, switch to residential proxies.

## Actor input object example

```json
{
  "mode": "sold",
  "keywords": [
    "charizard psa 10"
  ],
  "maxResultsPerKeyword": 100,
  "includeUnsold": false,
  "currency": "EUR",
  "includeDetails": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All scraped lot rows

# 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": [
        "charizard psa 10"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("phantom_coder/catawiki-sold-prices-scraper").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": ["charizard psa 10"] }

# Run the Actor and wait for it to finish
run = client.actor("phantom_coder/catawiki-sold-prices-scraper").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": [
    "charizard psa 10"
  ]
}' |
apify call phantom_coder/catawiki-sold-prices-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,phantom_coder/catawiki-sold-prices-scraper"
        }
    }
}
```

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/fIuOC2z2ctOFIxNq6/builds/Cz8pBEBHq1hMzbFzB/openapi.json
