/v2/cards
BetaPreview of the next cards API: graded cards and multi-region localized pricing.
card_idvariant_idtcgplayer_idtcgplayer_sku_idgtinPrices are quoted per region instead of converted between currencies — see Localized pricing for how the markets array works.
regionsNANAEUUKJPOCELATAMOTHER…ISO country codesGraded cards (PSA, BGS, CGC, and more) are one of the biggest additions in v2 — see Graded cards for how they fit into the response shape. The params below are how you request them.
Response
// BETA: the justtcg-js SDK gains /v2 support after v2 ships. Until then, call
// the endpoint directly. Note: 'regions' defaults to NA and 'markets' replaces
// the flat price fields.
const params = new URLSearchParams({
game: "pokemon",
regions: "eu,na", // optional (defaults to NA); first region is the primary market
graded: "include", // raw + graded variants (premium tier)
});
const res = await fetch(`https://api.justtcg.com/v2/cards?${params}`, {
headers: { "x-api-key": process.env.JUSTTCG_API_KEY! },
});
const { data } = await res.json();
// price now lives per-region under markets[]
const eu = data[0].variants[0].markets.find((m) => m.region === "EU");
console.log(eu?.price, eu?.currency);// GET /v2/cards?game=pokemon®ions=eu,na&graded=include&include=periods.7d,price_history.7d
// 200 OK
// RateLimit: limit=500000, remaining=499958, reset=...
// Link: <https://api.justtcg.com/v2/cards?...&cursor=eyJvIjoyMH0>; rel="next"
{
"data": [
{
"id": "9b2e4d1a-1111-5v5v-aaaa-000000000000", // card UUID (native)
"slug": "pokemon-base-set-charizard-holo-rare", // legacy v1 id
"name": "Charizard",
"game": { "id": "pokemon", "name": "Pokemon" },
"set": { "id": "base-set-pokemon", "name": "Base Set" },
"number": "4",
"rarity": "Rare Holo",
"external_ids": { "tcgplayer": "42445", "scryfall": null, "mtgjson": null },
"variants": [
{
"id": "d41f-raw-uuid",
"slug": "pokemon-base-set-charizard-holo-rare_near-mint_holofoil",
"type": "raw",
"condition": "Near Mint",
"printing": "Holofoil",
"language": null,
"grading": null,
"markets": [
{
"region": "EU", // markets[0] = primary market (drives sort/filter)
"currency": "EUR",
"price": 142.30, // observed EU price; null if none observed
"price_status": "current",
"last_price": null,
"updated_at": 1765635970,
"change_24h_pct": -1.8,
"periods": {
"7d": { "change_pct": -3.1, "avg": 148.0, "min": 140.0, "max": 151.2, "changes_count": 10, "sales_count": 12 }
},
"price_history": [
{ "t": 1765550000, "p": 150.0 },
{ "t": 1765636400, "p": 142.3 }
]
},
{
"region": "NA", // requested explicitly as the USD fallback reference
"currency": "USD",
"price": 155.00,
"price_status": "current",
"last_price": null,
"updated_at": 1765635970,
"change_24h_pct": -0.9,
"periods": {
"7d": { "change_pct": -2.4, "avg": 158.0, "min": 151.0, "max": 162.0, "changes_count": 11, "sales_count": 14 }
},
"price_history": [
{ "t": 1765550000, "p": 160.0 },
{ "t": 1765636400, "p": 155.0 }
]
}
]
},
{
"id": "d41f-psa10-uuid",
"slug": "d41f-psa10-uuid",
"type": "graded",
"condition": null,
"printing": "Holofoil",
"language": null,
"grading": {
"company": "PSA", "grade": 10, "grade_label": null,
"qualifier": null, "canonical": "PSA 10"
},
"markets": [
{ "region": "EU", "currency": "EUR", "price": null, "price_status": "unpriced", "last_price": null, "updated_at": 1765630000, "change_24h_pct": null, "periods": {}, "price_history": [] },
{ "region": "NA", "currency": "USD", "price": 4570.00, "price_status": "current", "last_price": null, "updated_at": 1765630000, "change_24h_pct": 0.4, "periods": { "7d": { "change_pct": 1.2, "avg": 4480.0, "changes_count": 2, "sales_count": 3 } }, "price_history": [] }
]
}
]
}
]
}Two time signalsBeta
price_historyis the{ t, p }series of observed market prices over time. It is exactly what v1 exposed, so your existing charts keep working, and it is now scoped to each market entry.periodsholds aggregate statistics (average, min and max, standard deviation, trend, and more), now grouped by time window. The fields line up one-to-one with v1's flat stats and carry the same values, so anything you computed in v1 continues to match.
Request costBeta
Most requests cost the same as v1. Two optional features add to that cost, and they are easy to spot below. If you use neither, you pay the base rate.
graded=include and extra regions, costs the same as the equivalent v1 call. The schedule below is what takes effect at general availability, and it will be announced in the changelog before it switches on.One region, raw OR graded variants only
The same cost as the equivalent v1 call. This is what you pay by default.
Two features add a surcharge
graded=includeReturning raw and graded variants together adds a surcharge. Asking for just one kind (graded=exclude or graded=only) stays at normal cost.
regions=na,eu,…Each region beyond the first is a separate price lookup, so it adds cost. This applies as more regions come online.
Surcharges stack: use both features and both apply.
Migrating from v1Beta
v1 does not change. v2 is a new, additional path you can opt into. The defaults already reproduce v1 pricing:regions falls back to NA and graded defaults to exclude, so a plain request returns the same raw NA pricing as v1. The identifier rename is covered in full on the Identifiers page.
What changes for an existing integrationThe call-outs that matter when you adopt /v2/cards
idis now the UUID. The old slug moves to a field calledslug. This is the single biggest rename.regionsdefaults toNA. That matches v1's US pricing, andNAis the only region live today.price&price_historynow live insidemarkets[]. Withregions=NA,markets[0]is the drop-in replacement.- There is no more
offsetpagination. Use thecursorfrom theLinkheader instead. - There is no standalone
us_pricefield. RequestNAand read that market entry. - The flat stat fields move into
periods. They are regrouped by time window undermarkets[].periods, with the same values as v1. printingnow holds the print type only. The" - <Language>"suffix has been removed, so readlanguageseparately.- Absent is not the same as null. v2 leaves out fields you did not request instead of sending them as
null. The one exception is a market'sprice. - Errors now use
application/problem+json. Update any parser that keys on{ error, code }.
Frequently asked questionsBeta
The questions we are asked most often about the v2 beta: why it exists, when it leaves beta, and why graded and regional coverage is still filling in.
Why is /v2/cards in beta, and who is it for right now?
v2 opened in beta earlier than a typical release because its first users are the stores in the JustTCG Partner Program. Graded prices in v2 are built from recorded sales, and a store can only report a graded sale once its own systems can address graded cards the way v2 does: as first-class variants with stable identifiers. Opening the beta early gives those stores a finished contract to integrate against while the data behind it is still filling in. Developers are welcome to build on v2 today, with the understanding that coverage is still growing and the beta label reflects the data more than the API.
When will v2 leave beta?
There is no fixed date. The request and response shape is essentially final, so what gates general availability is data coverage and quality, not the API. Coverage grows as more partner stores integrate and report sales, and we would rather reach GA with a dataset that holds up than with a date on a calendar. The status change will be announced in the changelog. Existing v1 integrations are unaffected either way.
See: Changelog
Why does a graded card have no price, and why is graded coverage incomplete?
A graded variant exists for every card, grading company and grade, but its price stays null until a sale of that exact slab has been recorded. Graded prices are built from real sales, not from estimates or multipliers on the raw price, so coverage is deepest for slabs that trade often and thinnest for rare ones. An unpriced variant is not an error: in browse and search responses it carries price: null and price_status: "unpriced", while a direct lookup of a variant that has never been priced returns a 404 problem response. Coverage fills in as partner stores report sales, and your integration needs no change when it does.
See: Graded cards · Variant object (v2)
Why do some graded prices carry an old updated_at?
updated_at is when that slab's price was last observed. Graded prices do not expire the way high-value raw prices do, because a slab that trades a few times a year is normal rather than a sign of a stale market. Treat updated_at as part of the price: if your use case needs freshness, weight or discount older observations in your own logic.
How can I get coverage for the cards I care about sooner?
Coverage follows reported sales. If you run a store, or build for one, joining the JustTCG Partner Program is the most direct way to improve it: sales reported by a partner feed graded and regional prices for the region they were sold in, and partners get API access in return. If you are a developer without a store, the best move is to build against the v2 shape now. Nothing in your integration changes when a slab or a region gets its first price.
Will the response shape change before general availability?
The shape is finalized and we treat it as stable. The beta label keeps room for a correction if real-world data exposes a problem, and any change that could break a client is announced in the changelog before it takes effect, with what to update. v1 does not change at all, so you can adopt v2 one call at a time and fall back if you need to.
See: Changelog · Migrating from v1
When do EU, UK, JP and other regions come online?
Each region switches on once it has enough locally observed sales to stand on its own. v2 does not convert US prices into other currencies, so a region is not enabled until its local data meets our coverage threshold. Today NA is the only region with data. The regions parameter and the markets array are already shaped for the rest, so a region arriving changes nothing in your integration. Partner stores outside North America directly accelerate their own region.
See: Localized pricing
Can I fetch many graded cards in one request?
Does v2 show how many times a card has sold?
Yes. Every markets[] entry carries sales_count inside periods: the number of sales recorded for that variant in that market over each window (7d, 30d, 90d, 1y and all_time), for raw and graded variants alike. It counts sales, not units, so a sale of several copies counts once, and refunded or voided sales are left out. A window appears only when it holds at least one sale, so read a missing window as zero. Next to it, changes_count is the number of times the sale price changed in the window. Counts are aggregates: individual sale records, such as each sale's price and date, are not part of the API. periods are on by default, and include=periods.30d narrows the response to one window.
See: periods fields · Changelog
Can I look up sealed product by its barcode?
Yes. Pass the code printed under the barcode as gtin, for example gtin=820650850714. It accepts a 12-digit UPC-A, a 13-digit EAN-13 or JAN, an 8-digit EAN-8 and a 14-digit GTIN-14, and ignores spaces and hyphens. If your app already sends whatever the user scans or types to q, a 12 to 14 digit barcode there is matched the same way and falls back to a normal name search when nothing carries it. Barcodes cover sealed product such as booster boxes, Elite Trainer Boxes and bundles, and coverage is growing. One barcode can belong to more than one listing, so a lookup can return several cards.
See: gtin parameter · Changelog
Do graded or multi-region requests cost more during the beta?
No. While /v2/cards is in beta, the surcharges for graded=include and for extra regions are not applied: every v2 request costs the same as the equivalent v1 call. The Request cost section on this page describes the schedule that takes effect at general availability, so you can plan for it now. Surcharges will not switch on silently; the change is announced in the changelog ahead of time.
See: Request cost