Skip to main content
Read a trading card catalog over HTTP: cards, the expansions and series they belong to, the sealed products that contain them, and daily prices for all of it. Every response is JSON. Every endpoint reads with GET, except POST /v1/scan. Every price is USD, and an integer number of cents, never a decimal.

Base URL

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

Responses

Every read 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. Branch on code:
message is always present and never empty. It is written for a human reading a log, and its wording is not part of the contract. POST /v1/scan adds codes of its own. They are listed on Scan a card.

Paging

List endpoints page by cursor, not by offset, so rows do not shift under you between pages. A limit above the maximum is clamped rather than refused, and a value that is not a number falls back to the default. To walk a whole expansion, keep calling until next_cursor comes back null. 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: 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. You get back a ranked set of candidates, not one resolved id. See List cards for how the ranking and its ties behave. Some endpoints take include with a comma-separated list. On List cards it accepts variants, metadata and change. Without it the key is absent, not null and not [], so a response stays byte-identical to what it was before the field existed. A value the endpoint does not recognise is dropped rather than refused.

Caching

/v1/* responses carry an ETag. Send it back as If-None-Match and an unchanged resource answers 304 Not Modified with no body, which is 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 and 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 some corner of the catalog is staler than it should be. The API keeps answering either way. feeds is the part you can act on. Both catalog and tcgplayer report their own status, when each last synced successfully as synced_at, and the newest observed_on each holds. observed_on is always null for catalog, which carries no prices. Those last two answer different questions, and the gap between them is the useful part. A recent synced_at beside an old observed_on means nothing newer has been published for that feed, which is a different problem from a feed that has not synced at all. degraded while every feed reads ok is a real state, and a quiet one: what you are being served is current, and something behind it needs a look. last_updated is the most recent successful sync across every feed: the last moment any served data could have changed. It is null before the first sync.