Skip to main content
A read-only HTTP API over a catalog of trading cards, the expansions and series they belong to, the sealed products that contain them, and the daily prices of all of it. Every price is an integer number of cents — USD on the TCGplayer side, EUR on the Cardmarket side — never a decimal. Everything is JSON, every read is a GET, and one endpoint takes a photo of a card and tells you which card it is.

Base URL

Every endpoint below /v1 needs a key. See Authentication.

Responses

Every read of the catalog — series, expansions, cards, products and their prices — answers with the same two keys, and so does every failure. Exactly one of them is ever non-null, so checking either one tells you about the other. A detail endpoint puts the object in data:
A list puts its rows and its cursor in data as well. next_cursor sits inside the envelope rather than beside it, so data and error are the only two keys any response has:
next_cursor is null on the last page.

Errors

Every failure has the same shape, and the code is the part to branch on:
message is always present and never empty — it is for a human reading a log, and its wording is not part of the contract. Branch on code. POST /v1/scan adds codes of its own; they are listed on Scan a card.

Paging

List endpoints page by cursor, not by offset — rows do not shift under you between pages. A limit above the maximum is clamped, not refused, and a value that is not a number falls back to the default. Walking a whole expansion means calling until next_cursor comes back null — here, three cards at a time:
Treat a cursor as opaque. It encodes sort position, and constructing one by hand will break. Searching with q changes what a page contains, not how you walk it: the rows come back best-match first, each carrying a score, and the same cursor keeps working.

Searching

There is no search endpoint. Every list takes q and searches its own resource: cards by name or collector number, series, expansions and products by name. q composes with that list’s own filters, so ?q=charizard&expansion_id=base1 searches inside one expansion. What comes back is a ranked set of candidates rather than one resolved id. See List cards for how the ranking and its ties behave, and Scan a card to identify a card from a photo instead of a name. Some endpoints take include with a comma-separated list. Today only prices is understood, on List cards. Without it the prices key is absent, not null and not [], so a response stays byte-identical to what it was before the field existed.

Caching

/v1/* responses carry an ETag. Send it back as If-None-Match and an unchanged resource answers 304 Not Modified with no body — cheaper to receive and cheaper to parse. It is not cheaper against your rate limit: the limiter counts the request before the 304 is produced, so a conditional request that comes back unchanged costs exactly as much of your budget as a full 200 would.

Versioning

The version is in the path. /v1 is current, and a breaking change would arrive as /v2 rather than by altering /v1 under you. Adding a field to a response is not breaking, so parse defensively: ignore keys you do not recognise.

Service health

GET /health is unauthenticated and reports whether the data being served is current:
status is ok or degraded. Degraded means a background task has not succeeded inside its window, so some corner of the catalog is staler than it should be — the API keeps answering.