# Tappan Collective Artist & Artwork Scraper (`artsiom_k/tappan-collective-scraper`) Actor

Scrape TappanCollective.com limited-edition fine-art photography/print products (price range, size/edition/frame variants, artist) and a real artist directory with live product counts, with built-in delta mode. No login required.

- **URL**: https://apify.com/artsiom\_k/tappan-collective-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/actors/running/actors-in-store.md#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

## tappan-collective-scraper

Scrapes **artworks** (limited-edition fine-art photography/print products) and a real **artist
directory** from [Tappan Collective](https://www.tappancollective.com), a curated contemporary-art
marketplace representing emerging artists — via the site's own real Shopify product data. No login
required.

### Contents

- [Key features](#key-features)
- [Output](#output)
- [Input](#input)
- [Input examples](#input-examples)
- [Incremental (delta) mode](#incremental-delta-mode)
- [FAQ](#faq)

### 🔑 Key features

- **Two entity types, one Actor.** Set `entityType` to `artworks` or `artists`.
- **Every real purchasable size/edition/frame/mount combination**, not just one price per artwork —
  a single artwork can have dozens of variants (size × edition × frame finish × mount type), each
  with its own price, pulled from Shopify's own structured variant options (not parsed out of a
  compound title string).
- **A real, exhaustive artist directory with live product counts** — unlike some sibling actors in
  this portfolio, Tappan Collective's own `/pages/artists` page is a genuine, complete artist list,
  so `entityType=artists` is a fresh live crawl every run, not an accumulator that only grows as
  `artworks` runs happen to see a given artist.
- **Delta mode built in** for `artworks` — every run classifies each item as `new`, `changed`,
  `unchanged`, or `delisted` against a persisted baseline.
- **Confirmed-dead listings are never re-fetched.** A removed artwork returns a plain HTTP 404 —
  tracked in a persisted registry so a future run skips it without a wasted request.
- **`maxItems` defaults to 50** — a fast, cheap preview, and what keeps an unconfigured run within
  Apify's automated 5-minute QA check. Clear it (`null`) for a full crawl (~8,000 artworks, ~133
  artists as of this build).

### 📋 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.

**Example artwork record:**

```json
{
  "source": "tappan-collective",
  "entity_type": "artworks",
  "external_id": "2180501125",
  "url": "https://www.tappancollective.com/products/nathalie",
  "name": "Nathalie",
  "description": "DESCRIPTION 2016 This Exclusive Tappan Edition is an archival photographic art print...",
  "artist_name": "Gia Coppola",
  "product_type": "Photograph",
  "tags": ["gia-coppola", "artist-roster", "collection-California", "..."],
  "image_urls": ["https://cdn.shopify.com/s/files/..."],
  "price_min": 275.0,
  "price_max": 275.0,
  "currency": "USD",
  "variant_count": 1,
  "variants": [
    {
      "sku": "GC.N_XF_9.45x14",
      "title": "9.45 x 14 inches | Edition of 100 / White / Matted",
      "size_raw": "9.45 x 14 inches",
      "width": 9.45,
      "height": 14,
      "unit": "in",
      "edition_type": "Edition",
      "edition_size": 100,
      "finish": "White",
      "mount_type": "Matted",
      "price": 275.0,
      "currency": "USD",
      "available": true
    }
  ],
  "change_type": "new"
}
```

**Example artist record:**

```json
{
  "source": "tappan-collective",
  "entity_type": "artists",
  "external_id": "gia-coppola",
  "url": "https://www.tappancollective.com/collections/gia-coppola",
  "name": "Gia Coppola",
  "tracked_product_count": 44
}
```

### 🔧 Input

| Field | Type | Default | Description |
|---|---|---|---|
| `entityType` | string | `artworks` | `artworks` or `artists`. |
| `startUrls` | array | — | Specific artwork product URLs to scrape directly instead of the full sitemap. `artworks` only. |
| `maxItems` | integer | `50` | Stop after pushing this many items. Set to `null` for a full crawl. |
| `mode` | string | `auto` | `auto` / `full` / `incremental` — see [Incremental mode](#incremental-delta-mode). |
| `concurrency` | integer | `25` | How many product (or artist collection) pages to fetch in parallel. |
| `impersonate` | string | `chrome` | curl\_cffi TLS-impersonation target. |

### 📥 Input examples

**Default preview (50 artworks):**

```json
{ "entityType": "artworks" }
```

**Full artwork catalog:**

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

**Full artist directory:**

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

### 🔁 Incremental (delta) mode

`auto` mode does a full scan the first time it runs for `entityType=artworks`, then only pushes
new/changed items on later runs (`price_min`/`price_max`/`variant_count` are the watched fields).
Only an uncapped, unscoped run (no `maxItems` limit reached, no `startUrls`, no item errors) can
detect delistings or update the baseline. `entityType=artists` has no delta mode — it's a fresh live
directory crawl every run.

### ❓ FAQ

**Why is structured data (dimensions, edition) parsed from variant options instead of the product
description or tags?**
A real, deliberate choice based on live evidence: this site's own product `tags` are internal
marketing/SEO labels with no consistent schema, and the description text's own structure varies
product to product (different section labels, sometimes no structure at all). Shopify's own variant
`option2`/`option3` fields cleanly and consistently carry frame finish and mount type across every
product checked; only the combined size+edition string (`option1`) needs light parsing, handled
defensively — an unrecognized shape keeps the raw string rather than guessing.

**Why can `tracked_product_count` undercount a very prolific artist?**
It's read from Shopify's own `/collections/{artist}/products.json?limit=250` endpoint, which caps at
250 products per page. An artist with more than 250 listed works would be undercounted. Given ~8,000
total artworks spread across ~133 artists, this is not expected to bind for any single artist, but
hasn't been exhaustively verified for every one.

**Why is `entityType=artists` slow (~4 minutes for the full directory)?**
A real, hard rate limit on the artist-collection endpoint (`/collections/{slug}/products.json`) that
even a lower concurrency plus retries couldn't clear cleanly. Fixed with fully sequential fetching
plus a 1-second pause between requests, which reliably gets all 133 real artists with 0 errors. This
doesn't affect `entityType=artworks`, which uses a completely different endpoint with no rate-limit
issue found even at real scale (7,326 products, concurrency 25, 0 errors).

**Does this need a proxy?**
No — no anti-bot friction was found anywhere on this site while building this Actor (a real burst
test came back 100% clean, and a full large-scale run confirmed it holds at real volume).

# Actor input Schema

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

Scrape artworks (limited-edition photography/print products, with price range and every real size/edition/frame/mount variant) or artists (a real directory crawl with live per-artist product counts — see dataset\_schema.json for the field set of each).

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

Optional list of specific Tappan Collective product URLs (https://www.tappancollective.com/products/{handle}) to scrape directly, instead of discovering via the full sitemap. Only applies to entityType=artworks. A startUrls-scoped run cannot detect delistings or update the incremental baseline.

## `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. The full site has ~8,000 artworks and ~133 artists. Raise this or clear it (set to null) for a full run.

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

"auto" (recommended): full scan on the first run for entityType=artworks, incremental (new/changed only) afterwards. "full": always push every item and refresh the baseline — schedule this periodically to catch delistings and price changes. "incremental": always push only new/changed items. Only a plain, unscoped run (no startUrls, no item errors) can detect delistings or update the baseline. Not used by entityType=artists (a fresh live directory crawl every run, no delta mode).

## `concurrency` (type: `integer`):

How many product detail pages to fetch in parallel for entityType=artworks. Defaults to 25 (this portfolio's standard default) — a real large-scale test (7,326 products) came back 100% clean at this concurrency. Not used by entityType=artists, which always fetches sequentially regardless of this value — the artist-collection endpoint has a real, hard rate limit that even concurrency=5 plus retries couldn't clear cleanly (see README).

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

curl\_cffi browser TLS-impersonation target. Defaults to "chrome" internally.

## Actor input object example

```json
{
  "entityType": "artworks",
  "maxItems": 50,
  "mode": "auto",
  "concurrency": 25
}
```

# 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/tappan-collective-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/tappan-collective-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/tappan-collective-scraper --silent --output-dataset

```

## MCP server setup

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