The q parameter is a compact query language for finding cards, expansions and sealed
product. Match on any field, combine conditions with AND/OR, exclude with a minus, and use
wildcards and numeric ranges. It’s accepted by Search cards, Search expansions, List cards in an expansion and Search sealed products. The grammar is the same
everywhere; what changes is which fields each resource registers, so the tables below are per
resource.
A query is a set of field:value terms. A bare word with no field is matched
against name. Matching is case-insensitive, and text fields match on substrings, so name:char finds “Charizard” and “Charmander”.
charizard → name contains "charizard" name:charizard → same, explicit supertype:Trainer → all Trainer cards rarity:"Special Illustration Rare" → quote values that contain spaces
Wrap a value in double quotes when it contains spaces. Use ! before a field for
an exact (whole-value) match instead of a substring:
!name:pikachu → name is exactly "Pikachu" (not "Pikachu V")
Multiple terms are ANDed together. Use OR (uppercase) and
parentheses to group alternatives, and a leading - to exclude.
name:charizard subtypes:vmax → charizard AND a VMAX (subtypes:mega OR subtypes:vmax) → either subtype name:char* -types:water → char… but NOT Water type supertype:Pokémon -rarity:Common → Pokémon that aren't Common
* matches any run of characters. Put it anywhere except the start of a value.
name:char* → starts with "char" name:char*der → starts "char", ends "der" (Charmander)
*zard) are not allowed. Plain text search already matches substrings, so use name:zard.* per value, and at least 2 literal characters.Range syntax works on number and date fields. Square brackets are inclusive, curly braces are
exclusive, and * is an open (unbounded) end.
hp:[150 TO *] → hp ≥ 150
hp:{150 TO 200} → 150 < hp < 200 (exclusive)
raw_price:[100 TO 500] → $100–$500 inclusive
expansion.release_date:[2024-01-01 TO *] → released on/after 2024-01-01
card_count.total:[200 TO *] → big sets (expansions search) A plain value on a number field is an equality test: hp:150 means exactly 150.
Some fields are lists or nested objects. You query them with a dotted name; the term matches if any element matches.
subtypes:mega → one of the card's subtypes is "mega" attacks.name:"G-Max Wildfire" → has an attack with that name abilities.text:draw → an ability whose text mentions "draw" expansion.id:sv1 → only cards in set sv1 weaknesses.type:Water → weak to Water
Accepted on every game:
| Field | Type | Notes |
|---|---|---|
name | text | Default field, so a bare term matches on name |
id | exact id | Full card id — one printing, e.g. sv3pt5-6. Case-insensitive |
code | exact id | Printing-family code — the art and finish variants of one card in one language. code:op16-085 matches op16-085 and op16-085vaa, never op16_ja-085, which is its own code. Not a substitute for the printings endpoint — see below |
subtypes | array | Pokémon classification (VMAX, Mega); One Piece traits (Straw Hat Crew) |
number | exact id | Printed number; ranges match the numeric part |
rarity | text | e.g. Rare Holo, Special Illustration Rare |
artist | text | Illustrator name (alias: illustrator) |
raw_price | number | Ungraded market price (USD); supports ranges |
is_variant / is_holo / is_reverse / is_first_edition | boolean | true or false |
abilities.name / abilities.text | text | Searches within the card’s abilities |
attacks.name / attacks.text | text | Searches within the card’s attacks |
attacks.damage | number | Numeric attack damage; supports ranges |
weaknesses.type / resistances.type | text | e.g. Water, Fire |
expansion.id | exact id | Restrict to one set, e.g. sv3pt5 |
expansion.name | text | Set name |
expansion.series_id | exact id | Series id, e.g. sv |
expansion.release_date | date | Set release date; supports ranges |
language | exact id | e.g. en, ja, zh. Also a top-level param — see Languages below |
| Field | Type | Notes |
|---|---|---|
supertype | text | Pokémon, Trainer or Energy |
types | array | Energy types, e.g. Fire, Water |
hp | number | Hit points; supports ranges |
| Field | Type | Notes |
|---|---|---|
type | text | Leader, Character, Event, Stage or DON!! |
cost | number | Play cost; supports ranges |
power | number | Battle power; supports ranges |
colors | array | Red, Green, Blue, Purple, Black, Yellow |
attribute | array | Slash, Strike, Ranged, Special, Wisdom. Dual attributes match either half |
rules | array | Effect text, one term per printed line, e.g. rules:Blocker |
variant | text | Printing within a code: normal, altArt, specialAltArt, … |
counter | number | Printed Counter; supports ranges. Partial coverage — see the One Piece card endpoints |
life | number | Leader Life; supports ranges. Leaders only, partial coverage |
block | array | Printed Block icon: 1–5, or X for no numbered block. 77% coverage |
code: and /cards/{id}/printings answer different
questions. They share the same id-stripping SQL, but not the same scope: a code
carries its language, so the Japanese printing of OP01-016 has the code op01_ja-016 and can never match code:OP01-016 — not even with language=all.
code:OP01-016 &language=en → 5 printings code:OP01-016 &language=all → 5 printings /cards/OP01-016/printings → 10 printings
Use code: to gather one language's variants inside a larger query. Use the
printings endpoint when you want every printing of a card across languages.
Each of these filters under the name it comes back as, so a deck query reads the way the card
does: colors:Black cost:[1 TO 3] type:Character. The three Pokémon fields above
also resolve on One Piece — the sync overloads those columns, so supertype, types and hp are older spellings of type, colors and power. Both work; the One Piece name is the one to write. type is One Piece only: on /pokemon it returns a 400 pointing at types, because there the two are different things. Fields not listed anywhere here
(e.g. national_pokedex_numbers) are not searchable and return a 400 rather than an
empty result.
| Field | Type | Notes |
|---|---|---|
name | text | Default field |
id | exact id | Expansion id, e.g. sv3pt5 |
series_id | exact id | Series id, e.g. sv |
language | exact id | e.g. en, ja, zh |
release_date | date | Supports ranges |
card_count.total | number | Total cards incl. secret rares; supports ranges |
card_count.official | number | Printed count; supports ranges |
Sealed product registers its own, much shorter field set. Card fields
(rarity, hp, colors, …) are not in scope on /sealed and return a 400 with a suggestion, and there is no distinct: a product is a single row, not a family of printings.
| Field | Type | Notes |
|---|---|---|
name | text | Default field, so a bare term matches on name |
id | exact id | Product id. Case-insensitive, like card ids |
product_type | exact id | Product family — see the closed list below. An unrecognised value is a 400, not an empty page |
expansion.id | exact id | Restrict to one set, e.g. sv3pt5 |
expansion.name | text | Set name |
expansion.release_date | date | The set’s street date — a product has none of its own; supports ranges |
language | exact id | Inherited from the expansion. Defaults to en, as card lists do |
price | number | Current market value (USD); supports ranges |
is_on_sale | boolean | true or false |
product_type is a closed vocabulary — booster_pack, sleeved_pack, booster_box, booster_pack_case, bundle, pack_art_bundle, blister_pack, three_pack_blister_pack, elite_trainer_box, ultra_premium_collection, collection_box, special_box, tin, other — and ?product_type= is shorthand for the q term. Anything outside the list is
rejected with a 400 rather than silently matching nothing.
There is no starter_deck. Starter decks are stored as other and nothing
in the data separates them from the rest of that bucket, so a query for one returns an error, not
an empty page — and no amount of filtering will conjure the value.
GET /api/v1/pokemon/sealed?q=product_type:booster_box expansion.id:sv3pt5 GET /api/v1/onepiece/sealed?q=price:[100 TO *]&orderBy=-price GET /api/v1/azuki/sealed?include=prices
orderByA comma-separated list of fields; prefix a field with - for descending. Up to 3
keys. Results always get a stable tiebreak on id.
orderBy=-release_date → newest sets first orderBy=-raw_price,name → most expensive first, then A→Z
Sortable card fields: name, number, rarity, raw_price (alias market_price), release_date (alias expansion.release_date), expansion.id, id — plus hp on Pokémon and cost, power, counter, life on One Piece. Sortable expansion fields: name, release_date, id. Sortable sealed fields: name, price, release_date, id — note it is price there, not raw_price, since a product has no condition to qualify.
orderBy=cost → cheapest to play first (deck-builder pool) orderBy=-power,name → biggest hitters first, then A→Z orderBy=-market_price → most expensive first
Cards with no value for the sort key come last in both directions. Postgres would otherwise put them first on a descending sort, so “most expensive first” would open with every card we hold no price for.
That tiebreak makes every ordering a total one, the default included: id is
unique, so no two rows tie and no page boundary can shift between requests. Walking every page
of a result set is safe — it can’t skip or duplicate rows.
Every card exists once per printed language — OP15-079 and OP15_ja-079 are two objects, not one card with a translation. List endpoints
therefore default to language=en; without it a search returns two
catalogs interleaved, which is rarely what a client wants and always twice the payload.
(no language param) → English only language=ja → Japanese only language=en,ja → both language=all → every language we index
language in the list envelope, so you can always see which filter ran.language:, id: or expansion.id: term in q already pins the language, so the default steps aside rather than fighting it — q=id:OP15_ja-079 finds that card without any extra param./expansions/{id}/cards has no default at all: the expansion id already picks a language (EB01 vs EB01_ja)./cards/{id}) is never language-filtered.Every printing is its own row — base art, alternate art, each parallel — so a search returns
printings, and total_count counts printings. For a card-shaped list, pass distinct=code: one row per card, and total_count counts cards.
GET /onepiece/expansions/EB01/cards → 107 printings GET /onepiece/expansions/EB01/cards?distinct=code → 61 cards
rarity, raw_price, is_holo, variant) can match only a parallel and still return nothing. Every gameplay field — type, cost, power, colors, rules, block — is shared by all printings of a code, so deck-building queries are unaffected.distinct belongs to the two endpoints that return cards — /cards and /expansions/{id}/cards. An expansion and a sealed product are each a single row, with no family to collapse, so neither /expansions nor /sealed reads it.page (default 1) and page_size (default 100, max 100; pageSize is accepted too). page × page_size can’t exceed 10,000, so narrow with q to go deeper.page_size is a 400, never a silent clamp — if you asked for 250 and got 100 rows back, check the spelling of the param you sent.total_count so you can compute the last page.select=id,name,rarity returns only those top-level fields (id is always included).include=prices adds raw + graded pricing under each card’s pricing object — on /sealed it adds the single factory-sealed market value instead. Either way it costs nothing extra.A rejected query returns 400, and the machine-readable error code says
which kind of wrong it was: parse_error for syntax the grammar can’t read (an
unclosed quote or parenthesis, a missing term), unknown_field for a field this
resource doesn’t register, invalid_value for a value that doesn’t fit the field it
was given to, and query_too_complex for anything over the caps below. When the
problem is at a specific spot, details.position is the 0-based character offset,
and an unknown field comes back with a suggestion.
GET /api/v1/pokemon/cards?q=subtype:mega
400 {
"success": false,
"message": "unknown field \"subtype\". Did you mean \"subtypes\"?",
"error": "unknown_field",
"details": { "position": 0 }
} orderBy's 3-key limit — return 400 with error: "query_too_complex", and so do the wildcard
guardrails (a leading *, more than three per value, fewer than two literal
characters). A hand-typed query rarely reaches them; a generated filter reaches 20 terms
easily, so count as you build and split into several narrower calls rather than one wide one.# Charizard VMAX/Mega cards with at least 300 HP, priciest first, with prices GET /api/v1/pokemon/cards?q=name:charizard (subtypes:vmax OR subtypes:mega) hp:[300 TO *]&orderBy=-raw_price&include=prices # Non-Common Water Pokémon in Scarlet & Violet era sets GET /api/v1/pokemon/cards?q=types:Water -rarity:Common expansion.series_id:sv # One Piece Leaders that are red, sorted by name GET /api/v1/onepiece/cards?q=type:Leader colors:Red&orderBy=name # A One Piece deck-building pool: cheap black characters with a Blocker, # one row per card, cheapest first GET /api/v1/onepiece/cards?q=type:Character colors:Black cost:[1 TO 3] rules:Blocker&distinct=code&orderBy=cost # Standard-legal pool for the current rotation (blocks 2-5), minus the DON!! rows GET /api/v1/onepiece/cards?q=(block:2 OR block:3 OR block:4 OR block:5) -type:"DON!!"&distinct=code # Japanese printings only, and the base art of one code rather than its parallels GET /api/v1/onepiece/cards?q=expansion.id:OP15_ja&language=ja GET /api/v1/onepiece/cards?q=expansion.id:EB01 variant:normal # English sets from 2024 onward with 200+ cards, newest first GET /api/v1/pokemon/expansions?q=language:en release_date:[2024-01-01 TO *] card_count.total:[200 TO *]&orderBy=-release_date
Every endpoint page has a live “Try it” runner with a q box. Edit any example above
and run it against staging.