Skip to content
pokemontcgapi.com

Design rules

The parts of the contract you only notice when they are missing: free 304s, paging that stays correct at depth, stable error codes, ids you can build by hand.

Everything on this page was verified against the running service, on the date stated next to each sample. The three rules that show up in every response body — every sort ends with id, a client mistake is never a 5xx, quota exhaustion is 429 — are stated in the introduction; these are the ones you meet once you start caching, paging and handling errors.

A 304 costs nothing

Every collection carries an ETag. Send it back as If-None-Match and an unchanged result answers 304 with no body: no database work on our side, no parsing on yours, and no credit charged. Re-syncing often is the behaviour we want to reward, not bill for. The mechanics are in caching and ETags.

Pagination never repeats or drops a row

Paging is keyset-based and every sort ends with the id as a tiebreaker, at any depth. Follow links.next until it is gone; the cursor carries a signature of the sort order, so changing orderBy mid-walk is rejected rather than quietly skipping rows. See pagination.

Stable error codes

Machine-readable SCREAMING_SNAKE codes that do not change once published, a message that names the offending value, a details object that lists what would have been valid, and a request_id you can quote at us. A client mistake is never answered with a 5xx. The full taxonomy is in errors.

Ask for the fields you use

select trims the response on the wire and include opts into relations — prices, images, translations, set, artist — so a list view moves a fraction of the bytes a detail view does, and a wrong field name is a 400 that tells you the right ones. The grammar is in query syntax.

Ids you can build by hand

Set code, dash, collector number. You can construct an id from the card in front of you, and the alternate legacy_id resolves on the same route, so migrating a catalogue you already have does not start with a matching problem. Every route is in the API reference.

Callable from a browser

CORS is enabled, so front-end code can call the API directly. Keep secret keys out of bundles: a public key type with catalogue-only scope and a referrer allow-list is what belongs in client code. See authentication.

A real response head

curl -D -
HTTP/2 200
content-type: application/json; charset=utf-8
etag: W/"JR4-4C3IFIC3FTB9bDiwsTpaJsg-gzip"
cache-control: public, max-age=60, s-maxage=300, stale-while-revalidate=600
ratelimit-limit: 5
ratelimit-remaining: 9
ratelimit-reset: 1
x-credits-cost: 1
x-quota-limit: 80
x-quota-remaining: 79
x-quota-reset: 2026-10-01T00:00:00Z
x-request-id: 923508b19a52fb733c27778f87366bc6

An etag on every collection and an x-request-id on every response, which is the one thing support can act on. Every metered response also carries RateLimit-Limit, RateLimit-Remaining, X-Quota-Remaining and X-Credits-Cost, so a client can back off before a 429 rather than after. Verified against the running service on 10 September 2026.

A real error

400 Bad Request
{
  "error": {
    "code": "INVALID_SELECT_FIELD",
    "message": "Unknown field \"hitpoints\" in \"select\". See details.valid_fields for the full list.",
    "details": {
      "field": "hitpoints",
      "valid_fields": ["id", "legacy_id", "name", "number", "hp", "…"]
    },
    "request_id": "abe7c040-a7c1-4e58-88ca-92e8ff24a1de"
  }
}

One round trip to fix

The details object lists what would have been valid, so a mistyped field is one round trip to fix instead of a trip to the documentation. Copied from /v1/cards?select=hitpoints on 27 August 2026.

view this page as markdown