Search cards
Query 52,337 cards with Lucene syntax, keyset pagination, field projection and price sorting.
/v1/cards1 creditfree planCache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600
The main read path. q accepts a Lucene-style query grammar — field terms, phrases, booleans, wildcards and ranges — documented in full in query syntax.
Results are ordered by id unless you say otherwise, and every sort ends with id as a tiebreaker. That is not a detail: without it, two cards sharing a sort key have undefined relative order between queries, and paging silently duplicates and drops rows.
Every row carries index_eur and last_price_at, so a price-sorted list is one request with no include and no second round trip. The example below sorts the whole catalogue by our composite index.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
q | string | — | Lucene query. Omit it to list everything. Full grammar in query syntax. |
set | string | — | Comma-separated set codes, slugs or alternate ids. A shortcut for q=set.id:(a OR b) that does not need quoting. |
orderBy | string | id | Up to 4 keys, comma-separated, - for descending: id, name, number, rarity, hp, index_eur, last_price_at, updated_at, created_at, release_date, set, artist. A fifth key is QUERY_TOO_COMPLEX; an unsortable name is INVALID_PARAMETER. id is appended as tiebreaker whether you ask for it or not. |
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. |
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 -s -G "https://api.pokemontcgapi.com/v1/cards" \
--data-urlencode "q=name:charizard" \
--data-urlencode "orderBy=-index_eur" \
--data-urlencode "limit=2" \
--data-urlencode "select=id,name,number,rarity,set_code,set_name,release_date,artist_name,index_eur,last_price_at" \
-H "X-Api-Key: $PTCG_API_KEY"Example response
200 OK · Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600
{
"data": [
{
"id": "nde-107",
"name": "Shining Charizard",
"number": "107",
"rarity": "Rare Shining",
"index_eur": 5894.78,
"last_price_at": "2026-08-26T19:03:20.955Z",
"set_code": "nde",
"set_name": "Neo Destiny",
"release_date": "2002-02-28",
"artist_name": "Hironobu Yoshida"
},
{
"id": "e5-089",
"name": "Charizard",
"number": "089",
"rarity": "Secret Rare",
"index_eur": 5371.99,
"last_price_at": "2026-08-26T19:03:20.955Z",
"set_code": "e5",
"set_name": "Mysterious Mountains",
"release_date": "2002-10-04",
"artist_name": null
}
],
"meta": { "limit": 2, "count": 2, "has_more": true },
"links": {
"next": "https://api.pokemontcgapi.com/v1/cards?q=name%3Acharizard&orderBy=-index_eur&limit=2&cursor=eyJrIjpbNTM3MS45OV0sImlkIjoiZTUtMDg5Iiwicy..."
}
}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_QUERY | The q parameter does not parse, or names a field that does not exist. |
| 400 | QUERY_TOO_COMPLEX | A structural limit was exceeded — the query has too many clauses or nests too deep, or orderBy carries more than 4 keys. |
| 400 | QUERY_UNSUPPORTED | The syntax is valid but cannot run: a leading wildcard, or a range on a field that is neither numeric nor a date. |
| 400 | INVALID_CURSOR | The cursor is malformed, or was issued for a different orderBy than the one on this request. |
| 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. |
| 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 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.
Paging a full result set
Follow links.next until it is absent. Do not rebuild the URL yourself and do not change orderBy between pages — the cursor carries the sort order it was issued for, and a mismatch is rejected with INVALID_CURSOR rather than quietly skipping rows.
let next: string | null =
"https://api.pokemontcgapi.com/v1/cards?set=bs&orderBy=number&limit=250";
while (next) {
const res = await fetch(next, { headers: { "X-Api-Key": key } });
const page = await res.json();
for (const card of page.data) upsert(card);
next = page.links?.next ?? null;
}What the catalogue holds
This endpoint searches identity, printing and price: name, number, rarity, HP, set, series, region, artist and the composite index. Rules text — attacks, abilities, subtypes, flavour text — is declared in the schema and carries no rows today, so a query that filters on it matches nothing. The card object states the coverage of every field with the date it was measured.