# info.description
A complete Pokémon TCG REST API: every card and set in six languages, daily prices with stated provenance, card images, and a free tier. Cursor pagination, ETags, rate-limit headers, OpenAPI.

Authentication is a single `X-Api-Key` header. Collections page with an opaque cursor and every
sort ends with `id` as a tiebreaker, so paging never repeats or drops a row. Errors always carry a
stable `error.code`.

This document lists only the operations the service serves today.

Unofficial. Not produced, endorsed, supported by or affiliated with Nintendo, Creatures Inc., GAME FREAK inc. or The Pokémon Company International. Pokémon and all related marks are trademarks of their respective owners.

# paths./v1/cards.get.description
Query 52,337 cards with Lucene syntax, keyset pagination, field projection and price sorting.

# paths./v1/cards.get.parameters[].description
Lucene query. Omit it to list everything. Full grammar in [query syntax](/docs/query-syntax).

# paths./v1/cards.get.parameters[].description
Comma-separated set codes, slugs or alternate ids. A shortcut for `q=set.id:(a OR b)` that does not need quoting.

# paths./v1/cards.get.parameters[].description
Up to 4 keys, comma-separated, `-` for descending: `id`, `name`, `number`, `rarity`, `hp`, `index_eur`, `last_price_at`, `updated_at`, `created_at`, `release_date`, `set`, `artist`. A fifth key is `QUERY_TOO_COMPLEX`; an unsortable name is `INVALID_PARAMETER`. `id` is appended as tiebreaker whether you ask for it or not.

# paths./v1/cards.get.parameters[].description
Comma-separated card fields to return. `id` is always included. camelCase and snake_case both resolve, so `nationalPokedexNumbers` works and comes back as `national_pokedex_numbers`. An unknown name is `INVALID_SELECT_FIELD`, with the full list in `details.valid_fields`.

# paths./v1/cards.get.parameters[].description
Comma-separated relations to expand: `prices`, `translations`, `images`, `set`, `artist`. Each one is an extra join, so ask only for what you render. `include=prices` is how you get the per-source price breakdown. `legalities` is **not** an accepted value and is rejected with 400 `INVALID_INCLUDE`.

# paths./v1/cards.get.parameters[].description
Locale for the card name: `en`, `ja`, `fr`, `de`, `es`, `it` — the six with translation rows in the catalogue. A card with no translation in the requested locale falls back to English rather than to null.

# paths./v1/cards.get.parameters[].description
Rows per page, 1 to 250. Above 250 you get `LIMIT_EXCEEDED` — follow `links.next` instead of raising it.

# paths./v1/cards.get.parameters[].description
Opaque keyset cursor from `links.next`. Never construct one; it carries the sort order it was issued for and is rejected if `orderBy` changes mid-run.

# paths./v1/cards.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/cards.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/cards.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/cards.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/cards" \
  --data-urlencode "q=name:charizard" \
  --data-urlencode "orderBy=-index_eur" \
  --data-urlencode "limit=2" \
  --data-urlencode "select=id,name,number,rarity,set_code,set_name,release_date,artist_name,index_eur,last_price_at" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/cards.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/cards");
url.searchParams.set("q", "name:charizard");
url.searchParams.set("orderBy", "-index_eur");
url.searchParams.set("limit", "2");
url.searchParams.set("select", "id,name,number,rarity,set_code,set_name,release_date,artist_name,index_eur,last_price_at");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/cards.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/cards",
    params={"q": "name:charizard", "orderBy": "-index_eur", "limit": "2", "select": "id,name,number,rarity,set_code,set_name,release_date,artist_name,index_eur,last_price_at"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/cards/{id}.get.description
Fetch one card by our id or its alternate `{set}-{number}` id, with prices, images and translations on request.

# paths./v1/cards/{id}.get.parameters[].description
Our public id (`bs-4`) or the alternate `legacy_id`, a string of the same `{set}-{number}` form (`base1-4`). Both resolve to the same row, so ids you already have stored need no translation table.

# paths./v1/cards/{id}.get.parameters[].description
Comma-separated card fields to return. `id` is always included. camelCase and snake_case both resolve, so `nationalPokedexNumbers` works and comes back as `national_pokedex_numbers`. An unknown name is `INVALID_SELECT_FIELD`, with the full list in `details.valid_fields`.

# paths./v1/cards/{id}.get.parameters[].description
Comma-separated relations to expand: `prices`, `translations`, `images`, `set`, `artist`. Each one is an extra join, so ask only for what you render. `include=prices` is how you get the per-source price breakdown. `legalities` is **not** an accepted value and is rejected with 400 `INVALID_INCLUDE`.

# paths./v1/cards/{id}.get.parameters[].description
Locale for the card name: `en`, `ja`, `fr`, `de`, `es`, `it` — the six with translation rows in the catalogue. A card with no translation in the requested locale falls back to English rather than to null.

# paths./v1/cards/{id}.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/cards/{id}.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/cards/{id}.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/cards/{id}.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/cards/base1-4" \
  --data-urlencode "include=prices" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/cards/{id}.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/cards/base1-4");
url.searchParams.set("include", "prices");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/cards/{id}.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/cards/base1-4",
    params={"include": "prices"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/cards/batch.get.description
Resolve up to 100 card ids in one request instead of 100 round trips.

# paths./v1/cards/batch.get.parameters[].description
Comma-separated ids, at most 100 — a 101st is `LIMIT_EXCEEDED`. Alternate `{set}-{number}` ids are accepted alongside ours, and may be mixed freely.

# paths./v1/cards/batch.get.parameters[].description
Comma-separated card fields to return. `id` is always included. camelCase and snake_case both resolve, so `nationalPokedexNumbers` works and comes back as `national_pokedex_numbers`. An unknown name is `INVALID_SELECT_FIELD`, with the full list in `details.valid_fields`.

# paths./v1/cards/batch.get.parameters[].description
Comma-separated relations to expand: `prices`, `translations`, `images`, `set`, `artist`. Each one is an extra join, so ask only for what you render. `include=prices` is how you get the per-source price breakdown. `legalities` is **not** an accepted value and is rejected with 400 `INVALID_INCLUDE`.

# paths./v1/cards/batch.get.parameters[].description
Locale for the card name: `en`, `ja`, `fr`, `de`, `es`, `it` — the six with translation rows in the catalogue. A card with no translation in the requested locale falls back to English rather than to null.

# paths./v1/cards/batch.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/cards/batch.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/cards/batch.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/cards/batch.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/cards/batch" \
  --data-urlencode "ids=base1-4,sv3-223,zzz-1" \
  --data-urlencode "select=id,name,set_code,index_eur" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/cards/batch.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/cards/batch");
url.searchParams.set("ids", "base1-4,sv3-223,zzz-1");
url.searchParams.set("select", "id,name,set_code,index_eur");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/cards/batch.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/cards/batch",
    params={"ids": "base1-4,sv3-223,zzz-1", "select": "id,name,set_code,index_eur"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/sets.get.description
All 615 sets with release date, totals, print region and series, newest first.

# paths./v1/sets.get.parameters[].description
Accent-insensitive substring match on the set name, or an exact `code` or `slug`.

# paths./v1/sets.get.parameters[].description
Series slug, e.g. `base`. Sets whose series is unmapped match no value.

# paths./v1/sets.get.parameters[].description
Print region: `WEST` (176 sets), `JP` (379) or `CN` (60), counted 2026-08-27. `KR` is accepted and matches 0 rows. Anything else is `INVALID_PARAMETER` with the accepted values in `details.supported`.

# paths./v1/sets.get.parameters[].description
One or more of `code`, `name`, `release_date`, `total`, `updated_at`. Tiebreaker is `code`.

# paths./v1/sets.get.parameters[].description
Rows per page, 1 to 250. Above 250 you get `LIMIT_EXCEEDED` — follow `links.next` instead of raising it.

# paths./v1/sets.get.parameters[].description
Opaque keyset cursor from `links.next`. Never construct one; it carries the sort order it was issued for and is rejected if `orderBy` changes mid-run.

# paths./v1/sets.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/sets.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/sets.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/sets.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/sets" \
  --data-urlencode "region=JP" \
  --data-urlencode "limit=2" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/sets.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/sets");
url.searchParams.set("region", "JP");
url.searchParams.set("limit", "2");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/sets.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/sets",
    params={"region": "JP", "limit": "2"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/sets/{code}.get.parameters[].description
Set code (`bs`), slug (`base`) or alternate id (`base1`). All three resolve, because whatever you have stored is one of them and you should not have to work out which.

# paths./v1/sets/{code}.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/sets/{code}.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/sets/{code}.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/sets/{code}.get.x-codeSamples[].source
curl -s "https://api.pokemontcgapi.com/v1/sets/bs" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/sets/{code}.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/sets/bs");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/sets/{code}.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/sets/bs",
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/sets/{code}/cards.get.description
Every card in a set, in collection order, with correct paging past 250 rows.

# paths./v1/sets/{code}/cards.get.parameters[].description
Further Lucene filter, applied inside the set.

# paths./v1/sets/{code}/cards.get.parameters[].description
Defaults to collection order. Same keys as [search cards](/docs/api/cards/search). `number` sorts on the extracted numeric part first, so `1S` lands next to `1` and not next to `100`.

# paths./v1/sets/{code}/cards.get.parameters[].description
Comma-separated card fields to return. `id` is always included. camelCase and snake_case both resolve, so `nationalPokedexNumbers` works and comes back as `national_pokedex_numbers`. An unknown name is `INVALID_SELECT_FIELD`, with the full list in `details.valid_fields`.

# paths./v1/sets/{code}/cards.get.parameters[].description
Comma-separated relations to expand: `prices`, `translations`, `images`, `set`, `artist`. Each one is an extra join, so ask only for what you render. `include=prices` is how you get the per-source price breakdown. `legalities` is **not** an accepted value and is rejected with 400 `INVALID_INCLUDE`.

# paths./v1/sets/{code}/cards.get.parameters[].description
Locale for the card name: `en`, `ja`, `fr`, `de`, `es`, `it` — the six with translation rows in the catalogue. A card with no translation in the requested locale falls back to English rather than to null.

# paths./v1/sets/{code}/cards.get.parameters[].description
Rows per page, 1 to 250. Above 250 you get `LIMIT_EXCEEDED` — follow `links.next` instead of raising it.

# paths./v1/sets/{code}/cards.get.parameters[].description
Opaque keyset cursor from `links.next`. Never construct one; it carries the sort order it was issued for and is rejected if `orderBy` changes mid-run.

# paths./v1/sets/{code}/cards.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/sets/{code}/cards.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/sets/{code}/cards.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/sets/{code}/cards.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/sets/bs/cards" \
  --data-urlencode "limit=2" \
  --data-urlencode "select=id,name,number,rarity,index_eur" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/sets/{code}/cards.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/sets/bs/cards");
url.searchParams.set("limit", "2");
url.searchParams.set("select", "id,name,number,rarity,index_eur");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/sets/{code}/cards.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/sets/bs/cards",
    params={"limit": "2", "select": "id,name,number,rarity,index_eur"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/artists.get.description
All 399 credited illustrators with a card count, searchable by name.

# paths./v1/artists.get.parameters[].description
Accent-insensitive substring match on the name, so `arita` finds `Mitsuhiro Arita`.

# paths./v1/artists.get.parameters[].description
`slug`, `name` or `card_count`. Tiebreaker is `slug`.

# paths./v1/artists.get.parameters[].description
Rows per page, 1 to 250. Above 250 you get `LIMIT_EXCEEDED` — follow `links.next` instead of raising it.

# paths./v1/artists.get.parameters[].description
Opaque keyset cursor from `links.next`. Never construct one; it carries the sort order it was issued for and is rejected if `orderBy` changes mid-run.

# paths./v1/artists.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/artists.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/artists.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/artists.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/artists" \
  --data-urlencode "q=arita" \
  --data-urlencode "limit=3" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/artists.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/artists");
url.searchParams.set("q", "arita");
url.searchParams.set("limit", "3");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/artists.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/artists",
    params={"q": "arita", "limit": "3"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/artists/{slug}.get.parameters[].description
Slug as returned by the list endpoint. Not free text — "Mitsuhiro Arita" is not a slug.

# paths./v1/artists/{slug}.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/artists/{slug}.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/artists/{slug}.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/artists/{slug}.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/artists/mitsuhiro-arita");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/artists/{slug}.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/artists/mitsuhiro-arita",
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/series.get.description
The blocks that group sets — Base, EX, Sword & Shield, Scarlet & Violet — with the number of sets in each.

# paths./v1/series.get.parameters[].description
`set_count`, `name` or `slug`, with a leading `-` for descending. Every order ends on `slug` as the tiebreaker.

# paths./v1/series.get.parameters[].description
Rows per page, 1 to 250. Above 250 you get `LIMIT_EXCEEDED` — follow `links.next` instead of raising it.

# paths./v1/series.get.parameters[].description
Opaque keyset cursor from `links.next`. Never construct one; it carries the sort order it was issued for and is rejected if `orderBy` changes mid-run.

# paths./v1/series.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/series.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/series.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/series.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/series" \
  --data-urlencode "limit=3" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/series.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/series");
url.searchParams.set("limit", "3");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/series.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/series",
    params={"limit": "3"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/sealed.get.description
Booster boxes, elite trainer boxes, tins, blisters and collections: 2,088 products with names in six locales, images and prices.

# paths./v1/sealed.get.parameters[].description
Free text on the product name — in the locale given by `lang`, if any — or an exact `sku`. Not the card query grammar: there are no fields or ranges here, because "evolving skies booster box" is the whole question.

# paths./v1/sealed.get.parameters[].description
Set code, slug or alternate id, as on `/v1/sets/{code}`. Products of a set that is not in the catalogue carry `set_code: null` and cannot be reached this way.

# paths./v1/sealed.get.parameters[].description
Product kind, as printed in `kind`: `BOOSTER_BOX`, `ETB`, `TIN`, `BLISTER`, `COLLECTION`, `BOOSTER_PACK`, `BOOSTER_BUNDLE`, `STARTER_DECK`, `BUILD_AND_BATTLE`, `PREMIUM_COLLECTION`, `SLEEVED_BOOSTER` and twelve more. Read the values off the list rather than hardcoding them; an unknown kind returns an empty page, not an error.

# paths./v1/sealed.get.parameters[].description
Locale for `name`: `en`, `ja`, `fr`, `de`, `es`, `it`. A product without a name in that locale falls back to English.

# paths./v1/sealed.get.parameters[].description
`release_date`, `name`, `sku`, `index_eur`, `last_price_at`, `updated_at` or `set`, with a leading `-` for descending. Every order ends on `sku` as the tiebreaker.

# paths./v1/sealed.get.parameters[].description
Rows per page, 1 to 250. Above 250 you get `LIMIT_EXCEEDED` — follow `links.next` instead of raising it.

# paths./v1/sealed.get.parameters[].description
Opaque keyset cursor from `links.next`. Never construct one; it carries the sort order it was issued for and is rejected if `orderBy` changes mid-run.

# paths./v1/sealed.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/sealed.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/sealed.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/sealed.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/sealed" \
  --data-urlencode "q=evolving skies" \
  --data-urlencode "kind=BOOSTER_BOX" \
  --data-urlencode "limit=2" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/sealed.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/sealed");
url.searchParams.set("q", "evolving skies");
url.searchParams.set("kind", "BOOSTER_BOX");
url.searchParams.set("limit", "2");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/sealed.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/sealed",
    params={"q": "evolving skies", "kind": "BOOSTER_BOX", "limit": "2"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/sealed/{id}.get.parameters[].description
The `sku` (which is also the `id`) or the `slug`. Both resolve; the sku wins if a slug happens to equal another product’s sku.

# paths./v1/sealed/{id}.get.parameters[].description
Locale for `name`, falling back to English.

# paths./v1/sealed/{id}.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/sealed/{id}.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/sealed/{id}.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/sealed/{id}.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/sealed/evolving-skies-booster-box");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/sealed/{id}.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/sealed/evolving-skies-booster-box",
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/reference.get.description
Every closed vocabulary the API uses — locales, currencies, print regions, conditions, printings, graders, price variants and bases, change kinds, image faces and sizes — in one response.

# paths./v1/reference.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/reference.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/reference.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/reference.get.x-codeSamples[].source
curl -s "https://api.pokemontcgapi.com/v1/reference" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/reference.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/reference");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/reference.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/reference",
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/status.get.description
Catalogue counts, the age of each ingest job and whether the upstream contract still holds — the numbers this site quotes, straight from the service.

# paths./v1/status.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/status.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/status.get.x-codeSamples[].source
curl -s "https://api.pokemontcgapi.com/v1/status" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/status.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/status");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/status.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/status",
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/cards/{id}/prices.get.description
Every live price series for one card — one row per source, variant, printing and grade — plus the composite index in EUR.

# paths./v1/cards/{id}/prices.get.parameters[].description
Card id (`bs-4`) or alternate id (`base1-4`). Both resolve.

# paths./v1/cards/{id}/prices.get.parameters[].description
Keep only rows from one source, as listed by `GET /v1/prices/sources`. A card can carry over a hundred rows once graded medians are included, so this is usually the difference between a response you parse and one you filter. An unknown value is `INVALID_PARAMETER` with the valid list in `details`, never a silently unfiltered page.

# paths./v1/cards/{id}/prices.get.parameters[].description
Keep only one variant: `LOW`, `MARKET`, `TREND`, `AVG_1D`, `AVG_7D`, `AVG_30D`, `MEDIAN_GRADED`, `INDEX`.

# paths./v1/cards/{id}/prices.get.parameters[].description
Keep only rows for one printing language. Rows that do not state a language are excluded rather than assumed to match.

# paths./v1/cards/{id}/prices.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/cards/{id}/prices.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/cards/{id}/prices.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/cards/{id}/prices.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/cards/base1-4/prices");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/cards/{id}/prices.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/cards/base1-4/prices",
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/sealed/{id}/prices.get.description
The live price series of one sealed product, in the same shape as the card route.

# paths./v1/sealed/{id}/prices.get.parameters[].description
The `sku` or the `slug` of the product, as on `/v1/sealed/{id}`.

# paths./v1/sealed/{id}/prices.get.parameters[].description
Keep only rows from one source, as listed by `GET /v1/prices/sources`. A card can carry over a hundred rows once graded medians are included, so this is usually the difference between a response you parse and one you filter. An unknown value is `INVALID_PARAMETER` with the valid list in `details`, never a silently unfiltered page.

# paths./v1/sealed/{id}/prices.get.parameters[].description
Keep only one variant: `LOW`, `MARKET`, `TREND`, `AVG_1D`, `AVG_7D`, `AVG_30D`, `MEDIAN_GRADED`, `INDEX`.

# paths./v1/sealed/{id}/prices.get.parameters[].description
Keep only rows for one printing language. Rows that do not state a language are excluded rather than assumed to match.

# paths./v1/sealed/{id}/prices.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/sealed/{id}/prices.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/sealed/{id}/prices.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/sealed/{id}/prices.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/sealed/evolving-skies-booster-box/prices");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/sealed/{id}/prices.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/sealed/evolving-skies-booster-box/prices",
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/prices/current.get.description
The current prices of up to 50 cards in one request, for a list, a collection or a deck.

# paths./v1/prices/current.get.parameters[].description
Comma-separated card ids or alternate ids, 1 to 50. More than 50 is `LIMIT_EXCEEDED`; split the list instead.

# paths./v1/prices/current.get.parameters[].description
Keep only rows from one source, as listed by `GET /v1/prices/sources`. A card can carry over a hundred rows once graded medians are included, so this is usually the difference between a response you parse and one you filter. An unknown value is `INVALID_PARAMETER` with the valid list in `details`, never a silently unfiltered page.

# paths./v1/prices/current.get.parameters[].description
Keep only one variant: `LOW`, `MARKET`, `TREND`, `AVG_1D`, `AVG_7D`, `AVG_30D`, `MEDIAN_GRADED`, `INDEX`.

# paths./v1/prices/current.get.parameters[].description
Keep only rows for one printing language. Rows that do not state a language are excluded rather than assumed to match.

# paths./v1/prices/current.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/prices/current.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/prices/current.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/prices/current.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/prices/current" \
  --data-urlencode "ids=bs-4,bs-2" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/prices/current.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/prices/current");
url.searchParams.set("ids", "bs-4,bs-2");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/prices/current.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/prices/current",
    params={"ids": "bs-4,bs-2"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/cards/{id}/prices/history.get.description
A time series for one card: one point per day, week or month, per source and variant.

# paths./v1/cards/{id}/prices/history.get.parameters[].description
One source code, e.g. `CARDMARKET`, `CARDTRADER`, `TCGPLAYER` or `PTCG_INDEX`. A chart that mixes sources means nothing, so filter to one.

# paths./v1/cards/{id}/prices/history.get.parameters[].description
One variant, e.g. `MARKET`, for the same reason.

# paths./v1/cards/{id}/prices/history.get.parameters[].description
Restrict to one printing: `HOLOFOIL`, `REVERSE_HOLO`, …

# paths./v1/cards/{id}/prices/history.get.parameters[].description
`YYYY-MM-DD`, inclusive. Defaults to the start of the plan’s window.

# paths./v1/cards/{id}/prices/history.get.parameters[].description
`YYYY-MM-DD`, inclusive. Defaults to today.

# paths./v1/cards/{id}/prices/history.get.parameters[].description
`day`, `week` or `month`. Coarser buckets keep a long window under the point cap.

# paths./v1/cards/{id}/prices/history.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/cards/{id}/prices/history.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/cards/{id}/prices/history.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/cards/{id}/prices/history.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/cards/base1-4/prices/history" \
  --data-urlencode "source=TCGPLAYER" \
  --data-urlencode "variant=MARKET" \
  --data-urlencode "bucket=week" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/cards/{id}/prices/history.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/cards/base1-4/prices/history");
url.searchParams.set("source", "TCGPLAYER");
url.searchParams.set("variant", "MARKET");
url.searchParams.set("bucket", "week");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/cards/{id}/prices/history.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/cards/base1-4/prices/history",
    params={"source": "TCGPLAYER", "variant": "MARKET", "bucket": "week"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/cards/{id}/prices/stats.get.description
Low, high, median, first, last and the percentage change of the composite index over a window.

# paths./v1/cards/{id}/prices/stats.get.parameters[].description
`7d`, `30d`, `90d` or `365d`. A window longer than the plan’s history is `UPGRADE_REQUIRED`, with `details.plan_window_days`.

# paths./v1/cards/{id}/prices/stats.get.parameters[].description
Restrict the index to one printing language.

# paths./v1/cards/{id}/prices/stats.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/cards/{id}/prices/stats.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/cards/{id}/prices/stats.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/cards/{id}/prices/stats.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/cards/base1-4/prices/stats" \
  --data-urlencode "window=30d" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/cards/{id}/prices/stats.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/cards/base1-4/prices/stats");
url.searchParams.set("window", "30d");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/cards/{id}/prices/stats.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/cards/base1-4/prices/stats",
    params={"window": "30d"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/prices/movers.get.description
The cards whose composite index rose or fell the most over a window.

# paths./v1/prices/movers.get.parameters[].description
Ignore cards whose index at the end of the window is below this many euros. Percentages on a ten-cent card are noise.

# paths./v1/prices/movers.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/prices/movers.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/prices/movers.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/prices/movers.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/prices/movers" \
  --data-urlencode "window=90d" \
  --data-urlencode "min_value=5" \
  --data-urlencode "limit=3" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/prices/movers.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/prices/movers");
url.searchParams.set("window", "90d");
url.searchParams.set("min_value", "5");
url.searchParams.set("limit", "3");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/prices/movers.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/prices/movers",
    params={"window": "90d", "min_value": "5", "limit": "3"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/prices/sources.get.description
The redistribution policy in the open: every source that can appear in a price row, with its label and minimum delay.

# paths./v1/prices/sources.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/prices/sources.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/prices/sources.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/prices/sources.get.x-codeSamples[].source
curl -s "https://api.pokemontcgapi.com/v1/prices/sources" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/prices/sources.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/prices/sources");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/prices/sources.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/prices/sources",
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/changes.get.description
An append-only feed of catalogue mutations, so a mirror can resync without refetching everything.

# paths./v1/changes.get.parameters[].description
The `meta.next_since` of your previous page. `0`, or omitted, starts from the oldest entry still available.

# paths./v1/changes.get.parameters[].description
Comma-separated entity kinds: `CARD`, `SET`, `SEALED`, `PRICE`, `IMAGE`.

# paths./v1/changes.get.parameters[].description
Rows per page, 1 to 250. Above 250 you get `LIMIT_EXCEEDED` — follow `links.next` instead of raising it.

# paths./v1/changes.get.parameters[].description
Send back the `ETag` you stored. The API returns a strong ETag on every catalogue response, and a 304 is a header exchange: no body, no database row read.

# paths./v1/changes.get.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/changes.get.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/changes.get.x-codeSamples[].source
curl -s -G "https://api.pokemontcgapi.com/v1/changes" \
  --data-urlencode "limit=2" \
  -H "X-Api-Key: $PTCG_API_KEY"

# paths./v1/changes.get.x-codeSamples[].source
const url = new URL("https://api.pokemontcgapi.com/v1/changes");
url.searchParams.set("limit", "2");

const res = await fetch(url, {
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();

# paths./v1/changes.get.x-codeSamples[].source
import os, httpx

res = httpx.get(
    "https://api.pokemontcgapi.com/v1/changes",
    params={"limit": "2"},
    headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
)
res.raise_for_status()
payload = res.json()

# paths./v1/vision/identify.post.description
Send a photograph, get back the cards it could be, ranked, with a distance you can threshold yourself.

# paths./v1/vision/identify.post.requestBody.content.multipart/form-data.schema.properties.image.description
The photograph. Three ways to send it: a multipart field named `image`, the raw bytes as the whole body with an `image/*` content type, or base64 (a `data:` URL is accepted) in a JSON field of this name. JPEG, PNG, WebP and GIF; 10 MB and 96 px per side are the bounds.

# paths./v1/vision/identify.post.requestBody.content.multipart/form-data.schema.properties.top_k.description
How many candidates to return, 1 to 10. In query string when the body is the image itself.

# paths./v1/vision/identify.post.requestBody.content.multipart/form-data.schema.properties.set.description
Restrict to one set code. This is the parameter that resolves a reprint: from artwork alone the original and its reprint are the same card, and if you know which pack it came out of, say so.

# paths./v1/vision/identify.post.requestBody.content.multipart/form-data.schema.properties.region.description
Restrict to a print region — `WEST`, `JP` or `CN`. Same purpose: a Japanese card and its Western twin share the illustration.

# paths./v1/vision/identify.post.requestBody.content.application/json.schema.properties.image.description
The photograph. Three ways to send it: a multipart field named `image`, the raw bytes as the whole body with an `image/*` content type, or base64 (a `data:` URL is accepted) in a JSON field of this name. JPEG, PNG, WebP and GIF; 10 MB and 96 px per side are the bounds.

# paths./v1/vision/identify.post.requestBody.content.application/json.schema.properties.top_k.description
How many candidates to return, 1 to 10. In query string when the body is the image itself.

# paths./v1/vision/identify.post.requestBody.content.application/json.schema.properties.set.description
Restrict to one set code. This is the parameter that resolves a reprint: from artwork alone the original and its reprint are the same card, and if you know which pack it came out of, say so.

# paths./v1/vision/identify.post.requestBody.content.application/json.schema.properties.region.description
Restrict to a print region — `WEST`, `JP` or `CN`. Same purpose: a Japanese card and its Western twin share the illustration.

# paths./v1/vision/identify.post.responses.200.headers.ETag.description
Strong validator. Send it back as If-None-Match; a 304 costs no quota.

# paths./v1/vision/identify.post.responses.304.description
Not Modified — the ETag you sent still matches. No body, no quota.

# paths./v1/vision/identify.post.x-codeSamples[].source
curl -s -X POST "https://api.pokemontcgapi.com/v1/vision/identify" \
  -H "X-Api-Key: $PTCG_API_KEY" \
  -F "image=@card.jpg" \
  -F "top_k=3"

# paths./v1/vision/identify.post.x-codeSamples[].source
# What a mobile client does: the body IS the photo.
curl -s -X POST "https://api.pokemontcgapi.com/v1/vision/identify?top_k=1&set=sv3" \
  -H "X-Api-Key: $PTCG_API_KEY" \
  -H "Content-Type: image/jpeg" \
  --data-binary @card.jpg

# paths./v1/vision/identify.post.x-codeSamples[].source
const form = new FormData();
form.set("image", file);
form.set("top_k", "3");

const res = await fetch("https://api.pokemontcgapi.com/v1/vision/identify", {
  method: "POST",
  headers: { "X-Api-Key": process.env.PTCG_API_KEY ?? "" },
  body: form,
});

const { data } = await res.json();

// Never trust the top of the list without reading the decision.
if (data.decision === "match") {
  await addToCollection(data.id);
} else {
  await askTheUser(data.candidates);
}

# paths./v1/vision/identify.post.x-codeSamples[].source
import os, httpx

def identify(path, set_hint=None):
    with open(path, "rb") as fh:
        res = httpx.post(
            "https://api.pokemontcgapi.com/v1/vision/identify",
            files={"image": fh},
            data={"top_k": 3, **({"set": set_hint} if set_hint else {})},
            headers={"X-Api-Key": os.environ["PTCG_API_KEY"]},
        )
    res.raise_for_status()
    return res.json()["data"]

out = identify("binder-page-3.jpg", set_hint="sv3")
print(out["decision"], [c["id"] for c in out["candidates"]])

# components.schemas.Error.description
Every failure has this shape. A client mistake is never answered with a 5xx.

# components.schemas.Error.properties.error.properties.code.description
Stable machine-readable code. Once published it does not change meaning.

# components.schemas.Error.properties.error.properties.message.description
Human-readable, names the offending value.

# components.schemas.Error.properties.error.properties.details.description
What would have been valid, where that can be listed.

# components.schemas.Card.description
A single printed card: identity, printing, artwork, set membership and the ids that link it to external marketplaces.

# components.schemas.Card.properties.id.description
Public id, `{set_code}-{number}`. Stable, unique and guessable — `bs-4` is the Base Set Charizard.

# components.schemas.Card.properties.legacy_id.description
Alternate string id in `{set}-{number}` form (`base1-4`). Accepted anywhere an id is accepted, so ids you already store keep working without a translation table.

# components.schemas.Card.properties.name.description
Printed name. Localised when `lang` is set, with English as fallback.

# components.schemas.Card.properties.number.description
Collector number as printed. A **string**: `TG12`, `SV107`, `1S` and `H1` all exist.

# components.schemas.Card.properties.number_sort.description
Numeric part extracted for sorting. Null on numbers with no leading digits.

# components.schemas.Card.properties.supertype.description
`Pokémon`, `Trainer` or `Energy`. Three values, and `GET /v1/reference` returns the live list under `supertypes`.

# components.schemas.Card.properties.subtypes.description
Printed subtypes, recomposed into one array — stage, suffix, Trainer kind and Energy kind all land here, so `q=subtypes:"Stage 2"` finds every Stage 2 printing. Populated on **38%** of the catalogue (20,082 cards), 26 distinct values.

# components.schemas.Card.properties.types.description
Energy types. Populated on **43%** of the catalogue (22,363 cards). Empty where we hold no game text for the printing, which is mostly the Japanese and Chinese ones.

# components.schemas.Card.properties.hp.description
Hit points. Populated on 100% of the sample. Null for cards with no printed HP.

# components.schemas.Card.properties.level.description
Reserved; still empty after the 2026-09-03 enrichment, because no source we hold carries it.

# components.schemas.Card.properties.evolves_from.description
Name of the printing this one evolves from. **14%** of the catalogue (7,158 cards) — that is the share that evolves from something, not a coverage gap.

# components.schemas.Card.properties.evolves_to.description
Names this one evolves into. **15%** (7,624 cards). Derived from the evolution graph rather than read off the card, so it names whatever evolves from this Pokémon anywhere in the catalogue.

# components.schemas.Card.properties.rarity.description
Printed rarity. Free text across eras and print lines — 67 distinct values on 2026-09-03, so read them from `GET /v1/reference` under `rarities` rather than hardcoding an enum.

# components.schemas.Card.properties.regulation_mark.description
Single letter used by Standard rotation. Present only on the modern print runs that carry one.

# components.schemas.Card.properties.flavor_text.description
The italic prose under the artwork. **20%** of the catalogue (10,284 cards); many printings simply do not carry one.

# components.schemas.Card.properties.abilities.description
Abilities, Poké-Powers, Poké-Bodies and their kin. **8%** of the catalogue (4,042 cards) — the real share of printings that have one. Shape: `name`, `text`, `type`.

# components.schemas.Card.properties.attacks.description
Attacks in printed order. **33%** of the catalogue (17,207 cards), which is 83% of the Western printings. Shape: `name`, `cost`, `converted_cost`, `damage`, `text`. `damage` is a **string**: `100`, `180+` and `30×` all occur.

# components.schemas.Card.properties.weaknesses.description
Weaknesses. **31%** of the catalogue (16,124 cards). Shape: `type`, `value` — and `value` is a string that carries its own operator, `×2` or `+20`.

# components.schemas.Card.properties.resistances.description
Resistances, same shape as weaknesses. **9%** (4,741 cards): most printings have none.

# components.schemas.Card.properties.retreat_cost.description
One entry per energy needed to retreat, as symbols. **31%** of the catalogue (16,161 cards); also empty on the printings that retreat for free.

# components.schemas.Card.properties.converted_retreat_cost.description
The length of `retreat_cost`, precomputed so a range filter is possible. **33%** (17,061 cards) — slightly more than `retreat_cost`, because a free retreat is `0` here and `[]` there.

# components.schemas.Card.properties.rules.description
Rule box text, which for a Trainer is the card effect. **6%** of the catalogue (2,951 cards).

# components.schemas.Card.properties.national_pokedex_numbers.description
National Pokédex numbers. **42%** of the catalogue (21,738 cards). Multiple entries on cards depicting more than one Pokémon.

# components.schemas.Card.properties.tcgplayer_id.description
External marketplace id, for joining against data you already licence. Present for joining, not a licence to resell their data.

# components.schemas.Card.properties.jp_twin_id.description
The Japanese printing of the same card, as an id you can request back. Present where the pairing is known. (This row read `tcgdex_id` until 2026-09-03; the service has never returned that name.)

# components.schemas.Card.properties.index_eur.description
Our composite index in EUR, denormalised onto the card so you can sort and filter on price in the same query that reads the card. The row behind it is the `PTCG_INDEX` entry under `include=prices`.

# components.schemas.Card.properties.last_price_at.description
When a price was last observed for this card. Sortable, so "recently repriced" is one query.

# components.schemas.Card.properties.row_version.description
Incremented by a database trigger on every real update, so an unchanged re-import does not bump it.

# components.schemas.Card.properties.set_code.description
Code of the owning set. Denormalised so the common render needs no `include`.

# components.schemas.Card.properties.ptcgo_code.description
Set code as used by the game client, e.g. `BS`.

# components.schemas.Card.properties.release_date.description
`YYYY-MM-DD`. A date, never a timestamp — the hour was never recorded.

# components.schemas.Card.properties.print_region.description
`WEST`, `JP` or `CN`. `KR` is a valid value in the schema and matches no rows today.

# components.schemas.Card.properties.series.description
Series name, e.g. `Base`. Null on print lines whose series we have not mapped — most Japanese sets today.

# components.schemas.Card.properties.artist_name.description
Illustrator name. Named `artist_name`, not `artist`: `artist` belongs to `include`, and a field that changes type depending on another parameter is what breaks typed clients.

# components.schemas.Set.description
An expansion: codes, totals, release date, print region and the artwork that identifies it.

# components.schemas.Set.properties.id.description
Same value as `code`. Present so code that stores one id field keeps compiling.

# components.schemas.Set.properties.code.description
Canonical code, e.g. `bs`. The natural key across every importer.

# components.schemas.Set.properties.slug.description
URL-safe name, e.g. `base`. Resolves anywhere `code` resolves.

# components.schemas.Set.properties.legacy_id.description
Alternate string id for the set, e.g. `base1` for `bs`. Null on sets that never had one — most Japanese and Chinese print lines.

# components.schemas.Set.properties.series.description
Series name, e.g. `Base`. Null where the series is not mapped.

# components.schemas.Set.properties.region.description
`WEST` (176 sets), `JP` (379) or `CN` (60), counted 2026-08-27. `KR` is valid in the schema and matches no rows. The region decides which upstream is authoritative and which prices are comparable.

# components.schemas.Set.properties.total.description
Cards in the set including secret rares.

# components.schemas.Set.properties.printed_total.description
The number printed on the card, e.g. 102 of a 134-card set.

# components.schemas.Set.properties.ptcgo_code.description
Code used by the game client, e.g. `BS`.

# components.schemas.Set.properties.symbol_url.description
Expansion symbol. **Null on all 615 sets**: our upstream carries the logo and not the symbol, so nothing is written here. Use `logo_url`, which is present on 613 of them. (This row read "frequently null" until 2026-09-03, which was a kinder word than the data deserved.)

# components.schemas.Sealed.description
A booster box, elite trainer box, tin, blister or collection: the unit a shop sells and a collector keeps shrink-wrapped.

# components.schemas.Sealed.properties.id.description
Equal to `sku`. The value that opens `/v1/sealed/{id}` and `/v1/sealed/{id}/prices`.

# components.schemas.Sealed.properties.slug.description
URL slug, present on most products and usually equal to the sku. Also resolves on the single-product routes.

# components.schemas.Sealed.properties.name.description
English by default; `?lang=` swaps it for the requested locale with an English fallback.

# components.schemas.Sealed.properties.kind.description
Product kind as catalogued: `BOOSTER_BOX`, `ETB`, `TIN`, `BLISTER`, `COLLECTION`, `BOOSTER_PACK`, `BOOSTER_BUNDLE`, `STARTER_DECK`, `BUILD_AND_BATTLE`, `PREMIUM_COLLECTION`, `SLEEVED_BOOSTER` and twelve more. Filterable with `?kind=`.

# components.schemas.Sealed.properties.set_code.description
The set the product belongs to, when it is in the catalogue; opens `/v1/sets/{code}`. Null for products that span sets or whose set is not carried.

# components.schemas.Sealed.properties.pack_count.description
Booster packs inside, for the products that contain them.

# components.schemas.Sealed.properties.languages.description
Languages the product physically exists in — the printing, not the name. Empty when not catalogued.

# components.schemas.Sealed.properties.index_eur.description
Catalogue price in euros, where one is known. Sortable with `orderBy=-index_eur`.

# components.schemas.Sealed.properties.last_price_at.description
When a market price was last observed for this product.

# components.schemas.Artist.description
An illustrator, with a maintained count of the cards they drew.

# components.schemas.Artist.properties.slug.description
Stable identifier used by `/v1/artists/{slug}`. Not free text — "Mitsuhiro Arita" is not a slug.

# components.schemas.Artist.properties.card_count.description
Cards currently attributed to this artist.

# components.schemas.Price.description
One price observation: what it is, where it came from, and when it was seen.

# components.schemas.Price.properties.source.description
`TCGPLAYER`, `CARDMARKET`, `CARDTRADER`, `EBAY`, `PRICECHARTING`, `COMMUNITY`, `PTCG_INDEX` — the seven on the allow-list today, listed live by `GET /v1/prices/sources`. The source names where the price formed and who delivered it; `provenance` on the same row is the printable form. What each covers and how delayed it is: [price methodology](/price-methodology).

# components.schemas.Price.properties.basis.description
`SOLD` (completed sale), `ASKING` (listing), `GUIDE` (published guide value), `DERIVED` (computed by us). Read it before comparing two rows: a Cardmarket or eBay figure is what someone is asking, a PriceCharting figure is built on sales history. Averaging the two mixes offers with transactions.

# components.schemas.Price.properties.amount.description
Decimal, in the currency named by the next field. Not converted — the row is served in the currency it was observed in.

# components.schemas.Price.properties.currency.description
`EUR`, `USD`, `GBP`, `JPY`. `PTCG_INDEX` rows are EUR; the marketplace sources are mostly USD, so a single card returns rows in more than one currency.

# components.schemas.Price.properties.locale.description
Language of the printing the price refers to, when the source distinguishes them.

# components.schemas.Price.properties.condition.description
`MINT`, `NEAR_MINT`, `EXCELLENT`, `GOOD`, `LIGHT_PLAYED`, `PLAYED`, `POOR`. Null when the source does not grade condition, which is most of them.

# components.schemas.Price.properties.printing.description
`NORMAL`, `HOLOFOIL`, `REVERSE_HOLO`, `FIRST_EDITION`, `FIRST_EDITION_HOLOFOIL`. Null when the source quotes one number for all printings.

# components.schemas.Price.properties.grading.description
`{ company, score }` for graded slabs — `PSA`, `BGS`, `CGC`, `SGC`, `ACE`, `TAG`. Set on the `MEDIAN_GRADED` rows that name a grader; null on the ungraded ones.

# components.schemas.Price.properties.as_of.description
`YYYY-MM-DD` of the observation, not of the request.

# components.schemas.Price.properties.sample_n.description
Observations behind the number. Null when the source does not disclose it — which is itself worth surfacing to your users.

# components.schemas.Price.properties.provenance.description
Human-readable attribution string. Display it wherever you display the price.

# components.schemas.Image.description
One rendition of a card face, with the rights holder named.

# components.schemas.Image.properties.face.description
`FRONT`, `BACK`, `ART`, `LOGO`, `SYMBOL`. Only `FRONT` is served on cards today.

# components.schemas.Image.properties.size.description
`SMALL`, `NORMAL`, `LARGE`. Only `NORMAL` is served today — there is one rendition per card, not three.

# components.schemas.Image.properties.locale.description
Language of the printing shown. Null for language-neutral art and for rows where the printing language was not recorded.

# components.schemas.Image.properties.url.description
Fetchable URL. Cacheable: the rendition is part of the path, so the bytes behind a given URL do not change.

# components.schemas.Image.properties.image_source.description
Who owns or hosts the original. Credit it.

# components.schemas.Image.properties.width.description
Pixels. Reserved; still empty after the 2026-09-03 enrichment, because no source we hold carries it.

# components.schemas.Image.properties.height.description
Pixels. Reserved; still empty after the 2026-09-03 enrichment, because no source we hold carries it.