Kairos Archive

Query API

Search the registry with the site's syntax and get JSON back. One endpoint, four routes, on query.kairosarchive.net.

Everything else on the API is a static file. This is the one part that computes, on its own hostname: GET https://query.kairosarchive.net/cards?q=… runs the same parser and evaluator the search page runs in your browser, over the same records, and returns a page of card records as JSON. The full search syntax applies, including unique:, sort: and order:. It exists for tools and bots; for anything heavier than a lookup, download the data and query it locally.

Routes

RouteAnswers
/cards?q=…A page of cards matching the query. Parameters: page (1-based), page_size (1 to 200, default 100), and unique, sort, order as alternatives to the words in the query.
/cards/named?exact=…One card by its exact name, case-insensitive. fuzzy=… instead takes the best match for a partial name.
/cards/randomA random card; q=… narrows the pool to a query's matches. Never cached.
/cards/autocomplete?q=…Up to 20 card names for a partial name, ranked the way the site's search box ranks them.
/cards/C000230A 302 to the static object, api.kairosarchive.net/v3/cards/C000230.json; likewise /cards/P000937.

A query

curl · minions of Fire costing 2 or less
curl --fail --location \
  --user-agent 'my-card-tool/1.0 ([email protected])' \
  'https://query.kairosarchive.net/cards?q=t:minion%20e:fire%20cost<=2'
Response · a list
{
  "object": "list",
  "release": "v3.3.3",
  "q": "t:minion e:fire cost<=2",
  "unique": "cards", "sort": null, "order": null,
  "total": 32, "page": 1, "page_size": 100, "total_pages": 1,
  "has_more": false, "next_page": null,
  "rules_text_total": 0, "rules_text_hits": [],
  "data": [
    {
      "object": "card",
      "codex_id": "C000140", "name": "Awakened Mummies", "type": "Minion", "category": "Spell",
      "rarity": "Exceptional", "slot": "Exceptional", "subtypes": ["Undead"], "elements": ["Fire"],
      "keywords": ["Burrowing"], "umbrellas": ["Evil"], "cost": 1, "attack": 3, "defense": 3, "power": 3, "life": null,
      "thr_air": 0, "thr_earth": 0, "thr_fire": 1, "thr_water": 0,
      "rules_text": "…", "has_back": false, "errata": true,
      "set_codes": ["001", "002"], "printing_ids": ["P000573", "P000574", "P000575", "P000576"],
      "default_printing_id": "P000575", "image_status": "lowres",
      "api_url": "https://api.kairosarchive.net/v3/cards/C000140.json",
      "kairos_url": "https://kairosarchive.net/cards/C000575/awakened-mummies",
      "image_urls": { "small": "…", "normal": "…", "large": "…", "original": "…" },
      "printing": { "printing_id": "P000575", "api_url": "…", "kairos_url": "…" }
    }
  ]
}

A record is the compact card the search page uses, plus its addresses, its image addresses, and printing: the printing the match was judged on when the query read one (a set, finish, product, artist or date term, a printing flag, or unique:prints), with that printing's facts and addresses; otherwise the card's default printing by id. With unique:prints or unique:art the same card appears once per matching printing.

rules_text_hits is the second group the search page shows: cards whose rules text, not their name, contains the bare words of a query, so a plain word never silently mixes the two. release names the registry release the answer came from. An id or a slug as the whole query answers with that one card.

Why a card matched

When a query asks about rules text, every record in data carries a matched object saying why it is there: the sentence that matched, and where inside that sentence. It is left out entirely when the query never read rules text, and on the routes that answer with a single card.

"matched": {
  "text": "Submerge (This may attack targets that are submerged.)",
  "ranges": [[0, 8]]
}

A range is a pair of offsets into matched.text, half-open, so text.slice(0, 8) is the matched word. There can be several, and they never overlap. Offsets address the snippet, not the card's whole rules_text, which the sentence may have been cut from and marked with an ellipsis.

The ranges are worked out by the same matcher that judged the search, so r:merge marks the merge inside Submerge while r=merge matches no card at all. Mark them however suits you: the search page wraps them in <mark>, the Discord bot in bold. Matching the text again yourself is the one way to disagree with the result you were given.

From page JavaScript

Answers carry Access-Control-Allow-Origin: * and a five-minute Cache-Control, so a browser or a Worker can call the API directly.

fetch · every printing by one artist
const url = new URL("https://query.kairosarchive.net/cards");
url.searchParams.set("q", 'a:"Drew Tucker" unique:prints');
const page = await (await fetch(url)).json();
for (const card of page.data) console.log(card.name, card.printing.set_name, card.printing.finish);

Errors

Every answer is JSON with an object field, errors included. A query the parser cannot read is a 400 with the parser's own messages in warnings; a query with no matches is a 200 with an empty data. A path the API does not serve is a JSON 404.

Response · a query that could not be read
{ "object": "error", "status": 400, "code": "bad_query",
  "details": "The query could not be read.",
  "warnings": ["cost: \">3\" is not a number - try cost:3, cost>=3, …"] }
StatuscodeWhen
400missing_queryq is absent or blank
400bad_querythe parser rejected the query; warnings says why
400query_too_longq is over 500 characters
400bad_parameterunique, sort, order or page_size is not a value the API takes
400missing_parameter/cards/named without exact or fuzzy
403user_agent_requiredthe request sent no User-Agent
404not_foundno such name, no random match, or no such route
429rate_limitedmore than 60 requests a minute from one address; Retry-After: 60
405method_not_allowedanything but GET, HEAD or OPTIONS
503data_unavailablethe card data could not be loaded; try again shortly

The User-Agent rule applies here as everywhere on the API; the query checks it itself, so a request without one gets a JSON 403.

Fair use

The query API is for lookups by people and their tools: a bot answering a question in a channel, a script checking a few cards, a page showing a result. It is not a backend for another service. If your application answers searches, download the data, one file that changes a few times a year, and query it locally; the search grammar is open source and runs anywhere JavaScript does.

Two rules keep it that way. A request must carry a User-Agent naming your project, as everywhere on the API. And one address gets 60 requests a minute; past that the API answers 429 with Retry-After for the rest of the minute. Answers cache for five minutes, so a repeated query costs nothing.

What it reads, and how fresh it is

The API reads the same compact card and printing lists this site builds from the verified release, so an answer and the search page agree to the card. A new release rebuilds the site; the API picks the new lists up within ten minutes and says which release it answered from. Slug lookups resolve current slugs; for any slug ever issued, use /v3/slugs/{slug}.json.


Reference: registry API contract and published object definitions. Report a data or documentation issue.