# Dorotheum.com Auction & Artist Scraper (`artsiom_k/dorotheum-scraper`) Actor

Dorotheum.com auction scraper — real unpaywalled realized prices back to 1998, upcoming estimates, and artist profiles, with built-in delta mode.

- **URL**: https://apify.com/artsiom\_k/dorotheum-scraper.md
- **Developed by:** [Artsiom Kunitsyn](https://apify.com/artsiom_k) (community)
- **Categories:** E-commerce, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.50 / 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## dorotheum-scraper

Scrapes auction lots and artist profiles from [Dorotheum.com](https://www.dorotheum.com) — one of
Europe's oldest auction houses. Real, unpaywalled realized prices for historical sales going back to
1998, upcoming-auction estimates, and the artist directory, all from one Actor.

### Contents

- [Key features](#key-features)
- [Output](#output)
- [Input](#input)
- [Input examples](#input-examples)
- [Incremental (delta) mode](#incremental-delta-mode)
- [How to scrape Dorotheum.com](#how-to-scrape-dorotheumcom)
- [You might also like](#you-might-also-like)
- [FAQ](#faq)

### 🔑 Key features

- **Real hammer prices, completely open.** Unlike Artprice.com's paywalled "Cote" price index,
  Dorotheum publishes every historical lot's realized price with no login and no subscription.
- **Three entity types, one Actor.** `entityType: currentAuctions` (upcoming, not yet resolved),
  `auctionResults` (historical, with real realized prices — the default), or `artists`.
- **Unusually cheap to crawl at scale.** Each auction-event page embeds every lot in that sale
  already priced — one request returns a whole auction's results (confirmed live: 312 lots from a
  single fetch), not one request per lot. A full historical crawl costs on the order of the number
  of *auctions* (~9,300), not the number of lots.
- **Full historical archive in scope by default.** `auctionResults` walks the complete auction
  sitemap back to 1998 when unbounded (`maxItems: null`) — not a recent-years-only sample.
- **Delta mode, tuned per entity type.** `auctionResults` and `artists` default to the usual
  auto-incremental behavior (full scan first run, changes only after). `currentAuctions` always
  fully refreshes every run instead — upcoming-sale estimates change as a sale date approaches, so a
  full picture beats a delta for that one.
- **Honest about what "artist rollup" means here.** `entityType: artists` reports real bio fields
  (name, life dates, nationality) plus a *recent*-activity summary from the artist's own page — not
  a full-catalogue rollup like the other art actors in this account compute. See
  [Output](#output)'s known gaps for why, and what a real one would take.

### 📋 Output

One dataset item per lot or artist, depending on `entityType` — see
[`.actor/dataset_schema.json`](.actor/dataset_schema.json) for the full field list, or the Output
tab's per-entity-type views for a readable table.

**Example lot record** (historical, sold):

```json
{
  "source": "dorotheum",
  "entity_type": "auctionResults",
  "external_id": "dorotheum_10204744",
  "url": "https://www.dorotheum.com/en/l/10204744/",
  "title": "Peter Paul Rubens Nachfolger des 19. Jahrhunderts",
  "auction_name": "Summer auction",
  "auction_date": "2026-07-29",
  "lot_number": "5",
  "price": 1170,
  "currency": "EUR",
  "starting_bid": 900.0,
  "sold": true,
  "change_type": "new"
}
```

**Example artist record:**

```json
{
  "source": "dorotheum",
  "entity_type": "artists",
  "external_id": "dorotheum_artist_alvar-aalto",
  "url": "https://www.dorotheum.com/en/k/alvar-aalto/",
  "name": "Alvar Aalto",
  "birth_year": 1898,
  "death_year": 1976,
  "nationality": "Finland",
  "recent_lot_count": 30,
  "recent_avg_price": 4294.73,
  "change_type": "new"
}
```

Results can be downloaded as JSON, CSV, or Excel from the Console's Output tab, or pulled via the
Apify API/dataset endpoint.

**Known gaps:**

- `price` is only set once a lot is actually sold (`sold: true`) — the same underlying site field
  means "starting bid" instead when a lot hasn't sold, and this Actor doesn't conflate the two.
- `estimate_low`/`estimate_high` are only populated when Dorotheum itself shows a genuine estimate
  *range* for a lot — many lots (especially recent ones) only ever show a single `starting_bid`
  instead; that field is populated far more often and isn't a proxy for a missing estimate.
- `description` is the full free-text field (medium, dimensions, condition are embedded in prose)
  — not split into structured fields. The format varies too widely across 25+ years and every
  object category (paintings, furniture, jewelry, coins, ...) to extract reliably without guessing.
- `entityType: artists`' `recent_*` fields come from that artist's own page, which shows their most
  recent lots only (confirmed capped around 30 for a prolific artist) — not their full historical
  catalogue at Dorotheum. A true full-catalogue rollup would mean cross-referencing every
  `auctionResults` lot ever fetched against the artist — a real follow-up, not built here.
- `artist_name`/`artist_id` are only populated when Dorotheum's own system attributes a lot to a
  named artist — works catalogued as "follower of", "school of", or fully anonymous carry the
  attribution in the free-text `title` instead, with these fields left null.

### ⚙️ Input

See [`.actor/input_schema.json`](.actor/input_schema.json) for the full JSON schema.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `entityType` | String | `auctionResults` | `currentAuctions`, `auctionResults`, or `artists`. |
| `startUrls` | Array of strings | *(none)* | Specific auction URLs (`.../en/a/{id}/`) or artist URLs (`.../en/k/{slug}/`) to scrape directly, instead of the full sitemap-driven crawl. Scope is always `"custom"` — no delisting-detection, no persisted baseline (a hand-picked list is necessarily partial). |
| `maxItems` | Integer | `50` | Stop after pushing this many dataset items (lots or artists). Defaults to a fast, cheap preview (also what keeps an unconfigured run within Apify's automated 5-minute QA check). A full `auctionResults` crawl covers the entire historical archive — raise this or clear it (set to `null`) for that. |
| `mode` | String | `auto` | `auto` (recommended): the usual full-then-incremental behavior for `auctionResults`/`artists`; always `full` for `currentAuctions` regardless of an existing baseline (see Key features). `full`/`incremental` override this per run. |
| `impersonate` | String | `firefox` (internal) | curl\_cffi TLS-impersonation target. Dorotheum's Cloudflare Bot Management challenges plain requests, so this is set internally by default — override only if `firefox` stops working. |
| `proxyConfiguration` | Object | `{"useApifyProxy": false}` | Apify Proxy config. Off by default — this Actor clears Cloudflare fine without one. |

### 🧪 Input examples

**Quick preview of recent historical results** (the default):

```json
{ "entityType": "auctionResults" }
```

**Full historical archive** (exhaustive, slow — every auction event back to 1998, `maxItems: null`
explicitly overrides the 50-item default):

```json
{ "entityType": "auctionResults", "maxItems": null }
```

**What's currently up for auction, fully refreshed:**

```json
{ "entityType": "currentAuctions", "maxItems": null }
```

**Full artist directory:**

```json
{ "entityType": "artists", "maxItems": null }
```

**A specific auction's results:**

```json
{ "entityType": "auctionResults", "startUrls": ["https://www.dorotheum.com/en/a/123940/"] }
```

**Scheduled tracking run** — full, uncapped run (`maxItems` cleared — required for the baseline to
save and delistings to be detected):

```json
{ "entityType": "auctionResults", "mode": "incremental", "maxItems": null }
```

### 🔄 Incremental (delta) mode

Every run classifies each item as `new`, `changed` (price/sold-status moved, for lots; recent
activity moved, for artists), `unchanged`, or `delisted`, using a state baseline persisted in a
named Apify Key-Value Store scoped to `entityType`.

- `auctionResults`/`artists`: `mode: auto` (default) — first run for a scope pushes everything
  (`full`); later runs push only `new`/`changed`/`delisted` (`incremental`).
- `currentAuctions`: `mode: auto` always behaves as `full`, every run — see Key features for why.
- A `startUrls`-scoped run is always partial and never updates the baseline or reports delistings.

Full design: [`docs/incremental-mode.md`](../../docs/incremental-mode.md).

### 🚀 How to scrape Dorotheum.com

1. Open the Dorotheum Auction Results & Artist Scraper in Apify Console and go to the **Input** tab.
2. Pick `entityType` (`currentAuctions`, `auctionResults`, or `artists`).
3. `maxItems` defaults to 50 (a quick preview) — clear it (set to `null`) for a full, uncapped run.
4. Click **Start**.
5. When the run finishes, browse results in the **Output** tab, or download as JSON/CSV/Excel, or
   fetch them via the API.
6. To track over time instead of scraping once: create a **Schedule** with `mode: auto`.

### 🔗 You might also like

- **[Artcurial Auction Results & Artist Scraper](https://apify.com/artsiom_k/artcurial-scraper)** —
  a second Auction houses entry, same open-realized-price idea applied to France's leading
  independent auction house, with both hammer and all-in final prices per lot.
- **[SaatchiArt Artist & Artwork Scraper](https://apify.com/artsiom_k/saatchiart-scraper)**,
  **[Artmajeur Artist & Artwork Scraper](https://apify.com/artsiom_k/artmajeur-scraper)**, and
  **[Artsper Artist & Artwork Scraper](https://apify.com/artsiom_k/artsper-scraper)** — art
  marketplace data (current listings and artist profiles) rather than auction-house realized
  prices, for cross-referencing the same artist's market presence across primary and secondary
  markets.

### ❓ FAQ

**Is it legal to scrape Dorotheum.com?** It's legal to collect publicly available auction-result
data such as lot descriptions, prices, and public artist directory information. Scrape it only with
a legitimate purpose under GDPR.

**How do I get only new/changed items?** Use `mode: auto` (or `incremental`) on a schedule — see
[Incremental mode](#incremental-delta-mode).

**Why is `price` sometimes null even for a lot that's clearly listed on the results page?** That
lot didn't sell — see Known gaps under [Output](#output). Check `starting_bid` instead.

### Search keywords

dorotheum scraper, auction house scraper, auction results scraper, art price data, art market
analytics, realized price data, auction price index, art collector data feed

# Actor input Schema

## `entityType` (type: `string`):

"currentAuctions": upcoming, not-yet-resolved lots (estimates/starting bids only). "auctionResults": historical, resolved lots with real realized prices — the full archive back to 1998 by default. "artists": the artist directory. Each produces a different output shape (see dataset\_schema.json).

## `startUrls` (type: `array`):

Optional list of specific Dorotheum auction URLs (e.g. https://www.dorotheum.com/en/a/123940/) or artist URLs (e.g. https://www.dorotheum.com/en/k/alvar-aalto/) — use whichever kind matches entityType. Scope is always "custom", with no delisting-detection or persisted incremental baseline (a hand-picked list is necessarily partial) — leave empty for the full sitemap-driven crawl instead.

## `maxItems` (type: `integer`):

Stop after pushing this many dataset items (lots or artists). Defaults to 50 — a fast, cheap preview, and what keeps an unconfigured run within Apify's automated 5-minute QA check. A full auctionResults crawl covers the entire historical archive (thousands of auctions, each with many lots) — raise this or clear it (set to null) for that; note a capped run never updates the incremental baseline (see mode below).

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

"auto" (recommended): for auctionResults/artists, full scan on the first run for a scope, incremental (new/changed only) afterwards. For currentAuctions specifically, "auto" always behaves as a full refresh every run regardless of an existing baseline — upcoming auctions are a small, frequently-changing dataset where a full refresh is more useful than a delta. "full": always push every item and refresh the baseline. "incremental": always push only new/changed items. Only a plain, unscoped crawl (no startUrls) can detect delistings or update the baseline.

## `impersonate` (type: `string`):

curl\_cffi browser TLS-impersonation target. Dorotheum's Cloudflare Bot Management challenges plain (non-impersonated) requests, so this defaults to "firefox" internally — confirmed live that "chrome" specifically gets blocked from Apify's own infra while "firefox" clears cleanly (same requests, same infra, only the TLS fingerprint differs). Override only if "firefox" stops working.

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

Apify Proxy configuration. Leave off — confirmed on real Apify infra that default egress clears Cloudflare fine with the default "firefox" impersonation target (see the impersonate field above). An earlier build wrongly blamed IP reputation for a 403 that was actually caused by the "chrome" impersonation target's TLS fingerprint; proxies were tested at the time and didn't help either, consistent with it never having been an IP issue.

## Actor input object example

```json
{
  "entityType": "auctionResults",
  "maxItems": 50,
  "mode": "auto",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("artsiom_k/dorotheum-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("artsiom_k/dorotheum-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 '{}' |
apify call artsiom_k/dorotheum-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,artsiom_k/dorotheum-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/GiIwbj67loQiBvlhG/builds/fBgSfKnYZSxFBQaDB/openapi.json
