# Batch cards

> Resolve up to 100 card ids in one request instead of 100 round trips.

- **Endpoint**: `GET /v1/cards/batch`
- **Cost**: 1 credit per 25 ids, rounded up
- **Minimum plan**: free
- **Cache-Control**: `public, max-age=60, s-maxage=300, stale-while-revalidate=600`

Source: https://pokemontcgapi.com/docs/api/cards/batch

For hydrating a known list — a deck list, a collection, a set of ids from your own database. One request, one round trip.

The response is **not** an error when some ids are missing: `requested` and `found` let you diff without comparing arrays. Unknown ids are simply absent from `data`. In the example below `base1-4` resolves to `bs-4` and `sv3-223` to `obf-223`, because both were sent as alternate ids — which is also the quickest way to see that resolution happened.

## Query parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `ids` **required** | string | — | Comma-separated ids, at most 100 — a 101st is `LIMIT_EXCEEDED`. Alternate `{set}-{number}` ids are accepted alongside ours, and may be mixed freely. |
| `select` | string | — | 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`. |
| `include` | string | — | 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`. |
| `lang` | string | — | 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. |

## 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/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"
```

**TypeScript**

`request.ts`

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

**Python**

`request.py`

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

## Example response

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

`response.json`

```json
{
  "data": [
    { "id": "bs-4", "name": "Charizard", "index_eur": 561.84, "set_code": "bs" },
    { "id": "obf-223", "name": "Charizard ex", "index_eur": 91.32, "set_code": "obf" }
  ],
  "requested": 3,
  "found": 2
}
```

## 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 | `LIMIT_EXCEEDED` | `limit` is above 250, or a batch request carries more than 100 ids. |
| 400 | `INVALID_SELECT_FIELD` | A name in `select` is not a card field. |
| 400 | `INVALID_INCLUDE` | A name in `include` is not a known relation. |
| 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. |

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

There is no envelope and no cursor here: the caller already knows how many ids it sent, so `meta.has_more` would be a field that is always false.
