Skip to content
pokemontcgapi.com

clients

Pokémon TCG API SDKs

Typed clients for TypeScript, Python and Go, and an MCP server for agents. All of them sit on the same endpoints and the same 651 sets.

npm install @pokemontcgapi/sdk
one shape, three languages

The Python and Go clients are ports of the TypeScript one, checked against the same recorded responses: the same resources, the same error classes, the same retry policy and the same pagination. Learn one, and the other two read the same.

pagination that follows itself

Every list method returns a Page that is also iterable. The cursor carries a signature of the sort order, so it must never be rebuilt by hand — the client follows the URL the API returned, which is exactly the mistake this removes.

errors you branch on

Ten classes rather than one type with a string field, so instanceof, except and errors.As work and nobody has to re-type the taxonomy by hand. Rate limiting and quota exhaustion are separate classes because they look identical and are handled oppositely.

cards.ts
import { PokemonTcgApi } from "@pokemontcgapi/sdk";

const client = new PokemonTcgApi({ apiKey: process.env.PTCG_API_KEY });

// Both id forms resolve on the same route.
const card = await client.cards.get("base1-4", { include: ["prices"] });
console.log(card.id, card.name, card.index_eur);
// bs-4 Charizard 523.76   (the index on 16 September 2026; it moves)

// A Page is an AsyncIterable: this walks every Japanese set,
// following links.next for you.
for await (const set of await client.sets.list({ region: "JP" })) {
  console.log(set.code, set.name, set.release_date);
}

endpoint → method

Nothing to translate.

The resources mirror the paths in every language, so moving from the reference to the code needs no lookup table.

SDK method for each endpoint, in TypeScript, Python and Go
endpointTypeScriptPythonGo
GET /v1/cardsclient.cards.search(params)Page<Card>client.cards.search(**params)c.Cards.Search(ctx, params)
GET /v1/cards/{id}client.cards.get(id, params)Cardclient.cards.get(id, **params)c.Cards.Get(ctx, id, params)
GET /v1/cards/batchclient.cards.batch(ids, params)BatchResult<Card>client.cards.batch(ids, **params)c.Cards.Batch(ctx, ids, params)
GET /v1/setsclient.sets.list(params)Page<CardSet>client.sets.list(**params)c.Sets.List(ctx, params)
GET /v1/sets/{code}client.sets.get(code)CardSetclient.sets.get(code)c.Sets.Get(ctx, code, params)
GET /v1/sets/{code}/cardsclient.sets.cards(code, params)Page<Card>client.sets.cards(code, **params)c.Sets.Cards(ctx, code, params)
GET /v1/seriesclient.series.list(params)Page<Series>client.series.list(**params)c.Series.List(ctx, params)
GET /v1/artistsclient.artists.list(params)Page<Artist>client.artists.list(**params)c.Artists.List(ctx, params)
GET /v1/artists/{slug}client.artists.get(slug)Artistclient.artists.get(slug)c.Artists.Get(ctx, slug)
GET /v1/cards/{id}/pricesclient.prices.card(id, params)PricesResponse<CardPrices>client.prices.card(id, **params)c.Prices.Card(ctx, id, params)
GET /v1/prices/currentclient.prices.current(ids, params)BatchResult<CardPrices> — up to 50 idsclient.prices.current(ids, **params)c.Prices.Current(ctx, ids, params)
GET /v1/cards/{id}/prices/historyclient.prices.history(id, params)HistoryResponseclient.prices.history(id, **params)c.Prices.History(ctx, id, params)
GET /v1/cards/{id}/prices/statsclient.prices.stats(id, params)StatsResponseclient.prices.stats(id, **params)c.Prices.Stats(ctx, id, params)
GET /v1/prices/moversclient.prices.movers(params)MoversResponse — from Growthclient.prices.movers(**params)c.Prices.Movers(ctx, params)
GET /v1/prices/sourcesclient.prices.sources()PriceSourceInfo[]client.prices.sources()c.Prices.Sources(ctx)
GET /v1/sealedclient.sealed.list(params)Page<SealedProduct>client.sealed.list(**params)c.Sealed.List(ctx, params)
GET /v1/sealed/{id}client.sealed.get(id, params)SealedProductclient.sealed.get(id, **params)c.Sealed.Get(ctx, id, params)
GET /v1/sealed/{id}/pricesclient.sealed.prices(id, params)PricesResponse<SealedPrices>client.sealed.prices(id, **params)c.Sealed.Prices(ctx, id, params)
GET /v1/changesclient.changes(params)ChangesResponseclient.changes(**params)c.Changes(ctx, params)
GET /v1/referenceclient.reference.types()string[] — 11 valuesclient.reference.types()c.Reference.Types(ctx)
GET /v1/referenceclient.reference.subtypes()string[] — 26 valuesclient.reference.subtypes()c.Reference.Subtypes(ctx)
GET /v1/referenceclient.reference.supertypes()string[] — 3 valuesclient.reference.supertypes()c.Reference.Supertypes(ctx)
GET /v1/referenceclient.reference.rarities()string[] — every printed rarityclient.reference.rarities()c.Reference.Rarities(ctx)
POST /v1/vision/identifyclient.vision.identify(image, options)VisionResponseclient.vision.identify(image, **options)c.Vision.Identify(ctx, image, opts)
POST /v1/centering/measureclient.centering.measure(image, options)CenteringMeasureResponseclient.centering.measure(image, **options)c.Centering.Measure(ctx, image, opts)
POST /v1/centering/confirmclient.centering.confirm(input)CenteringConfirmResponseclient.centering.confirm(payload)c.Centering.Confirm(ctx, input)
POST /v1/centering/gradeclient.centering.grade(input)CenteringGradeResponseclient.centering.grade(payload)c.Centering.Grade(ctx, input)
GET /v1/centering/standardsclient.centering.standards(options)CenteringStandardsResponseclient.centering.standards(**options)c.Centering.Standards(ctx, opts)
GET /v1/statusclient.status()CatalogStatusclient.status()c.Status(ctx)
GET /v1/healthclient.health()Healthclient.health()c.Health(ctx)

the three parts that matter

Paging, failing and not paying twice.

Paging

Import all cards with the flat list. Add q: 'set.region:JP' or q: 'set.region:CN' for one print region, and include: ['translations'] for names at the plain catalogue cost. Use client.sets.cards for a selected set and client.sets.list for set metadata. The quickstart gives dated request counts. Materializing a page sequence requires an explicit ceiling in every language.

for await (const card of await client.cards.search({ limit: 250, orderBy: "id" })) {
  console.log(card.id, card.name, card.set_code);
}

const page = await client.cards.search({ set: "bs", select: ["id", "name"] });
const first50 = await page.toArray({ max: 50 });

Failing

import {
  NotFoundError,
  RateLimitedError,
  QuotaExceededError,
} from "@pokemontcgapi/sdk";

try {
  await client.cards.get("nope-1");
} catch (error) {
  if (error instanceof NotFoundError) {
    // 404 with the id in error.details
  }
  if (error instanceof RateLimitedError) {
    // error.retryAfter — the client already waited and retried
  }
  if (error instanceof QuotaExceededError) {
    // a separate class on purpose: retrying will never help
  }
  // error.requestId is the only thing support can look up
}

Not paying twice

const client = new PokemonTcgApi({ cache: "etag" });

// The second call sends If-None-Match and gets a 304 with no body.
// A 304 consumes no quota, so a mirror pays only for what changed.
await client.sets.list({ region: "JP" });
await client.sets.list({ region: "JP" });
retries with jitter

Exponential backoff with full jitter on 429, 5xx and network failures, honouring Retry-After. Quota exhaustion is never retried. Without jitter, a thousand clients that take the same 429 all retry in the same millisecond.

ETag cache, opt-in

Stores the ETag the API returns and replays 304s. The validator arrives weak, because the edge rewrites it when it compresses, and a 304 costs no quota either way. Kept in memory and per-instance on purpose: a cache on disk inside an SDK is a source of bugs the caller cannot inspect.

types that admit what is empty

The gameplay fields are typed as nullable and documented with their measured fill rate, on the field itself. A type that promises attacks: Attack[] makes people write code that never runs against the half of the catalogue we hold no English text for.

What the types will not promise you

The gameplay fields are uneven, and each one carries its measured fill rate in its own doc comment. Measured on 16 September 2026: attacks on 29.9% of the catalogue and 82.9% of the 20,725 Western printings, subtypes 35.0%, weaknesses 28.0%, flavor text 17.9%, abilities 7.0%, rules 5.1%. Japanese and Chinese printings carry none, and legalities and level are empty for every card. You see it in your editor before you write the code, which is the only useful place to find out.

source

Three repositories, one contract.

Each client is MIT and lives in its own public repository. Issues and pull requests are read there.

@pokemontcgapi/sdk

zero runtime dependencies, runs on Node 20+, Bun, Deno, Workers and browsers.

npm · source

pokemontcgapi

sync and async clients over httpx, TypedDict responses, Python 3.10+.

PyPI · source

github.com/pokemontcgapi/sdk-go

standard library only, context on every call, errors.As over the same taxonomy.

pkg.go.dev · source

A complete example in all three, with tests: a Discord price bot (a /price command with autocomplete, EUR and USD side by side, plus an /ask command over the MCP server).

agents

Or skip the code entirely.

@pokemontcgapi/mcp gives Claude, Cursor, VS Code and any other MCP client 11 read-only tools over the same catalogue — one command to install, and the model queries the data instead of recalling it.

questions

About the clients.

Is there an official Pokémon TCG API TypeScript client?
@pokemontcgapi/sdk is the client we maintain for this API. It has no runtime dependencies and ships its own types. From version 0.1.4 it wraps every data route: cards, sets, series, illustrators, sealed products, prices (current, history, stats, movers and the source list), the changes feed, the reference vocabularies and photo recognition. After each call, client.lastResponse carries what the request cost in credits, the quota left, and anything the plan withheld.
Is there a Python client?
Yes: pip install pokemontcgapi. It is a port of the TypeScript client with a synchronous PokemonTcgApi and an asynchronous AsyncPokemonTcgApi over httpx, its only dependency, on Python 3.10 or newer. Responses are the decoded JSON typed as TypedDict, pages are iterable, and the error classes, retries and ETag cache are the same. The quickstart still shows raw httpx for every endpoint if you prefer no client at all. Quickstart
Is there a Go client?
Yes: go get github.com/pokemontcgapi/sdk-go. Standard library only, Go 1.23 or newer, a context.Context on every call, generic Page[T] with a range iterator, and typed errors that work with errors.As at every level of the taxonomy (a PlanRequiredError is also a PermissionDeniedError).
Does the TypeScript client work outside Node?
Yes. It uses the global fetch and nothing else, so Bun, Deno, Cloudflare Workers and browsers all work. In a browser, use a key scoped to catalogue reads rather than a secret one.
How do I page through an entire set?
Iterate the Page returned by any list method — for await in TypeScript, for in Python, range over page.All(ctx) in Go — and it follows links.next until there is none. Do not reconstruct the cursor: it carries a signature of the sort order and the API rejects a rebuilt one.