# Tcgplayer Scraper (`normdata/tcgplayer-scraper`) Actor

Get TCGplayer card and product data for Pokemon, Magic, Yu-Gi-Oh, and every game on the platform — no API key needed. Real per-seller listings with condition and price, server-side filters by set/rarity/price, and full historical price charts. Search, lookup by ID, or track price history.

- **URL**: https://apify.com/normdata/tcgplayer-scraper.md
- **Developed by:** [Norm Data](https://apify.com/normdata) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $9.80 / 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.
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?

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

![Norm Data](https://i.ibb.co/rGbhM5Y8/Chat-GPT-Image-Sep-8-2026-02-20-50-PM.png)

## TCGplayer Scraper

Get TCGplayer card and product data for **Pokemon, Magic: The Gathering, Yu-Gi-Oh, and every other
trading card game on the platform** with no API key and no login: market price, every seller
listing with condition and reputation, and full historical price and sales-volume charts. Filter
by set, rarity, condition, and price, or look up specific products by ID.

Here is one real row:

```json
{
  "product_id": 106999,
  "product_name": "Charizard - Base Set (Shadowless)",
  "product_url": "https://www.tcgplayer.com/product/106999/Charizard-Base-Set-Shadowless",
  "image_url": "https://product-images.tcgplayer.com/fit-in/400x400/106999.jpg",
  "game": "Pokemon",
  "set_name": "Base Set",
  "set_code": "BS",
  "rarity": "Holo Rare",
  "market_price": 892.5,
  "median_price": 850,
  "lowest_price": 599.99,
  "lowest_price_with_shipping": 610.98,
  "total_listings": 14,
  "listings": [
    {
      "seller_name": "CardShopPro",
      "seller_rating": 99.8,
      "seller_sales": 15234,
      "verified_seller": true,
      "condition": "Near Mint",
      "printing": "Unlimited",
      "price": 599.99,
      "shipping_price": 10.99,
      "total_price": 610.98,
      "quantity": 1
    }
  ],
  "scraped_at": "2026-09-12T00:00:00.000Z"
}
```

### What it does

Reads TCGplayer's own public search and pricing endpoints directly and normalizes every card or
product into a flat row.

- **Search** filters the whole TCGplayer catalog: by name, game, set, rarity, listing condition,
  and price range.
- **Look up** resolves exact product IDs, one row per ID, with the same full detail as search. A
  product that doesn't resolve comes back as an `error` row.
- **Price history** returns a real daily price and sales-volume chart for each product ID, broken
  out by every condition/printing/edition combination TCGplayer tracks separately.

Missing source values are returned as `null`, never invented.

### Why this scraper

- **Real seller listings, not just an aggregate price.** Every listing's condition, printing,
  language, seller name, rating, sales count, and price plus shipping - not just a market-price
  number.
- **Server-side filters that actually narrow the search.** Set, rarity, listing condition, and
  price range all filter at the source, so you never pay for a page of results you then have to
  filter yourself.
- **Real historical price charts.** Daily market price, sale price range, and quantity sold, per
  condition/printing/edition, going back a full quarter - not a short list of recent sales.
- **Works with every TCGplayer game**, not just Pokemon: verified live across Magic: The
  Gathering, Yu-Gi-Oh, Cardfight!! Vanguard, Dragon Ball Super, and more in the same run.
- **A normal `maxItems` cap**, not manual page-and-count math.
- **No API key, no login, no browser.** Reads only what TCGplayer's own site reads.

### How it compares

Checked directly against both other TCGplayer scrapers on Apify (their own declared input/output,
not a guess):

| Capability | This actor | parseforge/tcgplayer-scraper | devcake/tcgplayer-data-scraper |
|---|:--:|:--:|:--:|
| Every TCGplayer game | yes | **Pokemon only** | yes |
| Per-seller listings (condition, rating, price + shipping) | yes | **no** | yes |
| **Filter by set** | **yes** | yes | **no** |
| **Filter by rarity** | **yes** | yes | **no** |
| **Filter by listing condition** | **yes** | **no** | **no** |
| **Filter by price range** | **yes** | **no** | **no** |
| Historical price / volume chart | yes | no | recent sales list only |
| `maxItems` cap | yes | yes | **no (manual page math)** |
| Direct lookup by product ID | yes | no | no |
| Declared dataset schema | yes | yes | **no** |

### Use cases

- **Arbitrage and reselling.** Find the cheapest real listing for a card, by condition, across
  every seller, not just an aggregate low price.
- **Collection valuation.** Price a whole collection by looking up each product ID.
- **Price tracking.** Pull the daily price and volume chart for cards you're watching.
- **Deck-building and budget planning.** Filter competitive staples by price range and set.
- **Market research.** Track price trends by set, rarity, or game over time.
- **Inventory pricing for a card shop.** Keep listings priced against real current market data.

### Quickstart

Search: Pokemon cards from Base Set with Holo Rare rarity, priced $10-500, Near Mint listings only.

```json
{ "mode": "search", "game": "Pokemon", "setName": "Base Set", "rarityName": "Holo Rare", "condition": "Near Mint", "priceMin": 10, "priceMax": 500, "maxItems": 100 }
```

Search: Magic: The Gathering cards matching a name, cheapest first.

```json
{ "mode": "search", "query": "black lotus", "game": "Magic: The Gathering", "sortField": "market-price", "sortOrder": "asc", "maxItems": 50 }
```

Look up specific products:

```json
{ "mode": "lookup", "productIds": [106999, 654213] }
```

Price history for the last quarter:

```json
{ "mode": "priceHistory", "productIds": [106999], "priceHistoryRange": "quarter" }
```

### Input reference

| Field | Applies to | Description |
|---|---|---|
| `maxItems` | all | Ceiling on rows written. |
| `mode` | all | `search`, `lookup`, or `priceHistory`. |
| `query` | search | Card or product name contains this text. |
| `game` | search | TCGplayer's own game name, e.g. Pokemon, Magic: The Gathering, YuGiOh. |
| `setName` | search | Exact TCGplayer set name. |
| `rarityName` | search | Exact TCGplayer rarity name; varies by game. |
| `condition` | search | Only return seller listings in this condition, e.g. Near Mint. |
| `priceMin` / `priceMax` | search | Market price range, USD. |
| `foilOnly` | search | Only foil-only prints. |
| `sortField` / `sortOrder` | search | Sort by market price (verified), best-selling, or product name. |
| `productIds` | lookup, priceHistory | TCGplayer product IDs (the number in a product's URL). |
| `priceHistoryRange` | priceHistory | `month` or `quarter` - the only two ranges TCGplayer's own endpoint supports. |

### Output reference (selected)

| Field | Description |
|---|---|
| `product_id` / `product_name` / `product_url` / `image_url` | Identity. |
| `game` / `set_name` / `set_code` / `rarity` | Classification. |
| `market_price` / `median_price` / `lowest_price` / `lowest_price_with_shipping` | Pricing. |
| `total_listings` | Total active seller listings for this product. |
| `listings` | `{ seller_name, seller_rating, seller_sales, verified_seller, gold_seller, condition, printing, language, price, shipping_price, total_price, quantity, listed_date }[]`. |
| `custom_attributes` | Game-specific card attributes (HP, energy type, stage, attack text, etc.), when TCGplayer has them. Cleaned of the display markup (`<strong>`, `<br>`, etc.) TCGplayer's own API embeds directly in the text. |
| `range` / `skus` | Price-history mode: the requested range, and one entry per condition/printing/edition with a dated `buckets[]` series. |
| `query` / `error` | Set on an unresolved look-up row. |
| `scraped_at` | ISO 8601 collection timestamp. |

### Run via API and CLI

```bash
curl -X POST "https://api.apify.com/v2/acts/USERNAME~tcgplayer-scraper/run-sync-get-dataset-items?token=<TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"mode":"lookup","productIds":[106999]}'
```

```bash
apify call USERNAME/tcgplayer-scraper --input '{"mode":"search","game":"Pokemon","rarityName":"Holo Rare","maxItems":500}'
```

### Fetch results

```bash
curl "https://api.apify.com/v2/datasets/<DATASET_ID>/items?token=<TOKEN>&clean=true&format=json"
curl "https://api.apify.com/v2/datasets/<DATASET_ID>/items?token=<TOKEN>&clean=true&format=csv"
```

### Billing and limits

- **Pay per result.** Billed per row written (one row per product, or one row per product ID in
  price-history mode).
- **No charge on failure.** A run that produces zero rows fails with a message and is not billed.
- An unresolved look-up or price-history product ID still writes one `error: "not found"` row and
  is billed as a base row.
- **Free Apify plans** run the built-in 10-row sample only.

### FAQ and troubleshooting

**Do I need a TCGplayer API key?** No. TCGplayer's own public search and pricing endpoints are
read directly, with no key and no login.

**Why did my search return zero rows?** No product matched the filters. Loosen them, check the
exact spelling of `setName`/`rarityName` (TCGplayer's own values, case-sensitive), or widen the
price range.

**Why is `market_price` null for some products?** TCGplayer only computes a market price when it
has enough recent sales data. An obscure or rarely-traded product can genuinely have none. This
Actor reports `null` rather than guessing.

**Why is `median_price` null much more often than `market_price`?** Verified directly against
TCGplayer's own raw response (about 1 in 4 products carry it, checked at scale): TCGplayer
computes a median far less often than a market price, most often skipping it for lower-volume
single cards. When it's missing, TCGplayer's own response doesn't include the field at all - not
a gap in how this Actor reads it.

**Why is `product_type` usually empty?** TCGplayer's own search endpoint doesn't return this
field for the large majority of products (confirmed at scale, not a parsing gap). It's kept in
the output for the products where it is present.

**Where does the product image come from?** TCGplayer's own search results carry no product
image field at all. `image_url` is built directly from `product_id` against TCGplayer's own
image CDN (`product-images.tcgplayer.com`), verified to return a real image for every product ID
tested.

**Why do `listings` and `custom_attributes` show up as raw text in CSV or Excel?** Those formats
have no way to represent a nested list or object inside one cell, so they're serialized to a JSON
string there - the same as every nested field on any Apify Actor. In the Apify Console's dataset
table, and in the JSON export, they render as proper structured data.

**Can I get a price history longer than a quarter?** No. TCGplayer's own history endpoint only
supports `month` and `quarter` ranges.

**Why can a large `search` run take longer than the result count suggests?** TCGplayer's own
relevance search doesn't paginate against a perfectly stable view of the catalog, so the same
product can appear on more than one page. This Actor drops repeats and keeps fetching until it
has enough *distinct* rows, not just enough raw pages - a large `maxItems` can take a little
longer as a result. The alternative (not de-duplicating) would be faster but wrong.

**How current is the data?** Read live from TCGplayer's own endpoints on every run. It is not
cached.

**Is this an official TCGplayer tool?** No. Independent, not affiliated with TCGplayer. It reads
only publicly available data.

### Local development

```bash
bun install
bun test                       # offline: normalisation, filter-building, input logic (real fixtures)
bun run src/main.ts            # reads storage/key_value_stores/default/INPUT.json
```

# Actor input Schema

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

Caps how many rows this run writes. Starts at 10 for a quick sample; raise it for a full run.

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

Search filters TCGplayer's own catalog. Look up resolves exact product IDs. Price history returns daily price/volume series per product ID.

## `query` (type: `string`):

Match products whose name contains this text.

## `game` (type: `string`):

TCGplayer's own game name, e.g. Pokemon, Magic: The Gathering, YuGiOh, Flesh and Blood, Digimon Card Game, One Piece Card Game. Leave empty for all games.

## `setName` (type: `string`):

Exact TCGplayer set name, e.g. Base Set, Base Set Unlimited.

## `rarityName` (type: `string`):

Exact TCGplayer rarity name, e.g. Holo Rare, Common, Secret Rare. Varies by game.

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

Only return seller listings in this condition, e.g. Near Mint, Lightly Played. Leave empty for all conditions.

## `priceMin` (type: `integer`):

Only products with a market price at or above this.

## `priceMax` (type: `integer`):

Only products with a market price at or below this.

## `foilOnly` (type: `boolean`):

Only products flagged as foil-only prints.

## `sortField` (type: `string`):

Leave as relevance unless you specifically want price or name order.

## `sortOrder` (type: `string`):

Only applies when Sort by is not Relevance.

## `productIds` (type: `array`):

TCGplayer product IDs, the number in a product's URL, e.g. 106999 for tcgplayer.com/product/106999/....

## `priceHistoryRange` (type: `string`):

How far back the price/volume chart goes. TCGplayer's own price-history endpoint only supports these two ranges.

## Actor input object example

```json
{
  "maxItems": 10,
  "mode": "search",
  "query": "charizard",
  "game": "Pokemon",
  "foilOnly": false,
  "sortOrder": "asc",
  "priceHistoryRange": "month"
}
```

# Actor output Schema

## `listings` (type: `string`):

Complete dataset with all scraped TCGplayer products.

## `overview` (type: `string`):

Overview view with key fields displayed in a table.

# 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 = {
    "maxItems": 10,
    "mode": "search",
    "query": "charizard",
    "game": "Pokemon"
};

// Run the Actor and wait for it to finish
const run = await client.actor("normdata/tcgplayer-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 = {
    "maxItems": 10,
    "mode": "search",
    "query": "charizard",
    "game": "Pokemon",
}

# Run the Actor and wait for it to finish
run = client.actor("normdata/tcgplayer-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 '{
  "maxItems": 10,
  "mode": "search",
  "query": "charizard",
  "game": "Pokemon"
}' |
apify call normdata/tcgplayer-scraper --silent --output-dataset

```

## MCP server setup

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