# Get a card

> Fetch one card by our id or its alternate `{set}-{number}` id, with prices, images and translations on request.

- **Endpoint**: `GET /v1/cards/{id}`
- **Cost**: 2 with include=prices, the same as GET /v1/cards/{id}/prices
- **Minimum plan**: free
- **Cache-Control**: `public, max-age=60, s-maxage=300, stale-while-revalidate=600`

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

No key yet? One call gets you one: `POST /v1/accounts/free` with an `Idempotency-Key` header and your email address returns a working key in the response. Keep the original Idempotency-Key and body for response replay for up to 24 hours, under the conditions in the quickstart. The trial is 800 credits over 30 days once the address is confirmed, no card; paid plans start at 29 € a month. See the [quickstart](https://pokemontcgapi.com/docs/quickstart) and the [pricing](https://pokemontcgapi.com/pricing).

Returns the card object directly, not wrapped in a collection envelope. A missing id is `404 CARD_NOT_FOUND`, which is a statement about the catalogue — cache it, do not poll it.

**This is the price endpoint.** `include=prices` expands the full per-source breakdown for the card, and the card row itself already carries `index_eur` and `last_price_at` whether you ask for the relation or not. The response below is a real one, trimmed at the fifteenth price row.

## Path parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `id` **required** | string | — | Our public id (`bs-4`) or the alternate `legacy_id`, a string of the same `{set}-{number}` form (`base1-4`). Both resolve to the same row, so ids you already have stored need no translation table. An id written with the set slug in place of the set code resolves too — `30th-celebration-001` answers with the card whose id is `30c-001` — and the response always carries the canonical `id`. |

## Query parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `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: `index`, `prices`, `translations`, `images`, `set`, `artist`. Each one is an extra join, so ask only for what you render. `include=index` puts `index_eur` and `last_price_at` on the rows of a list or batch, at 1 credit per 50 cards requested. Without it they are not there: a `select` that names them gets rows without them and `meta.withheld: ["index"]`, and an `orderBy` on them is 400 `INVALID_PARAMETER`. `include=prices` is how you get the per-source price breakdown, index included, and it makes the call a price call: 4 credits per 25 cards on a list or batch, 2 on a single card, and the rows are filtered by your plan exactly as on the price routes, with `meta.withheld` (the `X-Plan-Withheld` header on a single card) naming what was held back. A single card carries its index whatever you include. `legalities` is **not** an accepted value and is rejected with 400 `INVALID_INCLUDE`. |
| `lang` | string | — | Locale for the card name: `en`, `fr`, `de`, `ja`, `it`, `es`, `pt`, `zh` — the eight 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/base1-4" \
  --data-urlencode "include=prices" \
  -H "X-Api-Key: $PTCG_API_KEY"
```

**TypeScript**

`request.ts`

```ts
const url = new URL("https://api.pokemontcgapi.com/v1/cards/base1-4");
url.searchParams.set("include", "prices");

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/base1-4",
    params={"include": "prices"},
    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
{
  "id": "bs-4",
  "legacy_id": "base1-4",
  "name": "Charizard",
  "number": "4",
  "number_sort": 4,
  "supertype": "Pokémon",
  "hp": 120,
  "rarity": "Rare Holo",
  "tcgplayer_id": 42382,
  "cardmarket_id": 273699,
  "index_eur": 561.84,
  "last_price_at": "2026-08-26T19:03:20.955Z",
  "row_version": 6,
  "updated_at": "2026-08-26T17:47:39.900Z",
  "set_code": "bs",
  "set_name": "Base",
  "set_total": 134,
  "ptcgo_code": "BS",
  "release_date": "1999-01-09",
  "print_region": "WEST",
  "series": "Base",
  "artist_name": "Mitsuhiro Arita",
  "artist_slug": "mitsuhiro-arita",
  "prices": [
    {
      "source": "CARDMARKET",
      "variant": "LOW",
      "basis": "ASKING",
      "amount": 599.9,
      "currency": "EUR",
      "locale": "en",
      "condition": "NEAR_MINT",
      "printing": null,
      "grading": null,
      "as_of": "2026-09-01",
      "sample_n": null,
      "provenance": "Cardmarket"
    },
    {
      "source": "CARDTRADER",
      "variant": "LOW",
      "basis": "ASKING",
      "amount": 1250.64,
      "currency": "EUR",
      "locale": "en",
      "condition": "NEAR_MINT",
      "printing": null,
      "grading": null,
      "as_of": "2026-09-01",
      "sample_n": 4,
      "provenance": "CardTrader"
    },
    {
      "source": "TCGPLAYER",
      "variant": "LOW",
      "basis": "GUIDE",
      "amount": 510,
      "currency": "USD",
      "locale": "en",
      "condition": null,
      "printing": "HOLOFOIL",
      "grading": null,
      "as_of": "2026-07-30",
      "sample_n": null,
      "provenance": "TCGplayer"
    },
    {
      "source": "EBAY",
      "variant": "MEDIAN_GRADED",
      "basis": "ASKING",
      "amount": 400,
      "currency": "USD",
      "locale": "en",
      "condition": null,
      "printing": null,
      "grading": {
        "company": "PSA",
        "score": "1"
      },
      "as_of": "2026-08-26",
      "sample_n": 7,
      "provenance": "eBay"
    },
    {
      "source": "PRICECHARTING",
      "variant": "MEDIAN_GRADED",
      "basis": "GUIDE",
      "amount": 20061.4,
      "currency": "USD",
      "locale": "en",
      "condition": null,
      "printing": null,
      "grading": {
        "company": "PSA",
        "score": "10"
      },
      "as_of": "2026-08-24",
      "sample_n": null,
      "provenance": "PriceCharting"
    },
    {
      "source": "PTCG_INDEX",
      "variant": "INDEX",
      "basis": "DERIVED",
      "amount": 556.23,
      "currency": "EUR",
      "locale": "en",
      "condition": null,
      "printing": null,
      "grading": null,
      "as_of": "2026-09-02",
      "sample_n": 16,
      "provenance": "pokemontcgapi composite index"
    }
  ]
}
```

## 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_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. |
| 404 | `CARD_NOT_FOUND` | No card with that id. Both our id (`bs-4`) and the alternate `{set}-{number}` id (`base1-4`) resolve here, and so does an id written with the set slug in place of the set code (`30th-celebration-001` for `30c-001`); each answers with the canonical id. A 404 means none of the three forms matched. |
| 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. |
| 429 | `QUOTA_EXCEEDED` | Trial credits, monthly credits or the daily cap are exhausted, and `details` says which: `daily_cap` with `retry_after_s` is the trial daily cap, which resets at midnight UTC; `renews: true` with `resets_at` is the monthly quota of a paid plan; `renews: false` is the trial allowance, which never comes back. `details.next_step` carries the plan, prices and owner action, or email verification when it unlocks the full trial. |
| 403 | `PLAN_REQUIRED` | The feature is outside the plan (movers require Growth; photo recognition has 5 calls after Free email confirmation, then requires Growth), or the account subscription is canceled or suspended. Centering requires Growth or higher; Developer is excluded. After email confirmation the trial includes 5 measure and 5 confirm calls independently; grade and standards use trial credits without separate call counters. Before email confirmation, the message starts with “Confirm your email…”. Follow details.next_step when present. |
| 403 | `TRIAL_EXPIRED` | The trial has ended: the account is on the trial and its 30 days from signup are over, and this route costs credits. `details.trial_expired_at` is the date, `details.trial_days` the length, and `details.next_step` the owner handoff. Zero-cost routes — `/v1/me`, `/v1/me/usage`, the key routes and `/v1/billing/checkout` — still answer. |
| 403 | `CARD_ALLOWANCE_EXCEEDED` | The account has retrieved as many distinct cards as its plan covers in the period — 1,000 for the whole trial, 30,000 a month on Developer; Growth and above have no limit — and this request would add new ones. `details.cards_used`, `details.card_allowance` and `details.new_cards` give the numbers, `details.resets_at` the next period on paid plans, and `details.next_step` the owner handoff. Only the card routes count; the page that crossed the limit was served in full, and cards already retrieved keep answering. |

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

## Reading the price rows

One row per live series. `source` says who observed it, `basis` says what kind of number it is (`SOLD` a completed sale, `ASKING` a listing, `GUIDE` a published guide value, `DERIVED` ours), and `currency` is the currency the row was observed in — a single card returns USD and EUR rows side by side, and they are not converted for you.

`MEDIAN_GRADED` rows carry `grading` with the company and the score, so a PSA 10 and a CGC 10 stay separate numbers; `grading.company` is null on a grade band that applies to any grader. Every row carries `provenance` and `as_of`; display the provenance wherever you display the price. The sources, their delays and how the composite is built are on [price methodology](https://pokemontcgapi.com/price-methodology).

## The other relations

`include` takes any comma-separated subset of six names. What each one costs you is one join:

| Relation | Shape | Populated today |
| --- | --- | --- |
| `prices` | Array of [price](https://pokemontcgapi.com/docs/objects/price) rows | Yes — a dozen or more rows on a well-covered card |
| `images` | Array of [image](https://pokemontcgapi.com/docs/objects/image) rows | Yes — one `FRONT` / `NORMAL` rendition per card |
| `set` | One [set](https://pokemontcgapi.com/docs/objects/set) object | Yes |
| `artist` | One [artist](https://pokemontcgapi.com/docs/objects/artist) object | Yes, where the card is attributed |
| `translations` | Array of `{ locale, name }` | Yes — `en`, `ja`, `fr`, `de`, `es`, `it` |
| `legalities` | — | Not an accepted value: 400 `INVALID_INCLUDE`. Format legality is not in the catalogue, and an empty array would look like an answer |

`include-images-set-artist.json`

```json
{
  "images": [
    {
      "face": "FRONT",
      "size": "NORMAL",
      "locale": null,
      "url": "https://img.pokemontcgapi.com/media/cards/BASE/4-normal.webp?e=1791763200&s=Qm3vZk1yWq8dP2sL0aXt9c",
      "image_source": "rarebit-media",
      "width": null,
      "height": null
    }
  ],
  "set": {
    "id": "bs",
    "code": "bs",
    "slug": "base",
    "legacy_id": "base1",
    "name": "Base",
    "series": "Base",
    "region": "WEST",
    "release_date": "1999-01-09",
    "total": 134,
    "printed_total": 102,
    "ptcgo_code": "BS",
    "symbol_url": null,
    "logo_url": "https://img.pokemontcgapi.com/media/sets/BASE/logo-normal.webp?e=1791763200&s=7HgT2wKp0bNxYr4uEv1cLz",
    "updated_at": "2026-08-26T17:47:39.900Z"
  },
  "artist": { "slug": "mitsuhiro-arita", "name": "Mitsuhiro Arita", "card_count": 760 }
}
```

`translations` returns an array of `{ locale, name }`, one entry per locale that has a name for this card — never every locale, and never a null-filled row for the ones that do not. A card with no entry for the locale you asked for in `lang` comes back with its English name.

> **Image dimensions are null**
>
> The API returns one rendition per card and does not measure it on ingest, so `width` and `height` come back null. Reserve layout space with a fixed aspect ratio rather than reading those two fields.
