# Artsper.com Artist & Artwork Scraper (`artsiom_k/artsper-scraper`) Actor

Artsper.com artist & artwork scraper with real per-artist rollup stats (avg price, medium mix, sold count) from each artist's full catalog, plus delta mode.

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

## Pricing

from $5.00 / 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

## artsper-scraper

Scrapes both **artworks** and **artist profiles** from [Artsper.com](https://www.artsper.com),
including per-artist rollup stats (average price, medium mix, sold count, latest artwork) computed
from the artist's full catalog — not a sample.

### Contents

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

### 🔑 Key features

- **Two entity types, one Actor.** Set `entityType` to `artworks` or `artists` — no need for two
  separate scrapers to build a full picture of an artist and their work.
- **Real per-artist rollup stats, not a sample.** Average price, unique medium mix, sold-artwork
  count, and latest artwork are computed from an artist's **entire** catalog, enumerated via
  Artsper's own catalog pagination (paginated to completion), not just whatever a single search
  page happens to show.
- **Delta mode built in.** Every run classifies each item as `new`, `changed`, `unchanged`, or
  `delisted` against a persisted baseline — pay for what changed, not a full re-scrape every time.
- **Exhaustive when you ask for it.** With no `startUrls`/`artistUrls`, discovery is the full site
  sitemap — every artwork, every artist — but `maxItems` defaults to 50 (a fast preview, and what
  keeps an unconfigured run within Apify's automated 5-minute QA check), so clear it (`null`) for a
  real full crawl.
- **Rich data straight from the site's own structured markup.** Every artwork/artist page ships
  clean Schema.org JSON-LD (price, medium, dimensions, artist nationality, birth year, full
  biography) — read directly rather than scraped from display text, so it doesn't break on cosmetic
  page redesigns the way plain-HTML scraping would.
- **A verified, trackable artist list.** `artistUrls` lets you scrape exactly the artists you care
  about — each URL is checked against the real artist sitemap first (typos/removed artists are
  logged and skipped), and unlike a generic `startUrls` list, it gets its own persisted tracking
  scope: run the same list on a schedule and get real new/changed/delisted detection for just those
  artists.
- **Progress visible in the run log.** Every ~10 items or ~20 seconds, the log reports how many
  items have been processed (with a known total for bounded runs), pushed, and errored, plus the
  observed rate — and for `entityType: artists`, a running count of artworks fetched across all
  rollups so far.
- **Confirmed-gone URLs are never re-fetched.** Artsper.com doesn't return an HTTP error for a
  removed artwork — it silently serves the generic catalog page instead (still `200 OK`). This
  Actor detects that content signal, persists it, and skips the URL on every future run — no input
  needed to enable it.
- **Resilient to a real, confirmed rate limit — bounded, not stuck.** Artsper.com's Cloudflare
  protection sometimes throttles a large artist rollup (occasional 429s that mostly resolve on
  retry, just slowly). Rather than one artist's catalog silently stalling a run for a long time, a
  wall-clock budget cuts a rollup short after 180 seconds and still returns a real, useful partial
  result (flagged via `rollup_rate_limited` in the run log), and a separate failure-count breaker
  catches a harder, sustained block the same way.

### 📋 Output

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

**Example artwork record:**

```json
{
  "source": "artsper",
  "entity_type": "artworks",
  "external_id": "artsper_2462506",
  "url": "https://www.artsper.com/fr/oeuvres-d-art-contemporain/peinture/2462506/toile-lamour-dans-la-nature-jerome-mesnager",
  "title": "Toile L'amour dans la nature",
  "artist_name": "Jérôme Mesnager",
  "artist_url": "https://www.artsper.com/us/contemporary-artists/france/17/jerome-mesnager",
  "category": "Painting",
  "medium": "acrylic",
  "surface": "canvas",
  "date_created": "2022",
  "price": 2800,
  "currency": "EUR",
  "availability_status": "InStock",
  "width_cm": 81,
  "height_cm": 100,
  "condition": "Artwork sold in perfect condition, framed, ready to hang",
  "signature": "Hand-signed by artist",
  "gallery_name": "At Down",
  "gallery_country": "France",
  "change_type": "new"
}
```

**Example artist record:**

```json
{
  "source": "artsper",
  "entity_type": "artists",
  "external_id": "artsper_artist_17",
  "url": "https://www.artsper.com/fr/artistes-contemporains/france/17/jerome-mesnager",
  "name": "Jérôme Mesnager",
  "nationality": "France",
  "birth_year": 1961,
  "total_artworks": 45,
  "sold_artworks": 0,
  "avg_price": 1037.0,
  "mediums": "Fine Art Drawings, Painting, Print, Sculpture",
  "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:**

- `sold_artworks`/`sold_avg_price` are computed defensively from any `availability_status` other
  than `InStock`, but no genuinely sold-but-still-listed artwork was found while building this
  Actor — every old listing checked live was still `InStock`, and a truly removed one disappears
  from the catalog entirely rather than staying visible as "sold" (see the known-gone handling
  above). If Artsper never actually surfaces a non-`InStock` status this way, these fields will
  simply read `0`/`null` — an honest reflection of what's observable, not a guess.
- `price`/`currency` are native, unconverted values, and the artist rollup's `avg_price`/
  `sold_avg_price`/`latest_artwork_price` assume a single currency across an artist's whole catalog
  (the common case) — mixed currencies aren't detected or converted.
- `latest_artwork_*` assumes the catalog's default sort is newest-first; not independently
  confirmed against a date field.
- A large artist's rollup can be cut short by Cloudflare rate-limiting (see Key features) — when
  that happens, `total_artworks`/`avg_price`/etc. reflect only the artworks fetched before the
  180-second budget ran out, not the artist's full catalog. Check the run log for a
  "cut short by sustained rate-limiting" warning if a rollup's numbers look lower than expected.

### ⚙️ Input

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

| Parameter | Type | Default | Description |
|---|---|---|---|
| `entityType` | String | `artworks` | `artworks` or `artists`. |
| `startUrls` | Array of strings | *(none)* | Specific artist/artwork URLs, or a category/search catalog URL, to scrape directly instead of discovering via the full site sitemap. Scope is always `"custom"`, with no persisted tracking across runs — for persisted per-artist tracking, use `artistUrls` instead. A catalog URL's unfiltered form self-caps at 100 pages regardless of true match count — narrow with a category/filter for large result sets. |
| `artistUrls` | Array of strings | *(none)* | Artists only. Paste artist URLs, one per line (a `.txt` file's contents paste in directly, no upload needed) to scrape exactly those artists + their artworks. Verified against the real artist sitemap first (see `verifyArtistUrls`); gets its own persisted scope, so re-running the same list on a schedule gives real new/changed/delisted tracking for just those artists. Takes priority over `startUrls`. |
| `verifyArtistUrls` | Boolean | `true` | When `artistUrls` is set, check each URL against the real sitemap first (roughly a minute, one full sitemap walk, regardless of list size) and skip any not found. Turn off to scrape the list unverified. |
| `maxItems` | Integer | `50` | Stop after pushing this many dataset items. Defaults to a fast, cheap preview (also what keeps an unconfigured run within Apify's automated 5-minute QA check). A full crawl covers the entire site — raise this or clear it (set to `null`) for that; note a capped run never updates the incremental baseline (see below), so any real tracking run needs this cleared. |
| `mode` | String | `auto` | `auto` (recommended): full scan on the first run, incremental after. `full`: always push every item and refresh the baseline. `incremental`: always push only new/changed items. Only a plain, unscoped sitemap crawl or a verified `artistUrls` list can detect delistings or update the baseline. |
| `impersonate` | String | `chrome` (internal) | curl\_cffi TLS-impersonation target. Artsper's Cloudflare protection challenges plain requests, so this is set internally by default — override only if that stops working. |
| `proxyConfiguration` | Object | `{"useApifyProxy": false}` | Apify Proxy config. Off by default — the sitemap/profile/detail pages this Actor relies on worked fine without one while building it. |

### 🧪 Input examples

**Full artwork catalog scan** (exhaustive, slow — every artwork's detail page is fetched;
`maxItems: null` explicitly overrides the 50-item default):

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

**Bounded preview** (the default — fast, but not a curated subset, just the first N artworks the
sitemap yields):

```json
{ "entityType": "artworks", "maxItems": 200 }
```

**Full artist directory, with rollup stats:**

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

**A specific, tracked list of artists** (verified against the sitemap, own persisted scope —
`maxItems: null` so the whole list is scraped even if it's longer than the 50-item default):

```json
{
  "entityType": "artists",
  "artistUrls": [
    "https://www.artsper.com/fr/artistes-contemporains/france/17/jerome-mesnager"
  ],
  "maxItems": null
}
```

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

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

### 🔄 Incremental (delta) mode

Every run classifies each item as `new`, `changed` (price or availability moved, for artworks;
catalog size/sold-count/avg-price moved, for artists), `unchanged`, or `delisted`, using a state
baseline persisted in a named Apify Key-Value Store scoped to `entityType`.

- `mode: auto` (default) — first run for a scope pushes everything (`full`); later runs push only
  `new`/`changed`/`delisted` (`incremental`).
- A `startUrls`-scoped run is always partial and never updates the baseline. `artistUrls` is
  different: it's treated as a complete, closed universe (like a full sitemap crawl), so its
  baseline does get saved and delisted-detection does run — as long as no item hit a genuinely
  ambiguous error that run (a confirmed-gone URL doesn't count against this — see below; a timeout,
  5xx, or parse error does, and skips the baseline-save/delisted-detection that run to avoid false
  "delisted" reports).
- Every bounded run (`startUrls` or `artistUrls`) logs a quick "X new, Y already tracked, Z
  already known-dead" preview right after startup, before the full fetch loop begins —
  new/already-tracked is informational only (every URL is still fully fetched and classified
  regardless, real price/status-change detection isn't skipped for "already tracked" items);
  already-known-dead URLs genuinely are skipped, see below.
- A URL confirmed gone is recorded and never fetched again on any future run, regardless of `mode`
  or scope — logged distinctly from other errors. Doesn't count toward the baseline-save/
  delisted-detection gate above, since it's a trustworthy, already-understood outcome rather than
  an ambiguous failure.

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

### 🚀 How to scrape Artsper.com

1. Open the Artsper Artist & Artwork Scraper in Apify Console and go to the **Input** tab.
2. Pick `entityType` (`artworks` 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` — the
   first run does a full scan, every run after only bills what actually changed.

### 🔗 You might also like

- **[SaatchiArt Artist & Artwork Scraper](https://apify.com/artsiom_k/saatchiart-scraper)** — the
  same entity-type/rollup-stats design pointed at a second, independent art marketplace, for
  expanding artist coverage or cross-referencing the same artist's presence and pricing across
  platforms.
- **[Artmajeur Artist & Artwork Scraper](https://apify.com/artsiom_k/artmajeur-scraper)** — a third,
  independent art marketplace source, same design.

### ❓ FAQ

**Is it legal to scrape Artsper.com?** It's legal to collect publicly available marketplace data
such as artwork listings, prices, and public artist profile 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 don't sold artworks show a `sold` status more often?** See Known gaps under
[Output](#output) — no confirmed live example of a sold-but-still-listed artwork turned up while
building this Actor.

### Search keywords

artsper scraper, art marketplace scraper, artist data scraper, artwork data scraper, art price
data, art market analytics, art collector data feed

# Actor input Schema

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

Scrape artworks or artist profiles. Each produces a different output shape (see dataset\_schema.json).

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

Optional list of specific Artsper artist or artwork URLs, or a category/search catalog URL (e.g. https://www.artsper.com/fr/oeuvres-d-art-contemporain/peinture), to scrape directly instead of discovering via the full site sitemap — use whichever kind matches entityType. A catalog URL is paginated to completion but is never treated as a full, delisting-aware scan (see Incremental mode below); the unfiltered catalog also self-caps at 100 pages regardless of true match count, so narrow with a category/filter for large result sets. For artists specifically, consider artistUrls below instead: it checks each URL against the site's artist sitemap and gets its own persisted tracking across repeat runs.

## `artistUrls` (type: `array`):

Paste a list of specific Artsper artist URLs (one per line — paste a .txt file's contents directly, no upload needed) to scrape exactly those artists and their artworks, instead of crawling the full site. Each URL is checked against the real artist sitemap first (found/not-found is logged; not-found ones are skipped) unless verifyArtistUrls is off. Unlike startUrls, this list gets its own persisted tracking scope — re-running the same list on a schedule gives real new/changed/delisted detection for just those artists. Ignored when entityType is "artworks" or startUrls is set.

## `verifyArtistUrls` (type: `boolean`):

When artistUrls is set, check each URL against the real artist sitemap before scraping (adds one full sitemap walk, roughly a minute, regardless of list size) and skip any not found. Turn off to scrape the list as given, unverified.

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

Stop after pushing this many dataset items. Defaults to 50 — a fast, cheap preview, and what keeps an unconfigured run within Apify's automated 5-minute QA check. A full, uncapped sitemap crawl covers the entire site — hundreds of thousands of artworks, or the full artist directory — raise this or clear it (set to null) for that.

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

"auto" (recommended): full scan on the first run for a given entityType/scope, incremental (new/changed only) afterwards. "full": always push every item and refresh the baseline — schedule this periodically to catch delistings. "incremental": always push only new/changed items. Only a plain, unscoped sitemap crawl (no startUrls) or a verified artistUrls list can detect delistings or update the baseline — see docs/incremental-mode.md.

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

curl\_cffi browser TLS-impersonation target. Artsper.com's Cloudflare protection challenges plain (non-impersonated) requests, so this defaults to "chrome" internally — override only if that stops working.

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

Apify Proxy configuration. Leave off — this Actor clears Cloudflare fine without one, and neither Apify Proxy group tested during development actually helped: Residential wasn't provisioned on the account tested (immediate HTTP 403), and the datacenter group available was reputation-blocked outright by Cloudflare (also HTTP 403, worse than no proxy). A large artist rollup may still hit occasional rate-limiting either way — see the README's Known gaps.

## Actor input object example

```json
{
  "entityType": "artworks",
  "verifyArtistUrls": true,
  "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/artsper-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/artsper-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/artsper-scraper --silent --output-dataset

```

## MCP server setup

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