Skip to content
Prepaid, no subscription trap. Top up a balance and we draw the monthly fee from it.
tcggraph

GraphQL reference

One endpoint over the same catalog and prices as REST. Worth it when one round trip should cover several games, or when you want only a few fields of each card.

Endpoint

https://api.tcggraph.com/graphql
  • POST a JSON body { "query": "…", "variables": { … } }, or GET with ?query=.
  • The same key as REST, the same way: Authorization: Bearer or ?api_key=. See Authentication.
  • Introspection is on, so GraphiQL, Postman and codegen tools read the schema from the endpoint itself.

What a request costs

The requestHTTPCredits
Any document that runs, however many root fields or aliases it has2002
A field that resolves to nothing, such as a card id that does not exist2002
A document that fails to parse or validate, or is refused on its arguments4000

So one request that fetches three games costs 2 credits where three REST searches cost 6. Limits and headers are the same as REST: Credits and rate limits.

Examples

Run against the live API; the responses are what it answered.

One card with its prices

curl -X POST "https://api.tcggraph.com/graphql" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"{\n  card(id: \"pkm_sv3pt5_6\") {\n    id\n    name\n    set { code name }\n    prices { source listType finish currency market low }\n  }\n}"}'
200 · X-Credits-Cost: 2
{
  "data": {
    "card": {
      "id": "pkm_sv3pt5_6",
      "name": "Charizard ex",
      "set": {
        "code": "sv3pt5",
        "name": "151"
      },
      "prices": [
        {
          "source": "CARDMARKET",
          "listType": "RETAIL",
          "finish": "foil",
          "currency": "EUR",
          "market": 8.41,
          "low": 4.5
        },
        {
          "source": "TCGPLAYER",
          "listType": "RETAIL",
          "finish": "foil",
          "currency": "USD",
          "market": 8.76,
          "low": 5.99
        }
      ]
    }
  }
}

The most expensive card in three games, one request

Aliases put each root field under its own key. Three root fields are still one request, 2 credits.

curl -X POST "https://api.tcggraph.com/graphql" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"{\n  pokemon: cards(filter: { game: POKEMON, source: CARDMARKET }, sort: PRICE_DESC, limit: 1) {\n    nodes { id name prices { source finish market } }\n  }\n  magic: cards(filter: { game: MAGIC_THE_GATHERING, source: CARDMARKET }, sort: PRICE_DESC, limit: 1) {\n    nodes { id name prices { source finish market } }\n  }\n  lorcana: cards(filter: { game: DISNEY_LORCANA, source: CARDMARKET }, sort: PRICE_DESC, limit: 1) {\n    nodes { id name prices { source finish market } }\n  }\n}"}'
200 · X-Credits-Cost: 2
{
  "data": {
    "lorcana": {
      "nodes": [
        {
          "id": "lor_p1_3",
          "name": "Elsa - Snow Queen",
          "prices": [
            {
              "source": "CARDMARKET",
              "finish": "foil",
              "market": 1706.76
            },
            {
              "source": "CARDMARKET",
              "finish": "normal",
              "market": 5928.28
            }
          ]
        }
      ]
    },
    "pokemon": {
      "nodes": [
        {
          "id": "pkm_ecard3_149",
          "name": "Ho-oh",
          "prices": [
            {
              "source": "CARDMARKET",
              "finish": "normal",
              "market": 8082.3
            },
            {
              "source": "CARDMARKET",
              "finish": "reverse-holo",
              "market": 2338.05
            },
            {
              "source": "TCGPLAYER",
              "finish": "reverse-holo",
              "market": 900
            }
          ]
        }
      ]
    },
    "magic": {
      "nodes": [
        {
          "id": "mtg_6ce8744a-ede",
          "name": "Traveling Chocobo",
          "prices": [
            {
              "source": "CARDMARKET",
              "finish": "foil",
              "market": 50000
            }
          ]
        }
      ]
    }
  }
}

Game fields and bounds, through variables

gameData matches a field exactly; gameDataMin and gameDataMax are inclusive bounds on numeric fields.

curl -X POST "https://api.tcggraph.com/graphql" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"query FireHeavyweights($filter: CardFilter) {\n  cards(filter: $filter, sort: NAME, limit: 3) {\n    nodes { id name gameData }\n    pageInfo { page limit totalCount totalPages hasMore }\n  }\n}","variables":{"filter":{"game":"POKEMON","gameData":{"types":"Fire"},"gameDataMin":{"hp":300}}}}'
200 · X-Credits-Cost: 2
{
  "data": {
    "cards": {
      "nodes": [
        {
          "id": "pkm_pps_21_198",
          "name": "Blaziken VMAX",
          "gameData": {
            "hp": 320,
            "types": [
              "Fire"
            ],
            "attacks": [
              {
                "cost": [
                  "Fire"
                ],
                "name": "Clutch",
                "text": "During your opponent's next turn, the Defending Pokemon can't retreat.",
                "damage": "60",
                "convertedEnergyCost": 1
              },
              {
                "cost": [],
                "name": "2] Max Blaze",
                "text": "Choose up to 2 of your Benched Rapid Strike Pokemon and attach an Energy card from your discard pile to each of them.",
                "damage": "130",
                "convertedEnergyCost": 0
              }
            ],
            "subtypes": [
              "VMAX"
            ],
            "abilities": [],
            "supertype": "Pokémon",
            "weaknesses": [
              {
                "type": "Water",
                "value": "×2"
              }
            ],
            "evolvesFrom": null,
            "resistances": [],
            "retreatCost": [
              "Colorless",
              "Colorless"
            ],
            "catalogSource": "tcgplayer",
            "regulationMark": null,
            "convertedRetreatCost": 2,
            "nationalPokedexNumbers": []
          }
        },
        {
          "id": "pkm_swsh12tg_TG15",
          "name": "Blaziken VMAX",
          "gameData": {
            "hp": 320,
            "types": [
              "Fire"
            ],
            "attacks": [
              {
                "cost": [
                  "Fire"
                ],
                "name": "Clutch",
                "text": "During your opponent's next turn, the Defending Pokémon can't retreat.",
                "damage": "60",
                "convertedEnergyCost": 1
              },
              {
                "cost": [
                  "Colorless",
                  "Colorless"
                ],
                "name": "Max Blaze",
                "text": "Choose up to 2 of your Benched Rapid Strike Pokémon and attach an Energy card from your discard pile to each of them.",
                "damage": "130",
                "convertedEnergyCost": 2
              }
            ],
            "subtypes": [
              "VMAX",
              "Rapid Strike"
            ],
            "abilities": [],
            "supertype": "Pokémon",
            "weaknesses": [
              {
                "type": "Water",
                "value": "×2"
              }
            ],
            "evolvesFrom": "Blaziken V",
            "resistances": [],
            "retreatCost": [
              "Colorless",
              "Colorless"
            ],
            "regulationMark": "E",
            "convertedRetreatCost": 2,
            "nationalPokedexNumbers": [
              257
            ]
          }
        },
        {
          "id": "pkm_swsh6_200",
          "name": "Blaziken VMAX",
          "gameData": {
            "hp": 320,
            "types": [
              "Fire"
            ],
            "attacks": [
              {
                "cost": [
                  "Fire"
                ],
                "name": "Clutch",
                "text": "During your opponent's next turn, the Defending Pokémon can't retreat.",
                "damage": "60",
                "convertedEnergyCost": 1
              },
              {
                "cost": [
                  "Colorless",
                  "Colorless"
                ],
                "name": "Max Blaze",
                "text": "Choose up to 2 of your Benched Rapid Strike Pokémon and attach an Energy card from your discard pile to each of them.",
                "damage": "130",
                "convertedEnergyCost": 2
              }
            ],
            "subtypes": [
              "VMAX",
              "Rapid Strike"
            ],
            "abilities": [],
            "supertype": "Pokémon",
            "weaknesses": [
              {
                "type": "Water",
                "value": "×2"
              }
            ],
            "evolvesFrom": "Blaziken V",
            "resistances": [],
            "retreatCost": [
              "Colorless",
              "Colorless"
            ],
            "regulationMark": "E",
            "convertedRetreatCost": 2,
            "nationalPokedexNumbers": [
              257
            ]
          }
        }
      ],
      "pageInfo": {
        "page": 1,
        "limit": 3,
        "totalCount": 71,
        "totalPages": 24,
        "hasMore": true
      }
    }
  }
}

A card that does not exist

card is null, not an error. The document ran, so it is billed.

curl -X POST "https://api.tcggraph.com/graphql" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ card(id: \"pkm_nope_1\") { id name } }"}'
200 · X-Credits-Cost: 2
{
  "data": {
    "card": null
  }
}

Pagination

cards takes page (from 1) and limit (1 to 100, default 20). Values outside that range are clamped, and pageInfo.page and pageInfo.limit say what was used. Loop while pageInfo.hasMore is true; totalCount is the size of the whole result.

Filters

CardFilter has the same filters as GET /v1/cards, with enums where REST takes strings: game: POKEMON, source: CARDMARKET, grader: PSA, grade: G10. Game-specific fields go in gameData, and numeric bounds in gameDataMin and gameDataMax. Each game’s fields are listed by game(slug) { schema { name type } } and on the REST overview. A field that game does not declare is refused rather than matching nothing. Dotted paths such as banlist.tcg are not GraphQL names, so pass them through a variable.

filter.source picks which marketplace minPrice, maxPrice and PRICE_DESC read from, in that marketplace’s currency. Cards still return every quote they have, and pageInfo.priceSource echoes the choice.

Errors

Two envelopes, depending on how far the request got. A request refused before GraphQL runs (no key, a bad key, a limit reached, a body not sent as application/json) gets the REST envelope and status: { "error": { "code", "message", "details" } }, as listed on Errors. Everything after that answers in the GraphQL envelope:

WhenHTTPerrors[0].extensions.code
The body is not valid JSON, or has no query400BAD_REQUEST
An unknown field, a wrong type, an enum value that does not exist400GRAPHQL_VALIDATION_FAILED
An argument the schema allows but the data cannot serve: an undeclared gameData field, a non-numeric bound, grader without grade, a graded slot the index does not quote, sales(days:) out of range. extensions.field names it and extensions.allowed lists the valid values where there is a list.400invalid_request

A missing variable is refused the same way, but GraphQL reports it with no extensions.code; check errors first, then the code.

A field the schema does not have

Refused before it runs: HTTP 400, data absent, not billed.

curl -X POST "https://api.tcggraph.com/graphql" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ card(id: \"pkm_sv3pt5_6\") { id price } }"}'
400 · X-Credits-Cost: 0
{
  "errors": [
    {
      "message": "Cannot query field \"price\" on type \"Card\". Did you mean \"prices\"?",
      "locations": [
        {
          "line": 1,
          "column": 33
        }
      ],
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED"
      }
    }
  ]
}

A game field that does not exist

Refused, not answered with an empty page: HTTP 400, data: null, not billed.

curl -X POST "https://api.tcggraph.com/graphql" \
  -H "Authorization: Bearer $TCGGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ cards(filter: { game: POKEMON, gameData: { hitpoints: 330 } }) { nodes { id } } }"}'
400 · X-Credits-Cost: 0
{
  "errors": [
    {
      "message": "gameData has no field \"hitpoints\" for this game. Game.schema lists them.",
      "locations": [
        {
          "line": 1,
          "column": 3
        }
      ],
      "path": [
        "cards"
      ],
      "extensions": {
        "code": "invalid_request",
        "field": "gameData.hitpoints",
        "allowed": [
          "supertype",
          "subtypes",
          "hp",
          "types",
          "evolvesFrom",
          "attacks",
          "abilities",
          "weaknesses",
          "resistances",
          "retreatCost",
          "convertedRetreatCost",
          "regulationMark",
          "nationalPokedexNumbers"
        ]
      }
    }
  ],
  "data": null
}

The Game enum

The uppercase form of each game slug, with dashes as underscores.

POKEMON                 # pokemon
MAGIC_THE_GATHERING     # magic-the-gathering
ONE_PIECE               # one-piece
YUGIOH                  # yugioh
DISNEY_LORCANA          # disney-lorcana
STAR_WARS_UNLIMITED     # star-wars-unlimited
DIGIMON                 # digimon
GRAND_ARCHIVE           # grand-archive
DRAGON_BALL_SUPER       # dragon-ball-super

Schema

The complete schema the endpoint serves, printed from the running code. Descriptions are the field documentation.

"""A trading card game supported by TCGGraph."""
enum Game {
  POKEMON
  MAGIC_THE_GATHERING
  ONE_PIECE
  YUGIOH
  DISNEY_LORCANA
  STAR_WARS_UNLIMITED
  DIGIMON
  GRAND_ARCHIVE
  DRAGON_BALL_SUPER
}

enum CardSort {
  NAME
  NAME_DESC
  PRICE
  PRICE_DESC

  """
  By one slot of the graded index: the filter's grader and grade, PSA 10 when unset.
  """
  GRADED_PRICE
  GRADED_PRICE_DESC
  RELEASED
  RELEASED_DESC
}

type Set {
  code: String!
  name: String!

  """
  The set's own language: EN for an English set whatever language the
  printing is, JA for an own-language set (Japanese Pokémon), whose codes
  can repeat an English set's -- the Japanese sv3 is not the English sv3.
  """
  language: Language!
  releasedAt: String
}

type Images {
  small: String
  normal: String
  large: String
}

"""
Perceptual hashes of the card's artwork, for matching a photograph or a scan
against the catalogue. Null until the artwork has been processed.

Compare two hashes by Hamming distance. Read them together: 'phash' describes
structure and survives the colour cast and exposure a camera introduces, but
is blind to printings that differ only in colour. 'phashColor' sees exactly
that difference, so it breaks the ties 'phash' cannot.
"""
type Hashes {
  """64-bit DCT hash of the artwork's luma, as 16 hex characters."""
  phash: String!

  """192-bit colour hash, white-balance corrected, as 48 hex characters."""
  phashColor: String
}

"""A marketplace we quote prices from."""
enum PriceSource {
  """
  Cardmarket, the European market. Quotes in EUR. Every game but Grand Archive, which Cardmarket does not list.
  """
  CARDMARKET

  """
  TCGplayer, the North American market. Quotes in USD. Every game; Magic by way of MTGJSON, the rest read from TCGplayer's daily publication.
  """
  TCGPLAYER

  """
  Card Kingdom, a North American dealer. Quotes in USD, retail and buylist. Magic only.
  """
  CARDKINGDOM

  """Manapool, a North American marketplace. Quotes in USD. Magic only."""
  MANAPOOL
}

"""
Which side of the market a price is.

A retail price is what a buyer pays for the card. A buylist price is what the
dealer pays to buy it from you, and is always the lower of the two. They are
different markets and are never mixed within a series.
"""
enum ListType {
  RETAIL
  BUYLIST
}

enum PriceRegion {
  NA
  EU
}

"""
One quote, from one marketplace, for one finish. A card carries a row per
finish, so a foil and a nonfoil are priced as the separate products they are.
"""
type Price {
  source: PriceSource!
  region: PriceRegion!

  """Retail unless asked otherwise. Only Card Kingdom publishes a buylist."""
  listType: ListType!
  finish: String!

  """ISO-4217 code. EUR on Cardmarket."""
  currency: String!

  """
  The headline quote: the trend price where the marketplace publishes one, else the plain average.
  """
  market: Float

  """
  Cheapest live article, in any condition and any language. For one language, see 'languagePrices'.
  """
  low: Float

  """
  The marketplace's own smoothed price, and what European sellers anchor to.
  """
  trend: Float

  """Mean of the last day's sales."""
  avg1: Float

  """Mean of the last seven days."""
  avg7: Float

  """Mean of the last thirty days."""
  avg30: Float

  """
  Cardmarket only: where these figures were read. 'product-page' is the summary on the product's page as our crawl read it at 'updatedAt'; 'price-guide' is Cardmarket's daily price guide file. Null on the other marketplaces.
  """
  basis: String

  """When this quote was last read from the marketplace. ISO 8601."""
  updatedAt: String!
}

"""
Why a card does or does not carry a Cardmarket quote. An empty 'prices' on
its own cannot tell "Cardmarket does not sell this" from "not priced yet";
this can.
"""
enum CardmarketStatus {
  """
  A Cardmarket product is identified and quoted. The quote is on prices[].
  """
  PRICED

  """
  The product is identified but Cardmarket publishes no usable quote for it yet, which is typical of a new release before its first sales.
  """
  UNQUOTED

  """
  Cardmarket lists several products under this name in the expansion this printing belongs to, and nothing it publishes says which one is this printing. We do not guess.
  """
  AMBIGUOUS

  """
  Every candidate product under this name is identified as another printing of the same card, so none is left for this one. Those printings carry their quotes; this variant does not.
  """
  COLLAPSED

  """
  The expansion this printing belongs to is on Cardmarket but no product there carries this card's name and number.
  """
  UNLISTED

  """
  Cardmarket has no expansion for this set, or none we could identify with confidence, so nothing in it can be priced.
  """
  SET_UNLISTED

  """Cardmarket does not carry this game."""
  GAME_UNLISTED
}

"""
Who graded the copy a quote is for. RAW is an ungraded copy. ANY is a grade
quoted across grading companies rather than for one of them: the market does
not separate a PSA 8 from a BGS 8 closely enough to publish them apart. The
named companies are told apart only at grade 10.
"""
enum Grader {
  RAW
  ANY
  PSA
  BGS
  CGC
  SGC
}

"""
The grade on the slab. UNGRADED goes with RAW; the rest with the graders above.
"""
enum Grade {
  UNGRADED
  G7
  G8
  G9
  G9_5
  G10
}

"""
What a graded copy of this printing sells for, from tcggraph's graded index.
USD, refreshed daily from recorded sales of slabs.
"""
type GradedPrice {
  grader: Grader!
  grade: Grade!

  """ISO-4217 code. USD."""
  currency: String!
  price: Float!

  """Units of this printing sold in the last year, all grades together."""
  salesVolume: Int
  updatedAt: String!
}

"""
Why a card does or does not carry graded quotes. Same words as CardmarketStatus.
"""
enum GradedStatus {
  """
  This printing is identified in the graded index and quoted. The quotes are on gradedPrices[].
  """
  PRICED

  """
  The printing is identified in the graded index but no grade has enough recorded sales to quote yet, which is typical of a new release.
  """
  UNQUOTED

  """
  The graded index lists several products under this name and number in this set and nothing distinguishes which is this printing. We do not guess.
  """
  AMBIGUOUS

  """
  This printing shares its only candidate in the graded index with another printing of the same card. The base printing carries the quotes; this variant does not.
  """
  COLLAPSED

  """
  The set is in the graded index but no product there carries this card's name and number.
  """
  UNLISTED

  """
  The graded index has no entry for this set, or none we could identify with confidence, so nothing in it can be quoted.
  """
  SET_UNLISTED

  """The graded index does not cover this game."""
  GAME_UNLISTED
}

"""
Why a card does or does not carry a TCGplayer quote. Same words as
CardmarketStatus. Null for Magic, which is priced from MTGJSON.
"""
enum TcgplayerStatus {
  """
  A TCGplayer product is identified and quoted. The quote is on prices[].
  """
  PRICED

  """
  The product is identified but TCGplayer publishes no market or low price for it yet, which is typical of a new release before its first sales.
  """
  UNQUOTED

  """
  TCGplayer lists several products under this number in this set and neither their names nor their images settle which one is this printing. We do not guess.
  """
  AMBIGUOUS

  """
  The set is on TCGplayer but no product there carries this card's number.
  """
  UNLISTED

  """
  TCGplayer has no set matching this one, or none we could identify with confidence, so nothing in it can be priced.
  """
  SET_UNLISTED
}

"""
Cardmarket lows split by language: what the cheapest copy in English,
German, Japanese and so on is selling for right now. Cardmarket's own
price guide carries no language, so 'Price.low' is the cheapest copy in
any language; these rows are per language. EUR.
A card has no rows until its language data is available, and a row with
zero offers is a real answer: nothing for sale in that language.
"""
type LanguagePrice {
  source: PriceSource!
  language: Language!

  """
  The same closed list 'Price.finish' uses, and the same word for the same
  printing: an unmarked listing of a holo rare is 'foil' as its guide quote
  is, a Reverse Holo listing is 'reverse-holo'.
  """
  finish: String!

  """ISO-4217 code. EUR."""
  currency: String!

  """
  Cheapest live article in this language, any condition. Null when none is for sale.
  """
  low: Float

  """Cheapest Near Mint or Mint article in this language."""
  lowNm: Float

  """Median asking price of the cheapest offers in this language."""
  market: Float

  """Live articles counted in this language."""
  offers: Int!

  """
  True when only the cheapest 50 articles overall were counted, so dearer copies in this language may exist beyond them.
  """
  truncated: Boolean!

  """
  On a truncated row, the dearest price that was seen. A condition missing
  from 'conditions' has no copy at or below it; above it is unknown. Null
  when the read was complete.
  """
  truncatedAbove: Float

  """
  Cheapest copy per condition, best first, optionally one condition. Only
  conditions with a live article appear; on a truncated row a missing one
  may still be for sale above 'truncatedAbove'.
  """
  conditions(condition: Condition): [ConditionPrice!]!

  """When this row was last updated."""
  updatedAt: String!
}

"""
Cardmarket's condition ladder, best first. Roughly TCGplayer's NM, LP, MP,
HP and DMG: MT and NM are Near Mint, EX and GD Lightly Played, LP and PL
Moderately to Heavily Played, PO Damaged.
"""
enum Condition {
  MT
  NM
  EX
  GD
  LP
  PL
  PO
}

type ConditionPrice {
  condition: Condition!

  """Cheapest live article in this language, finish and condition. EUR."""
  low: Float!

  """Live articles counted in this condition."""
  offers: Int!
}

"""One day's realized sales on Cardmarket."""
type SalePoint {
  """YYYY-MM-DD."""
  day: String!

  """Average price copies actually sold for that day."""
  avgSell: Float!
}

"""
What copies actually sold for, by day: the realized price, not an asking
price and not the guide. Every language and condition together, as
Cardmarket publishes it; on a product sold in two versions (a foil
switch on its page), the plain version's, with the other under the
finish `foil` or `reverse-holo`. Days with no sale have no point.
The series starts about 30 sale days before the card entered our
coverage (a month for a card that sells daily, longer for one that
rarely does) and grows from there. For Pokémon, Yu-Gi-Oh!, Dragon Ball
Super and One Piece, archived copies of the product page (2018 to 2023)
add older windows, with a gap before our first read.
"""
type SalesHistory {
  """ISO-4217 code. EUR."""
  currency: String!

  """Oldest first."""
  sales: [SalePoint!]!
}

"""
What kind of variation a finish is. SURFACE is how the card face is
treated (foil, reverse holo); PATTERN a foil pattern printed across the
card (Poké Ball, Master Ball); STAMP a mark added for an event (Staff,
Prerelease, League); EDITION a print run (1st Edition, Shadowless).
"""
enum FinishKind {
  SURFACE
  PATTERN
  STAMP
  EDITION
}

"""
Where a printing's picture comes from: its own product photo, or the card's picture when it has none.
"""
enum ImageFrom {
  PRINTING
  CARD
}

"""
The marketplace products a printing's quotes are read from. Null where a marketplace has none for it.
"""
type PrintingExternalIds {
  cardmarketId: Int
  tcgplayerId: Int
}

"""
One physical way the card was produced: a finish with a label, the picture
that shows it and its quotes. 'prices' regrouped by 'finish', so a Poké
Ball pattern, a Staff stamp or a Shadowless print is its own entry with its
own picture and its own quotes. 'key' is stable across re-resolution; key
user selections on it. After the card's own finishes come its stamped and
holo reprints sold as products of their own (Pokémon: the GameStop and EB
Games exclusives, Prize Pack, Trick or Trade), each keyed by its record's id.
"""
type Printing {
  """
  The 'finish' value, one of the closed finish vocabulary (see /docs/prices); for a reprint sold as a product of its own, that record's id.
  """
  key: String!

  """
  Human label: 'Holofoil', 'Poké Ball pattern', 'Shadowless', 'GameStop Exclusive', 'Trick or Trade 2024'.
  """
  label: String!
  kind: FinishKind!

  """
  The printing's own picture where a marketplace shows one; the card's otherwise.
  """
  images: Images!
  imageFrom: ImageFrom!
  externalIds: PrintingExternalIds!

  """
  This printing's quotes, one per source. A reprint's may span several finishes, each quote naming its own.
  """
  prices: [Price!]!

  """
  The card 'prices' are kept on: the id price history and sales are asked for under.
  """
  cardId: ID!
}

type PriceStatus {
  """Null only for a card no price run has considered yet."""
  cardmarket: CardmarketStatus

  """Null for a card no TCGplayer run has considered yet, and for Magic."""
  tcgplayer: TcgplayerStatus

  """Null only for a card the graded index has not considered yet."""
  graded: GradedStatus
}

"""
The language a printing is in. Codes follow Scryfall: ZHS is simplified
Chinese, ZHT traditional. Yu-Gi-Oh's JP and KR and its SP are JA, KO and ES.
"""
enum Language {
  EN
  DE
  FR
  IT
  ES
  PT
  JA
  KO
  RU
  ZHS
  ZHT
  TH
  ID
  PH
  QYA
  DW
  GRC
  HE
  AR
  LA
  SA
}

"""
Whose quotes a card's 'prices', 'gradedPrices' and 'priceStatus' are. OWN for
a printing quoted in its own right -- an English printing, an own-language
set, or a localized printing bound to a marketplace product of its own, as
Japanese One Piece is on Cardmarket; ENGLISH_TWIN for a localized printing
served its English twin's quotes, because the marketplaces sell that product
across languages and we do not invent a per-language figure; LOCALIZED_TWIN
for an English printing no marketplace sells in English, served the quotes
of its twin in another language, named on 'pricesTwin' (a Japan-only One
Piece promo, priced as the Japanese copy).
"""
enum PricesFrom {
  OWN
  ENGLISH_TWIN
  LOCALIZED_TWIN
}

"""
Whose picture a card's 'images' is. OWN for the printing's scan;
ENGLISH_TWIN for a localized printing in an English set its source has no
scan of (older European Pokémon, Magic printings Scryfall shows in English
only), shown its English twin's picture. Such a card has no 'hashes' and
recognition never answers it.
"""
enum CardImageFrom {
  OWN
  ENGLISH_TWIN
}

"""
The printing an English card's quotes are from, with LOCALIZED_TWIN, and the language its copies are in.
"""
type PricesTwin {
  id: ID!
  language: Language!
}

"""
Core fields are identical for every game. Game-specific stats live in
`gameData`, whose shape is declared by `Game.schema`.

One row per printing per language. 'name' is always the English name, so a
Japanese printing and its English twin are found by the same search; the
name as printed is 'printedName'.
"""
type Card {
  id: ID!
  game: Game!

  """The English name, on every language's printing."""
  name: String!
  language: Language!

  """The name as printed on the card. Null on English printings."""
  printedName: String

  """
  The English printing this one corresponds to: the same set and number for
  a localized twin in an English set; the same illustration, by artwork
  hash, for a card in an own-language set (Japanese Pokémon), whose set and
  number differ. Null on English rows and where none is found.
  """
  englishId: ID

  """
  The printings of this card's illustration in the other catalogue, linked
  by artwork (Japanese Pokémon): on a Japanese card every English printing
  of the picture, reprints included, nearest first (englishId is the
  first); on an English card the Japanese printings of it. Empty where none
  is found and on games whose languages share set and number.
  """
  sameArtwork: [ID!]!

  """
  The card this record reprints, for a Pokémon card sold as a product of
  its own outside the expansions: a stamped store exclusive, a Prize Pack or
  World Championship deck copy. Null elsewhere and where the base could not
  be told for certain.
  """
  baseCardId: ID
  pricesFrom: PricesFrom!

  """
  With LOCALIZED_TWIN, the printing quoted and its language. Null otherwise.
  """
  pricesTwin: PricesTwin
  set: Set!
  collectorNumber: String!
  rarity: String!
  artist: String
  text: String
  images: Images!
  imageFrom: CardImageFrom!
  hashes: Hashes
  prices: [Price!]!

  """
  The card's printings: 'prices' regrouped by finish, each with its label, picture and products. Plain card first; stamped and holo reprints sold as products of their own last.
  """
  printings: [Printing!]!

  """
  Graded quotes for this printing. Both arguments or neither: one slot, or every slot held.
  """
  gradedPrices(grader: Grader, grade: Grade): [GradedPrice!]!

  """
  Cardmarket lows by language, optionally one language. Empty until language data is available for this card.
  """
  languagePrices(language: Language): [LanguagePrice!]!

  """
  Realized Cardmarket sales by day over the last 'days' days (90 by default, 7305 at most). 'finish' asks for a printing Cardmarket sells as its own product (a Master Ball pattern, a stamp). Empty in the playground.
  """
  sales(days: Int, finish: String): SalesHistory!
  priceStatus: PriceStatus!
  legalities: JSON!
  gameData: JSON!
}

type PageInfo {
  page: Int!
  limit: Int!
  totalCount: Int!
  totalPages: Int!
  hasMore: Boolean!

  """Which marketplace the price filters and the price sort ran against."""
  priceSource: PriceSource!
}

type CardConnection {
  nodes: [Card!]!
  pageInfo: PageInfo!
}

type SchemaField {
  name: String!
  type: String!
  description: String!
}

type GameInfo {
  slug: String!
  name: String!
  shortName: String!
  publisher: String!
  released: Int!
  languages: [String!]!
  formats: [String!]!
  sampleCount: Int!
  schema: [SchemaField!]!
}

input CardFilter {
  game: Game

  """
  Part of the English name or of the name as printed: 'dracaufeu' with language FR, 'ルフィ' with JA on One Piece. Case, accents and punctuation are ignored.
  """
  name: String
  text: String
  rarity: String
  set: String

  """
  The number printed on the card, loosely: 6, 006 and 006/165 are the same card. Pair with `set` for one printing.
  """
  collectorNumber: String
  artist: String

  """
  Which language's printings. English when omitted, so a query that never
  mentions language sees one row per printing as before. Set `allLanguages`
  instead to get every language, one row each.
  """
  language: Language

  """
  Every language, one row per printing per language. Overrides `language`.
  """
  allLanguages: Boolean

  """
  Which marketplace `minPrice`, `maxPrice` and the price sort read from.
  Defaults to `CARDMARKET`. Bounds are in that source's currency, so
  `{ source: TCGPLAYER, minPrice: 50 }` means $50, not €50.
  """
  source: PriceSource
  minPrice: Float
  maxPrice: Float

  """
  Which slot of the graded index `GRADED_PRICE` sorts on. Both or neither;
  PSA 10 when neither.
  """
  grader: Grader
  grade: Grade

  """
  Filter on game-specific fields, e.g. `{ hp: 330 }` or `{ types: "Fire" }`. A
  value matches a scalar field exactly (case-insensitively) or a member of a
  list field. Dotted paths reach into object fields; pass them through a
  variable, since `banlist.tcg` is not a GraphQL name.
  """
  gameData: JSON

  """
  Inclusive lower bounds on numeric game fields, e.g. `{ hp: 300 }` for hp >= 300.
  REST spells the same thing `?minHp=300`.
  """
  gameDataMin: JSON

  """
  Inclusive upper bounds on numeric game fields, e.g. `{ manaValue: 3 }`. REST: `?maxManaValue=3`.
  """
  gameDataMax: JSON
}

type Query {
  """Look up a single card by its stable TCGGraph id."""
  card(id: ID!): Card

  """Query cards across every game with one filter shape."""
  cards(filter: CardFilter, sort: CardSort, page: Int, limit: Int): CardConnection!

  """Every supported game, with counts and its field declaration."""
  games: [GameInfo!]!

  """One game by slug."""
  game(slug: String!): GameInfo
}

scalar JSON