TCG Vendor Base API
A read API for trading-card catalog data and market prices — cards and sealed products across multiple games, sourced from TCGplayer via tcgcsv. Prices in USD with a daily MYR snapshot.
Overview
Base URL
https://api.tcgvendorbase.com
All endpoints live under /api/v1/, accept GET only (except /cards/scan), and return JSON. Every request needs a bearer token from a registered app (see below). Responses are edge-cached, so repeat reads are fast.
curl -H "Authorization: Bearer YOUR_ID_TOKEN" \
"https://api.tcgvendorbase.com/api/v1/cards?q=charizard&game=pokemon"
Authentication
Send your end user's identity token as a bearer token on every call. There is no shared API key — the X-API-Key header was retired and is no longer accepted.
Authorization: Bearer YOUR_ID_TOKEN
Each consuming app is registered by its token issuer. Both Supabase Auth and Firebase Auth are supported; the API verifies the token against that issuer's public keys, so no secret is shared and nothing sensitive ships in a browser bundle. Contact the API owner to register an app.
Registration also fixes the app's scopes. A token is valid but still refused if its app lacks the scope an endpoint requires:
| Scope | Grants |
|---|---|
cards:read | /cards, /cards/{id}, /sets/{id}/cards |
prices:read | /prices, and ?with_prices=1 on any endpoint |
sealed:read | /sealed, /sealed/{id} |
sets:read | /sets |
scan | /cards/scan |
| Condition | Response |
|---|---|
| Header missing | 401 {"error":"missing Authorization bearer token"} |
| Issuer not registered | 401 {"error":"unrecognized token issuer"} |
| Bad signature / wrong audience | 401 {"error":"invalid token"} |
Token past exp | 401 {"error":"token expired"} |
| App lacks the scope | 403 {"error":"missing scope: …"} |
| Over the per-minute limit | 429 {"error":"rate limit exceeded"} |
getIdToken() / getSession()) rather than caching the string.Games & languages
Every product carries a game and a lang. Search endpoints accept both as optional filters (omit to search across all).
Currently available
pokemon
lang = en
lang = ja
lang=ja — e.g. ?q=charizard&lang=ja.Caching & prices
- Caching — GET responses are cached at the edge (
s-maxage=3600,stale-while-revalidate=86400). Expect data up to ~1 hour old. - Currency —
*_usdfields come straight from TCGplayer (USD).market_myris a snapshot computed at ingest usingfx_rate(USD→MYR). - Freshness — prices are refreshed from the source about daily;
captured_atis the source's publish time. - Variants — a product has one price row per
sub_type_name(e.g.Holofoil,Normal,Reverse Holofoil, orUnopenedfor sealed). - Prices — fetch with a batch /prices call using the
product_ids returned by a search.
Try it — live console
Requests run against this same API (same origin, so no CORS setup needed). Your token is sent only to this API; tick remember to keep it in this browser only. Inputs are pre-filled with working examples.
—Search cards
Fuzzy search over single cards, matching the card name, collector number and set name. 20 results per page, best match first.
| Param | Type | Notes |
|---|---|---|
q required* | string | Search text, matched against the card name, its collector number and its set name together — so q=voltorb gg01 and q=voltorb crown zenith both find the right card. A trailing number is additionally treated as the collector number, so q=charizard 4/102 ≡ q=charizard&number=4/102, and a query that is itself a number (e.g. q=021/187 or q=21) is a number-only lookup (an explicit number takes precedence). *Required unless number is given — pass either or both. |
game optional | string | e.g. pokemon. Omit for all games. |
lang optional | string | en or ja. |
number optional | string | Collector number, e.g. 025/078. Matches the part before the slash, ignoring leading zeros — 25, 025 and 025/078 all match a card numbered 025/078. Can be used on its own (without q) to look up by number alone. |
page optional | int | 1-based; default 1. |
Example
curl -H "Authorization: Bearer YOUR_ID_TOKEN" \
"https://api.tcgvendorbase.com/api/v1/cards?q=charizard&game=pokemon&number=4/102"
{
"page": 1,
"total": 161,
"results": [
{
"product_id": 84189,
"group_id": 1863,
"product_type": "card",
"lang": "en",
"game": "pokemon",
"name": "Charizard",
"card_number": "4/102",
"image_url": "https://...",
"tcgplayer_url": "https://...",
"set_name": "SM Base Set",
"similarity": 0.6
}
]
}
Note on similarity: it measures the query against the card name only, while matching and ranking also consider the collector number and set name. A result can therefore be highly relevant yet carry a low similarity — q=crown zenith returns that set's cards with similarity near 0, since the query words are in the set name, not the card name. Use it to compare results within one response; don't threshold on it to decide what to show.
Get a card
Full detail for one card by its product_id. Returns 404 if the id is not a card (e.g. it's a sealed product).
curl -H "Authorization: Bearer YOUR_ID_TOKEN" \
"https://api.tcgvendorbase.com/api/v1/cards/84189"
{
"product_id": 84189,
"group_id": 1863,
"game": "pokemon",
"lang": "en",
"product_type": "card",
"name": "Charizard",
"card_number": "4/102",
"image_url": "https://...",
"tcgplayer_url": "https://...",
"extended": [
{ "name": "Number", "displayName": "Number", "value": "4/102" },
{ "name": "Rarity", "displayName": "Rarity", "value": "Holo Rare" }
]
}
Search sealed
Same as Search cards but limited to sealed products (booster boxes, ETBs, bundles, …). Accepts q (required), game, lang, page.
curl -H "Authorization: Bearer YOUR_ID_TOKEN" \
"https://api.tcgvendorbase.com/api/v1/sealed?q=booster%20box&game=pokemon"
Get a sealed product
Detail for one sealed product by product_id. Returns 404 if the id is not sealed. Same response shape as Get a card (with product_type: "sealed").
Search all products
Searches cards and sealed together; each result carries its own product_type. Use the optional type filter to narrow.
| Param | Type | Notes |
|---|---|---|
q required | string | Search text, matched against the product name, its collector number and its set name. |
type optional | string | card or sealed. Omit for both. |
game optional | string | e.g. pokemon. |
lang optional | string | en or ja. |
page optional | int | default 1. |
List sets
Every set (TCGplayer group), newest published first. Use a set's group_id to list its cards via Cards in a set.
| Param | Type | Notes |
|---|---|---|
lang optional | string | en or ja. Omit for all languages. |
Example
curl -H "Authorization: Bearer YOUR_ID_TOKEN" \
"https://api.tcgvendorbase.com/api/v1/sets?lang=en"
{
"results": [
{
"group_id": 1863,
"name": "SM Base Set",
"abbreviation": "SM",
"lang": "en",
"category_id": 3,
"published_on": "2017-02-03"
}
]
}
Cards in a set
Every card in a set by its group_id, paginated 60 per page and ordered by collector number. Same product shape as Search cards (without similarity).
| Param | Type | Notes |
|---|---|---|
id required | int | Set group_id (path segment). |
page optional | int | 1-based; default 1. 60 results per page. |
Example
curl -H "Authorization: Bearer YOUR_ID_TOKEN" \
"https://api.tcgvendorbase.com/api/v1/sets/1863/cards?page=1"
{
"page": 1,
"total": 102,
"results": [
{
"product_id": 84189,
"game": "pokemon",
"lang": "en",
"name": "Charizard",
"card_number": "4/102",
"image_url": "https://...",
"image_url_fallback": "https://...",
"tcgplayer_url": "https://...",
"set_name": "SM Base Set"
}
]
}
Batch prices
Current prices for up to 200 products in one call. Returns one row per variant; works for cards and sealed alike.
| Param | Type | Notes |
|---|---|---|
ids required | string | Comma-separated product_ids. Max 200. |
picked optional | string | auto collapses each product to a single variant by priority (Holofoil > Normal > Reverse Holofoil, else first priced). |
history optional | bool | 1 returns the daily price series instead of current prices. Max 10 ids in this mode. Ignores picked. |
days optional | int | Window for history=1. Default 90, max 730. |
spark optional | bool | 1 returns chart-ready series for the card detail page — all five windows in one call. Max 5 ids. Takes precedence over history. |
curl -H "Authorization: Bearer YOUR_ID_TOKEN" \
"https://api.tcgvendorbase.com/api/v1/prices?ids=84189,91601"
{
"results": [
{
"product_id": 84189,
"sub_type_name": "Holofoil",
"product_type": "card",
"lang": "en",
"game": "pokemon",
"market_usd": 138.08,
"market_myr": 547.86,
"low_usd": 105.00,
"mid_usd": 572.12,
"high_usd": 1390.55,
"direct_low_usd": 999.99,
"fx_rate": 3.9677,
"captured_at": "2026-05-30T20:04:57+00:00"
}
]
}
Price history
One series per variant, oldest first. A row is appended only on the days the market price actually changed.
curl -H "Authorization: Bearer YOUR_ID_TOKEN" \
"https://api.tcgvendorbase.com/api/v1/prices?ids=84189&history=1&days=90"
{
"days": 90,
"results": [
{ "product_id": 84189, "sub_type_name": "Holofoil", "d": "2026-05-22", "market_usd": 131.40, "market_myr": 533.52 },
{ "product_id": 84189, "sub_type_name": "Holofoil", "d": "2026-06-04", "market_usd": 138.08, "market_myr": 560.63 }
]
}
Reading the series. Three things will trip you up if you treat it as a dense daily feed:
- Gaps mean the price held steady. A row is only written when the market price actually moves, so consecutive dates are usually days or weeks apart. Draw it as a step line, carrying each value forward until the next point.
- The first point may predate your window. If a card's price last moved before
daysago, that older point is still returned as the opening value — otherwise the chart would render empty for a card that simply hasn't moved. Itsdcan therefore be older thandays. - A null
market_usdis meaningful. It marks the day the card stopped having a market price at all, which is different from holding steady.
market_myr is converted at that day's USD→MYR rate, not today's, so the series is comparable over time. Weekend and holiday dates use the last published weekday rate.
Chart series (spark=1)
Built for a card detail page: one request serves all five windows. Two series come back per printing, and their point counts nest, so switching window is a slice of what you already have — no second request.
curl -H "Authorization: Bearer YOUR_ID_TOKEN" \
"https://api.tcgvendorbase.com/api/v1/prices?ids=274439&spark=1"
{
"as_of": "2026-08-19",
"windows": {
"7d": { "series": "daily", "take": 7 },
"30d": { "series": "daily", "take": 30 },
"3m": { "series": "weekly", "take": 13 },
"6m": { "series": "weekly", "take": 26 },
"1y": { "series": "weekly", "take": 53 }
},
"results": [
{
"product_id": 274439,
"sub_type_name": "Holofoil",
"daily": { "covered_since": "2026-07-21", "points": [ ... 30 points ... ] },
"weekly": { "covered_since": "2026-01-07", "points": [
{ "d": "2026-08-19", "close_usd": 4.34, "low_usd": 3.64,
"high_usd": 4.42, "close_myr": 17.61, "fx_rate": 4.0579 }
]}
}
]
}
Rendering it. Take the last take points of the named series — e.g. 6m is the last 26 weekly points. Plot close_usd as the line.
- Use
low_usd/high_usd, don't ignore them. They are the extremes actually reached inside that bucket. On weekly points the close can understate the week's high by 30–40%, so a line drawn from closes alone hides real spikes. Daily points hold one observation each, so their band is zero-width by construction. - Series can be shorter than
take. A printing that didn't exist a year ago has no year of history — about a fifth of the catalogue is younger than six months.covered_sinceis the first date present; label the chart from it (“Since Mar”) rather than claiming a full window. - Points are evenly spaced, not one-per-change. Each is the price in force on that date, so the x-axis is uniform and safe to plot directly — unlike
history=1, which is sparse and needs forward-filling. low_myr/high_myraren't sent; multiply by that point'sfx_rate.
Response fields
Product (search results & detail)
| Field | Type | Description |
|---|---|---|
product_id | int | Stable id (TCGplayer productId). Use it for prices. |
group_id | int | Set id. |
set_name | string | Set name (search results only). |
product_type | string | card or sealed. |
game / lang | string | e.g. pokemon / en|ja. |
name | string | Product name. |
image_url / tcgplayer_url | string | Image and TCGplayer page. |
similarity | float | Match score 0–1 (search results only). |
extended | array | Raw attribute list (detail only): rarity, number, etc. |
Price
| Field | Type | Description |
|---|---|---|
sub_type_name | string | Variant. |
market_usd | number | Market price (USD). |
market_myr | number | MYR snapshot at ingest. |
low_usd / mid_usd / high_usd / direct_low_usd | number | Other TCGplayer price points (USD). |
fx_rate | number | USD→MYR rate used. |
captured_at | timestamp | Source publish time. |
Price history (history=1)
| Field | Type | Description |
|---|---|---|
product_id | int | The product this series belongs to. |
sub_type_name | string | Variant. Each printing has its own series. |
d | date | Capture date (UTC). May predate the window for the opening point. |
market_usd | number | Market price that day. null = the card had no market price. |
market_myr | number | Converted at that day's rate, not today's. |
Errors
Errors return a JSON body {"error":"…"} with the matching status code.
| Status | Meaning |
|---|---|
| 400 | Bad request — missing q/ids, invalid lang, unknown game, bad id, etc. |
| 401 | Missing, malformed, expired, or unrecognized bearer token. |
| 403 | Your app lacks the scope this endpoint requires. |
| 404 | Not found — including type mismatches (a card id on /sealed/{id} or vice-versa). |
| 429 | Per-minute rate limit exceeded. See Retry-After and X-RateLimit-Remaining. |
| 503 | Auth registry temporarily unreachable. Retry. |