Query syntax
The complete Lucene-style grammar accepted by the q parameter: fields, phrases, boolean operators, negation, exact match, wildcards and ranges — and which fields hold data today.
The q parameter takes a Lucene-style query grammar: field:value terms, quoted phrases, AND/OR/NOT, - for exclusion, * and ? wildcards, and [a TO b] ranges. If you have written a Lucene, Elasticsearch or Solr query before, you already know most of it.
This page documents the whole grammar as we implement it — every construct, with the exact behaviour and the exact error. Every example is runnable against this API as written, and every example that is not explicitly marked as matching nothing was checked on 2026-08-27 to return rows.
curl -s -G "https://api.pokemontcgapi.com/v1/cards" \
--data-urlencode 'q=name:charizard rarity:rare hp:[300 TO *]' \
-H "X-Api-Key: $PTCG_API_KEY"URL-encode it
Queries contain spaces, colons, brackets and quotes. With curl use -G --data-urlencode; in code use URLSearchParams or your client’s params. Hand-built query strings are where most "why does this return nothing" tickets come from.
Two spellings, and only one of them is a query
This is the single trap on this page, so it comes before the grammar rather than after it.
Query fields are camelCase and dotted. Response keys are snake_case.
They are two different vocabularies and neither one is accepted in the other’s place. q=set.code:bs is a valid query; the card it returns carries "set_code": "bs". q=set_code:bs is 400 INVALID_QUERY — verified live on 2026-08-27. The same applies to nationalPokedexNumbers / national_pokedex_numbers, flavorText / flavor_text, regulationMark / regulation_mark and convertedRetreatCost / converted_retreat_cost.
Field names are matched case-insensitively within the query vocabulary, so set.releasedate and set.releaseDate resolve to the same field. What is not accepted is the response spelling: the underscore, not the capital letter, is what makes set_code unknown. An unknown field is 400 INVALID_QUERY with the token in details.field, its offset in details.position, and the complete accepted list in details.valid_fields — so you never have to hardcode the list below.
The grammar at a glance
Every construct the parser accepts, in one table. Each has its own section below with runnable examples.
| Construct | Written as | Does |
|---|---|---|
| Field term | name:charizard | Substring match on a named field. |
| Free text | charizard | Searches card names, whole words and substrings. |
| Phrase | name:"venusaur v" | Groups words into one term. |
| Implicit AND | a b | Both clauses must hold. |
| AND | a AND b / a && b | Same as adjacency, written out. |
| OR | a OR b / a || b | Either clause. |
| Grouping | (a OR b) c | Overrides precedence. |
| Negation | -a / NOT a | Excludes, null-safe. |
| Exact match | !name:charizard | Promotes "contains" to "is exactly". |
| Wildcard, many | name:char* | Any run of characters. |
| Wildcard, one | name:char?zard | Exactly one character. |
| Inclusive range | hp:[1 TO 100] | Both endpoints included. |
| Exclusive range | hp:{1 TO 100} | Neither endpoint included. |
| Open range | hp:[300 TO *] | One end unbounded. |
| Nested field | set.releaseDate:… | Reaches into a relation or a JSON column. |
Precedence is the standard one: NOT binds tightest, then AND, then OR. Parentheses override it.
Field terms
field:value is the basic unit. Text fields match on substring, case- and accent-insensitively.
| Query | Matches |
|---|---|
name:charizard | Any card whose name contains "charizard" — including "Dark Charizard" and "Charizard ex". |
rarity:rare | Every rarity containing "rare": Rare, Rare Holo, Rare Rainbow. |
artist:arita | "Mitsuhiro Arita", without typing the whole name. |
set.name:"obsidian flames" | Substring search on the set name. |
Free text
A bare term with no field searches card names two ways at once: a full-text index for whole words and a trigram index for substrings. They are complementary — charizard is found by the first, zard only by the second.
| Query | Matches |
|---|---|
charizard | Free-text search across card names. |
zard | Substring — still finds Charizard. |
Phrases
Double quotes group words into one term. Inside quotes, * and ? are literal characters rather than wildcards.
| Query | Matches |
|---|---|
name:"venusaur v" | The two words adjacent, in that order. |
name:venusaur v | Something different — see implicit AND below. |
set.series:"scarlet & violet" | Every set in the series, ampersand and all. |
Escape a quote inside a phrase with a backslash: set.name:"the \"promo\" set". The parser accepts it; whether anything matches is a separate question.
Boolean operators
Implicit AND
Two adjacent clauses are ANDed. a b and a AND b compile to exactly the same query.
| Query | Meaning |
|---|---|
name:charizard rarity:"rare holo" | Both conditions hold. |
name:charizard AND rarity:"rare holo" | Identical to the line above. |
name:charizard && rarity:"rare holo" | Also identical. |
OR and grouping
OR (or ||) widens. Parentheses control precedence, which is the standard Lucene order: NOT binds tightest, then AND, then OR.
| Query | Meaning |
|---|---|
set.code:bs OR set.code:obf | Either set. |
(set.code:bs OR set.code:obf) rarity:rare | Either set and a rare. |
set.code:bs OR set.code:obf rarity:rare | All of Base Set, plus the rares from Obsidian Flames. Without parentheses AND binds first. |
Operators are uppercase and bare
AND, OR and NOT are operators only when written in capitals and unqualified. and is a search term, and so is name:AND — which really does return cards, because "Rare Candy" contains it. Otherwise looking for a card literally named "AND" would be impossible.
Negation
A leading - excludes. NOT does the same thing.
| Query | Meaning |
|---|---|
name:charizard -rarity:common | Charizards that are not commons. |
name:charizard NOT rarity:common | The same. |
-rarity:common | Everything that is not common — including cards with no rarity recorded at all. |
That last row is the subtle one. Negation is null-safe: a card with no rarity still matches -rarity:common. A plain SQL NOT would evaluate to unknown on a null column and drop those rows, which is not what "not common" means to anybody.
A hyphen inside a value is just a hyphen
- is negation only at the start of a token. set.id:cn-cbb6c and name:ho-oh work exactly as written.
Exact match with !
A leading ! promotes a term from "contains" to "is exactly". It is not negation — - is negation — and it is the only way to tell apart values where one contains the other.
| Query | Matches |
|---|---|
!name:charizard | Cards named exactly "Charizard". Excludes "Charizard ex" and "Dark Charizard". |
name:charizard | All of them. |
!rarity:rare | The rarity "Rare" alone, not "Rare Holo" or "Rare Rainbow". |
! cannot be combined with a range: !hp:[1 TO 100] returns 400 INVALID_QUERY with a message saying so.
Wildcards
* stands for any run of characters, ? for exactly one. Matching is per word, not against the whole field.
| Query | Matches |
|---|---|
name:char* | "Charizard", "Charmander", and "Dark Charizard" — word-anchored, so the prefix may start any word. |
name:char*der | "Charmander". Not "Charizard". |
name:char?zard | One character in the gap: "Charizard". |
set.id:sv* | Every set whose id starts with sv. |
Leading wildcards are rejected
A pattern starting with * or ? returns 400 QUERY_UNSUPPORTED. There is no prefix to search on, so every row in the table has to be examined — slow across 52,337 cards, an incident when ten clients repeat it. You almost never need one: free-text search already matches substrings, so zard finds Charizard.
Ranges
Ranges work on numeric and date fields. Square brackets are inclusive, curly braces exclusive, and * is an open end.
| Query | Meaning |
|---|---|
hp:{1 TO 100} | Strictly between 1 and 100 — neither endpoint. |
hp:[* TO 100] | Up to and including 100, no lower bound. |
hp:[300 TO *] | 300 or more. |
hp:[1 TO 100} | Mixed ends are legal: inclusive lower, exclusive upper. |
hp:[* TO *] | The field is present at all — a null check, not a no-op. |
set.releaseDate:[1999-01-01 TO 1999-12-31] | Dates, YYYY-MM-DD or YYYY/MM/DD. |
number:[1 TO 50] | Collector numbers 1–50, sorted on the numeric part so "TG12" behaves. |
A range on a text field is 400 QUERY_UNSUPPORTED; a non-integer or impossible date is 400 INVALID_QUERY. Both are deliberate: passing them through would be a database cast error, which is to say a 500 caused by a client mistake.
Nested fields
Dotted names reach into related records and into structured columns. Nothing about the dot is magic — they are ordinary field names.
Set fields
These are the nested fields that carry data across the whole catalogue.
| Query | Meaning |
|---|---|
set.id:sv3 | Set by canonical code or alternate id. Both resolve, so a query built from stored ids works unchanged. |
set.code:bs | Our canonical code only. |
set.name:"obsidian flames" | Substring match on the set name. |
set.ptcgoCode:OBF | The code used by the game client. |
set.series:"scarlet & violet" | Every set in the series. |
set.releaseDate:[2020-01-01 TO *] | Sets released since 2020. |
set.id:sv3 matches sv3 and not sv3a: identifier fields always compare whole values, never substrings, even without !. Use a wildcard — set.id:sv3* — when you do want the family.
Legalities
The parser validates the value against legal, banned, restricted and rotated. The column itself is empty catalogue-wide, so rather than answering zero rows the whole field is rejected with 400 and an explanation: a query that silently matches nothing reads as "no card is Standard-legal", which is a different and wronger statement than "we do not hold legalities".
Fields that accept a query but match nothing
A grammar that accepts a field it can never satisfy is a trap, so here is the measurement rather than a promise. On 2026-09-03 we counted, over all 52,337 cards rather than a sample, how many rows carry a non-empty value in each column. The game text arrived that day and it is English, so it sits on the 20,725 Western printings and not on the Japanese and Chinese ones: divide by 20,725 instead of by 52,337 to read these as Western coverage, which for attacks is 83% rather than 33%.
| Field | Cards with a value | What that means for q |
|---|---|---|
rarity | 73.5% | Reliable. 67 distinct values — read them from GET /v1/reference. |
hp | 48.6% | Reliable on Pokémon; Trainers and Energy have none by nature. |
types | 42.7% | Western printings. |
nationalPokedexNumbers | 41.5% | Western printings; several entries on a card depicting more than one Pokémon. |
subtypes | 38.4% | 26 distinct values. Match the printed form: subtypes:"Stage 2", not stage2. |
attacks.name / .damage / .text | 32.9% | 83% of the Western printings. damage is a string: 100, 180+, 30×. |
convertedRetreatCost | 32.6% | A range filter works: convertedRetreatCost:[0 TO 1]. |
retreatCost | 30.9% | Slightly under the converted form, because a free retreat is 0 there and [] here. |
weaknesses.type / .value | 30.8% | value carries its own operator: ×2, +20. |
flavorText | 19.6% | Many printings carry none at all. |
evolvesTo | 14.6% | Derived from the evolution graph, not read off the card. |
evolvesFrom | 13.7% | The share that evolves from something, not a gap. |
resistances.type / .value | 9.1% | Most printings have none. |
abilities.name / .text / .type | 7.7% | The real share of printings that have one. |
rules | 5.6% | Trainer text. |
legalities.standard / .expanded / .unlimited | 0% | Rejected with 400 and an explanation, not answered with zero rows. |
level | 0% | Always zero rows: no source we hold carries it. |
Zero rows is not the same as a broken query
q=subtypes:stage2 is syntactically perfect, is accepted, and returns {"data":[],"meta":{"count":0}} — because the printed value is Stage 2 with a space, so q=subtypes:"Stage 2" is the query that finds them. The grammar validates the field name, not the data behind it, and never the spelling of a value. If a query you believe is correct returns nothing, check this table and the vocabulary in GET /v1/reference before you check your encoding. What is populated everywhere: names, ids, numbers, set membership and release dates, images and prices.
Every searchable field
This is the list the API itself returns in details.valid_fields on a 400 INVALID_QUERY, reproduced verbatim as of 2026-08-27. Thirty-three fields, and nothing else is accepted.
| Field | Kind | Populated | Notes |
|---|---|---|---|
abilities.name | json | Empty | Substring inside the array. |
abilities.text | json | Empty | Substring inside the array. |
abilities.type | json | Empty | Substring inside the array. |
artist | text | Yes | Illustrator name. 399 distinct artists. |
attacks.damage | json | Empty | Substring inside the array. |
attacks.name | json | Empty | Substring inside the array. |
attacks.text | json | Empty | Substring inside the array. |
convertedRetreatCost | integer | Empty | Ranges supported by the parser. |
evolvesFrom | text | Empty | Pre-evolution name. |
evolvesTo | array | Empty | Evolution names. |
flavorText | text | Empty | Substring. |
hp | integer | Yes | Ranges supported. Null on Trainers and Energy. |
id | identifier | Yes | Our id or the alternate {set}-{number} id. Whole-value match. |
legalities.expanded | legality | Empty | legal / banned / restricted / rotated. |
legalities.standard | legality | Empty | Same values. |
legalities.unlimited | legality | Empty | Same values. |
level | text | Empty | Printed level. |
name | text | Yes | Also the target of free-text terms. |
nationalPokedexNumbers | integer array | Partial | Ranges supported. Newer sets only. |
number | number | Yes | Exact match on the string, ranges on the numeric part. |
rarity | text | Yes | Free text across eras. 67 distinct rarities. |
regulationMark | identifier | Partial | Single letter, on the eras that print one. |
retreatCost | array | Empty | One entry per symbol. |
rules | array of phrases | Empty | Substring inside each rule sentence. |
set.code | identifier | Yes | Canonical code. |
set.id | identifier | Yes | Canonical code or alternate id. |
set.name | text | Yes | Substring. |
set.ptcgoCode | identifier | Partial | Game client code, where one exists. |
set.releaseDate | date | Yes | Ranges supported. |
set.series | text | Yes | Substring. |
subtypes | array | Empty | Case-insensitive unless prefixed with !. |
supertype | text | Yes | Pokémon, Trainer, Energy — three values. |
types | array | Partial | Energy types. Newer sets only. |
Limits
A query that stops being a search and starts being a load test gets 400 QUERY_TOO_COMPLEX rather than a slow response:
| Limit | Value |
|---|---|
| Leaf clauses | 32 |
| Parenthesis depth | 4 |
Length of q | 2048 characters |
orderBy keys | 4 |
If you are brushing against the clause limit, you are usually looking up a known list of ids — `/v1/cards/batch` does that in one request and one credit per 25 ids.
Worked examples
All five return rows against the live catalogue.
q=set.code:bs -rarity:common— Base Set without the commons.q=rarity:"rare holo" artist:arita— one illustrator’s holos.q=(set.code:bs OR set.code:obf) hp:[100 TO *]— the big Pokémon of two sets, 1999 and 2023.q=!name:pikachu set.releaseDate:[1999-01-01 TO 2000-12-31]— cards named exactly Pikachu from the first two years.q=types:fire set.code:sv3— wheretypesis populated, it composes like any other field.
When a query returns nothing
In order: check the fill-rate table above, then check that you used the camelCase query spelling and not the snake_case response spelling, then check encoding. A query with unencoded spaces reaches us truncated, and a truncated query is usually still valid — it just means something else.