Feeds
Feeds are bulk, downloadable snapshots of a game's catalogue and prices. Instead of paging the API product by product, you download a single file with everything for a game and process it locally.
Three feeds are available per game:
| Feed | Contents | Rebuilt when |
|---|---|---|
| Catalog | Every product (cards and sealed) in the game, without prices | The game's catalogue changes |
| Expansions | Every expansion (set) in the game | The game's catalogue changes |
| Prices | Current price per product | The game's prices are refreshed |
File format
Every feed is a gzip-compressed, newline-delimited JSON file (.ndjson.gz): one JSON object per line. Decompress it once, then read it line by line — you never hold the whole file in memory.
Downloading a feed
Feed files aren't served inline. You ask the API for a feed's metadata, which includes a time-limited download link:
curl https://public-api.cardnexus.com/v1/feeds/mtg/catalog \
-H "Authorization: Bearer cnk_live_4f2d8c91e5b34a7f9e6c2d1a8b5f7e3c"
{
"feedType": "catalog",
"url": "https://cardnexus-feeds.s3.eu-west-1.amazonaws.com/feeds/mtg/catalog.ndjson.gz?X-Amz-Expires=3600&X-Amz-Signature=8a1f...",
"urlExpiresAt": "2026-06-08T15:04:11.000Z",
"checksum": "9f2c4e1b7d3a6f508c2e9b1d4a7f0c3e6b9d2f5a8c1e4b7d0a3f6c9e2b5d8f1a",
"sizeBytes": 5242880,
"recordCount": 18432,
"format": "ndjson",
"encoding": "gzip",
"lastRefreshedAt": "2026-06-08T14:04:11.000Z",
"generatedAt": "2026-06-05T02:11:47.000Z"
}
url expires at urlExpiresAt — request the metadata again for a fresh link. GET /v1/feeds/{gameId} returns all three feeds' metadata in one call.
Detecting changes
checksum is the SHA-256 of the file's uncompressed contents. It changes only when the contents change, so you can store the last checksum you processed and skip a download when it's unchanged.
generatedAt advances only when the contents change. lastRefreshedAt advances on every rebuild, including one that produced an identical file. Dedupe on checksum or generatedAt, not lastRefreshedAt.
Reading a feed
Fetch the metadata, download url, decompress, and parse each line as JSON.
import { createGunzip } from "node:zlib"
import { createInterface } from "node:readline"
const meta = await fetch("https://public-api.cardnexus.com/v1/feeds/mtg/catalog", {
headers: { Authorization: "Bearer cnk_live_4f2d8c91e5b34a7f9e6c2d1a8b5f7e3c" },
}).then((r) => r.json())
const file = await fetch(meta.url)
const lines = createInterface({ input: file.body.pipe(createGunzip()) })
for await (const line of lines) {
if (!line) continue
const product = JSON.parse(line)
// process product
}
import gzip, json, requests
meta = requests.get(
"https://public-api.cardnexus.com/v1/feeds/mtg/catalog",
headers={"Authorization": "Bearer cnk_live_4f2d8c91e5b34a7f9e6c2d1a8b5f7e3c"},
).json()
resp = requests.get(meta["url"], stream=True)
with gzip.open(resp.raw, "rt", encoding="utf-8") as f:
for line in f:
product = json.loads(line)
# process product
The file is gzip as a format — your HTTP client should not transparently decompress it. Download the bytes, then gunzip them yourself, as shown above.
Linking records across feeds
A catalog record's id is the product's stable identifier — the same id the Products and Pricing endpoints use. The prices feed references it as productId. An expansions record's id works the same way: the catalog feed references it as expansionId, with expansionSlug for the expansion's slug.
These ids are stable integers — a product or expansion keeps its id for its lifetime.
Changelog
Each time a game's catalogue is updated, an entry is added to its changelog — when it happened and how many products and expansions were added, modified, or removed, optionally with the slug and id of each one. Poll GET /v1/feeds/{gameId}/changelog to decide when to re-pull the catalog and expansions feeds.
Authentication
Feeds require a valid API key but no scope — any key works. See Authentication.