# 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.

Source: https://pokemontcgapi.com/docs/design-rules

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](https://pokemontcgapi.com/docs); 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](https://pokemontcgapi.com/docs/caching-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](https://pokemontcgapi.com/docs/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](https://pokemontcgapi.com/docs/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](https://pokemontcgapi.com/docs/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](https://pokemontcgapi.com/docs/api).

## 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](https://pokemontcgapi.com/docs/authentication).

## A real response head

`curl -D -`

```http
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`

```json
{
  "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.
