Scrollrack provides a simple and efficient REST API for validating trading card game decklists. This document details the endpoints, request/response formats, and usage examples.
| Method | URL | Description |
|---|---|---|
| POST | /api/validate |
Validate a TCG decklist |
| POST | /api/image |
Parse a decklist into card objects with images |
| GET | /api/games |
Every game, its formats, and the zones each format accepts |
| GET | /api/card/{game}/{id} |
One card by id: names, type, text, stats, image, legality |
| GET | /api/search?game=&q= |
Prefix name search within a game |
The GET endpoints are read-only and cacheable; they send Cache-Control and allow any origin (public card data). Card databases rebuild nightly, so a cached card is at most a day behind.
/api/validateThis endpoint validates a decklist against game-specific format rules.
POST /api/validate
Content-Type: application/json
| Parameter | Type | Required | Description |
|---|---|---|---|
game |
string | Yes | Game identifier (mtg for Magic: The Gathering, pokemon for Pokémon) |
format |
string | Yes | Format to validate against (e.g., commander for MTG, standard for Pokémon) |
list |
string | Yes | Plain text decklist with section formatting |
{
"game": "mtg",
"format": "commander",
"list": "~~Commanders~~\n1 Atraxa, Praetors' Voice\n\n~~Mainboard~~\n4 Sol Ring\n3 Cultivate\n92 Plains"
}
{
"game": "pokemon",
"format": "standard",
"list": "~~Pokemon~~\n4 Pikachu SVI 123\n2 Charizard PAL 45\n\n~~Trainer~~\n4 Boss's Orders FST 236\n\n~~Energy~~\n10 Basic Fire Energy"
}
The API returns a JSON response with validation results.
| Field | Type | Description |
|---|---|---|
valid |
boolean | Whether the deck is valid for the given format |
format |
string | The format that was validated against |
errors |
array | List of validation errors (only when valid is false) |
decklist |
object | Normalized decklist with card data (only when valid is true) |
cardsPerSection |
object | Count of cards in each section (only when valid is true) |
totalCards |
number | Total number of cards in the deck (only when valid is true) |
colors |
array | The deck's colors in the game's order — its identity card's (commander, Legend, Leader) when it has one, else the main deck's non-land cards. [] = colorless; absent for games without colors (only mtg, riftbound, onepiece, gundam carry them); null when the identity card is unknown. Returned for invalid lists too |
meta |
object | Format metadata including rules and requirements |
{
"valid": true,
"format": "commander",
"decklist": {
"commanders": {
"Atraxa, Praetors' Voice": {
"quantity": 1,
"id": "bf6c1462-f1d2-4f58-b7a4-cb5b21399152"
}
},
"mainboard": {
"Sol Ring": {
"quantity": 1,
"id": "0afa0e33-4804-4b00-b625-c2d6b61090fc"
},
"Cultivate": {
"quantity": 3,
"id": "6e209ae9-7eca-4cf0-8070-c39a4da517f7"
},
"Plains": {
"quantity": 95,
"id": "3e9f26c9-64ec-4a23-a2a9-3eee8d0dcaac"
}
}
},
"cardsPerSection": {
"commanders": 1,
"mainboard": 99
},
"totalCards": 100,
"meta": {
"requiredSections": ["mainboard", "commanders"],
"rules": {
"requiredSections": ["mainboard", "commanders"],
"deckSize": { "exact": 100 },
"commanders": { "max": 2 },
"mainboard": { "min": 98 },
"maxCopies": 1
}
}
}
{
"valid": true,
"format": "standard",
"decklist": {
"pokemon": {
"Pikachu": {
"quantity": 4,
"set_id": "SVI 123",
"id": "abc123"
},
"Charizard": {
"quantity": 2,
"set_id": "PAL 45",
"id": "def456"
}
},
"trainer": {
"Boss's Orders": {
"quantity": 4,
"set_id": "FST 236",
"id": "ghi789"
}
},
"energy": {
"Fire Energy": {
"quantity": 10,
"set_id": null,
"id": "jkl012"
}
}
},
"cardsPerSection": {
"pokemon": 6,
"trainer": 4,
"energy": 10
},
"totalCards": 20,
"meta": {
"requiredSections": ["pokemon", "trainer", "energy"],
"rules": {
"requiredSections": ["pokemon", "trainer", "energy"],
"deckSize": { "exact": 60 },
"maxCopies": 4
}
}
}
{
"valid": false,
"errors": [
"Too Many Copies Of: Mana Crypt (4 > 1)",
"Use of Illegal Card: Mana Crypt"
],
"format": "commander",
"meta": {
"requiredSections": ["mainboard", "commanders"],
"rules": {
"requiredSections": ["mainboard", "commanders"],
"deckSize": { "exact": 100 },
"commanders": { "max": 2 },
"mainboard": { "min": 98 },
"maxCopies": 1
}
}
}
Decklists should be structured using section markers with the following format:
~~Section Name~~
[quantity] Card Name
~~Mainboard~~: Primary deck cards~~Commanders~~: Commander cards (for formats like Commander)~~Sideboard~~: Sideboard cards~~Stickers~~: Sticker sheets (Commander formats, up to 10)~~Attractions~~: Attraction deck (Commander formats, at least 10)~~Pokemon~~: Pokémon cards ~~Trainer~~: Trainer cards~~Energy~~: Energy cards~~Decklist~~: Main deck (exactly 50, up to two colors)~~Sideboard~~: Optional Best-of-3 sideboard (exactly 10 when present). // Side Deck and Side: headers are also accepted. Copy and color limits apply across both sections combined.Cards can be formatted in several ways:
4 Lightning Bolt <- Quantity followed by card name
Lightning Bolt <- Quantity defaults to 1 if omitted
4 Pikachu SVI 123 <- Quantity, set code, card number, and name
2 Basic Fire Energy <- Energy cards can be specified by name
mtg)Formats:
commander: Commander/EDH format (100 cards including commander)paupercommander: Pauper Commander (100 cards, 1-2 commanders)modern, standard, legacy, etc. use default rules (60 card minimum, 15 card sideboard maximum)pokemon)Formats:
standard: Standard format (60 cards exactly)expanded: Same as aboveunlimited: Same as abovegundam)Formats:
standard: 50-card deck with an optional 10-card sideboard for Best-of-3 eventsThe API can return various error types:
Missing Sections: When required sections are missing
"Missing required section: ~~Commanders~~"Deck Size Violations: When deck size doesn't meet format requirements
"Deck must contain exactly 100 cards (mainboard + commanders). Currently: 65"Section Constraints: When a section violates its size constraints
"Mainboard must have at least 98 cards. Currently: 45"Card Quantity Violations: When a card exceeds its copy limit
"Too Many Copies Of: Lightning Bolt (5 > 4)"Legality Violations: When a card is not legal in the format
"Use of Illegal Card: Black Lotus"Unknown Cards: When a card name cannot be found in the database
"Unknown Card: Made Up Card Name"Card Name Mismatch (Pokémon only): When the provided name doesn't match the database name for the given set/number
"Card name mismatch: "Pkachu" does not match "Pikachu""/api/gamesLists every supported game with its format keys and, per format, the zones a decklist may carry.
GET /api/games
[
{
"code": "yugioh",
"name": "Yu-Gi-Oh!",
"formats": {
"tcg": {
"zones": [
{ "zone": "Main", "header": "Deck", "min": 40, "max": null, "required": true },
{ "zone": "Side", "header": "Side", "min": null, "max": 15, "required": false },
{ "zone": "Extra", "header": "Extra", "min": null, "max": 15, "required": false }
]
}
}
}
]
zone is one of Main, Side, Extra, Leader, Viceroy, Locations, Attractions, Tribe. header is the section title the parser expects in a text list.
/api/card/{game}/{id}One card by its entity id — the id returned in /api/image output and by /api/search. For Magic this is the Scryfall oracle id; for Yu-Gi-Oh the passcode; for Bandai games the card number.
GET /api/card/mtg/e6d09f4b-9c8d-4f57-8bd8-0be1b4c2ea7d
| Field | Type | Description |
|---|---|---|
id |
string | Entity id |
name |
string | English name when stored, else the first stored name |
names |
array | Every stored name with its language code |
type |
string | null | The card's kind as the parser labels it (e.g. Creature, Monster, Spell) |
typeLine |
string | null | Printed type line (Magic type line, Yu-Gi-Oh card type, Bandai category and traits, …) |
text |
string | null | Rules text |
stats |
string | null | Combat stats as printed: power/toughness or loyalty, ATK/DEF, power, AP/HP, HP, might |
tribe |
string | null | Game-specific grouping (Magic colour identity, Bandai colour) |
points |
number | null | Points value in point-based formats |
imgUrl |
string | null | Card image |
legality |
object | Format key → Legal, NotLegal, Banned, Restricted, or Halved |
data |
object | The game's raw extra columns (e.g. CMC, ManaCost, Level, Attribute) |
Returns 404 for an unknown game or id.
/api/searchPrefix search over card names within one game. Every whitespace-separated term must prefix a word in the name; matching ignores case and accents.
GET /api/search?game=mtg&q=thassa%20ora&limit=10
| Parameter | Required | Description |
|---|---|---|
game |
Yes | Game code from /api/games |
q |
Yes | At least 2 characters |
limit |
No | 1–50, default 20 |
[
{ "id": "…", "name": "Thassa's Oracle", "imgUrl": "https://images.topdeck.gg/game-images/mtg/….webp" }
]
One row per card, ordered by match rank.
The API currently does not implement rate limiting, but excessive usage may be throttled in the future.