# List sets

> All 615 sets with release date, totals, print region and series, newest first.

- **Endpoint**: `GET /v1/sets`
- **Cost**: 1 credit
- **Minimum plan**: free
- **Cache-Control**: `public, max-age=60, s-maxage=300, stale-while-revalidate=600`

Source: https://pokemontcgapi.com/docs/api/sets/list

A few hundred rows, so this response carries `meta.total_count` — the card endpoints do not, because counting a filtered card query exactly would cost more than the query itself.

The catalogue runs from **Expansion Pack**, 1996-10-20, to **30th Celebration**, 2026-09-16, across three print regions. Asian print lines are separate sets with their own codes and numbering, not translations of Western ones, which is what `region` is for.

## Query parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `q` | string | — | Accent-insensitive substring match on the set name, or an exact `code` or `slug`. |
| `series` | string | — | Series slug, e.g. `base`. Sets whose series is unmapped match no value. |
| `region` | string | — | 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`. |
| `orderBy` | string | -release_date | One or more of `code`, `name`, `release_date`, `total`, `updated_at`. Tiebreaker is `code`. |
| `limit` | integer | 50 | Rows per page, 1 to 250. Above 250 you get `LIMIT_EXCEEDED` — follow `links.next` instead of raising it. |
| `cursor` | string | — | 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. |

## Headers

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `X-Api-Key` **required** | string | — | Your API key. `Authorization: Bearer <key>` is accepted as an alias. |
| `If-None-Match` | string | — | 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. |

## Example request

**curl**

```bash
curl -s -G "https://api.pokemontcgapi.com/v1/sets" \
  --data-urlencode "region=JP" \
  --data-urlencode "limit=2" \
  -H "X-Api-Key: $PTCG_API_KEY"
```

**TypeScript**

`request.ts`

```ts
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();
```

**Python**

`request.py`

```python
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()
```

## Example response

`200 OK` · `Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600`

`response.json`

```json
{
  "data": [
    {
      "id": "m6",
      "code": "m6",
      "slug": "storm-emeralda",
      "legacy_id": null,
      "name": "Storm Emeralda",
      "series": null,
      "region": "JP",
      "release_date": "2026-07-31",
      "total": 116,
      "printed_total": null,
      "ptcgo_code": null,
      "symbol_url": null,
      "logo_url": "https://media.rarebit.app/sets/m6/logo-normal.webp",
      "updated_at": "2026-08-26T17:47:39.900Z"
    },
    {
      "id": "m5",
      "code": "m5",
      "slug": "abyss-eye",
      "legacy_id": null,
      "name": "Abyss Eye",
      "series": null,
      "region": "JP",
      "release_date": "2026-05-22",
      "total": 118,
      "printed_total": null,
      "ptcgo_code": null,
      "symbol_url": null,
      "logo_url": "https://media.rarebit.app/sets/M5/logo-normal.webp",
      "updated_at": "2026-08-26T17:47:39.900Z"
    }
  ],
  "meta": { "limit": 2, "count": 2, "total_count": 379, "has_more": true },
  "links": { "next": "https://api.pokemontcgapi.com/v1/sets?limit=2&region=JP&cursor=eyJrIjpbIjIwMjYtMDUtMjJUMDA6MDA6MDAuMDAwWiJdLCJ..." }
}
```

## Errors

Every error body carries `error.code`, `error.message` and `error.request_id`. Switch on the code, never on the message.

| Status | Code | When |
| --- | --- | --- |
| 401 | `MISSING_API_KEY` | No `X-Api-Key` header and no bearer token on a route that requires one. |
| 401 | `INVALID_API_KEY` | The key does not match any account. |
| 400 | `INVALID_CURSOR` | The cursor is malformed, or was issued for a different `orderBy` than the one on this request. |
| 400 | `INVALID_PARAMETER` | A query parameter has the wrong type or an unsupported value — a non-integer `limit`, an unknown `lang`, an unknown `region`, a sort key that is not sortable. |
| 400 | `LIMIT_EXCEEDED` | `limit` is above 250, or a batch request carries more than 100 ids. |

The full taxonomy, with what to do about each code, is on the [errors](https://pokemontcgapi.com/docs/errors) page.

> **Unknown parameters are ignored**
>
> A query parameter this endpoint does not know is dropped silently — it is not an error and it does not change the result. A typo in a filter name therefore returns a full, unfiltered page rather than a 400, so check the parameter names in the table above before concluding that a filter does nothing.

## Regions, counted

| `region=` | Sets | What it covers |
| --- | --- | --- |
| `WEST` | 176 | The English-language print line and its European siblings |
| `JP` | 379 | Japanese sets, with their own codes and numbering |
| `CN` | 60 | Simplified Chinese print line |
| `KR` | 0 | Valid in the schema, no rows in the catalogue |

Those four numbers come from `meta.total_count` on `GET /v1/sets?region=…&limit=1`, which is the cheapest way to re-check them yourself.
