Changelog
Notable changes to the CardNexus Public API. The most recent release is listed first.
Versions follow semantic versioning. The API is in preview, so versions are 0.x: the minor number changes on any new release — including a breaking change, such as an endpoint or field being removed, renamed, or made stricter — and the patch number on documentation-only fixes. Breaking changes are always called out in the release notes below. The version here matches info.version in the API Reference.
0.8.0 — 2026-07-19
Search your inventory server-side — by product name, tag, location, and more — and export exactly the lines a search matches.
Breaking changes
filters on POST /v1/inventory/bulk/export has a new shape. It is now the same filter set POST /v1/inventory/search accepts, so a search can be turned into an export unchanged.
Exports without filters are unaffected — if you never pass filters, there is nothing to change.
If you do pass filters, update each field:
| Field | Before | Now |
|---|---|---|
game |
"game": "mtg" |
"gameFilters": { "game": "mtg" } |
productId |
"productId": [50212] |
"productIds": [50212] |
condition |
"condition": "NM" |
"condition": { "op": "or", "values": ["NM"] } |
language |
"language": "en" |
"language": { "op": "or", "values": ["en"] } |
finish |
"finish": "Foil" |
"finish": { "op": "or", "values": ["Foil"] } |
graded, forSale |
"forSale": true |
unchanged |
customId, customIdPrefix, customIdContains |
"customIdPrefix": "LOC-" |
unchanged shape; matching is now case-insensitive |
commentContains |
"commentContains": "binder" |
unchanged shape; matches whole words in order, not substrings inside a word |
The same export, before and after:
{ "format": "csv", "filters": { "game": "mtg", "condition": "NM", "language": "en", "forSale": true } }
{
"format": "csv",
"filters": {
"gameFilters": { "game": "mtg" },
"condition": { "op": "or", "values": ["NM"] },
"language": { "op": "or", "values": ["en"] },
"forSale": true
}
}
Two behaviour notes that come with the new shape:
- Filtered exports match lines the same way the search endpoint does, so the selection can lag your latest changes by a few seconds. Exports without
filtersalways reflect your latest changes, as before. - Every other search filter now works on exports too:
tags,location,name,quantity,listingPrice,productType,productCategory, andexpansionId— export a single location, every untagged line, or every line matching a name.
Added
- Lines —
POST /v1/inventory/searchsearches your inventory lines. Filter by free-textname(ranked by relevance),tagsandlocationby label name with{ "op": "and" | "or", "values": [...] }semantics — anullvalue matches lines with no tag or no location —customId/customIdPrefix/customIdContains(case-insensitive),commentContains(whole words in order),condition,language,finish,productType,graded,forSale,quantityandlistingPriceranges,productIds,expansionId, and the samegameFiltersobjectPOST /v1/products/searchaccepts. Sort withsortByandsortDirection. Results are offset-paginated over the first 10,000 matches and use the same line shape asGET /v1/inventory; they can lag your latest changes by a few seconds, whereGET /v1/inventoryalways reflects them. Requiresinventory:read, with a dedicatedinventory-searchrate limit of 600 per hour. See Tags & locations. - Lines —
GET /v1/inventorytakes two new filters:location(one location name) andtags(repeatable — matches lines carrying any of the named tags). Names match case-insensitively. - Listings — the same
locationandtagsfilters onGET /v1/listings. - Tags / Locations —
POST /v1/inventory/tagsandPOST /v1/inventory/locationsaccept"upsert": true: when a label with that name already exists, the call returns it — updated with anycolororiconyou sent — instead of failing withCONFLICT. One call then guarantees the label exists before you reference it on inventory lines.
Fixed
- Lines / Listings —
GET /v1/inventoryandGET /v1/listingsnow accept a singleproductId, e.g.?productId=10000. Passing one value previously returned400; passing two or more (?productId=10000&productId=10001) already worked. Both forms now behave the same. - Sales / Purchases — the same fix for the
statusfilter onGET /v1/salesandGET /v1/purchases: a single?status=…is now accepted, matching the repeated form. - Products — examples across the reference and guides showed
yugiohas the Yu-Gi-Oh! game slug; the actual slug isygo. Example-only fix; the endpoints are unchanged.
0.7.0 — 2026-07-18
Adding or updating a card in a list with a language, finish, or game the product doesn't exist in now fails with a clear validation error instead of a server error.
Changed
- List items —
POST /v1/lists/{listId}/itemsnow returns a422naming what's wrong with a card instead of a500:LANGUAGE_NOT_AVAILABLEwhen the product doesn't exist in the language you sent (the response lists the languages it does exist in),FINISH_NOT_AVAILABLEwhen it doesn't exist in the finish you sent (the response lists the finishes it does),PRODUCT_GAME_MISMATCHwhen the product belongs to a different game than the list, andITEM_NOT_FOUNDwhen theitemIdyou sent is not in the list.
0.6.0 — 2026-07-17
Every sold item names the inventory line it came from, cart items say what you're buying — condition, finish, language, grading — and each optimizer package names its seller.
Added
- Sales — each item on a sale now carries the inventory line it sold from:
inventoryId, which you can look up withGET /v1/inventory/{inventoryId}, andcustomId, your own identifier for that line captured at the time of sale (nullwhen the line had none). OnGET /v1/sales/{orderNumber}andGET /v1/sales. - Webhooks — the
order.createddelivery you receive as the seller now includesinventoryIdandcustomIdon each item, matching the sale response. See webhooks. - Cart — cart items carry the card detail of the listing they come from:
condition,language,finish, andgraded. Returned by every endpoint that returns your cart —GET /v1/cart,POST /v1/cart/items,PATCH /v1/cart/items/{listingId},DELETE /v1/cart/items/{listingId},DELETE /v1/cart, andPOST /v1/optimizer/runs/{id}/apply.
Changed
- Optimizer (experimental) — each package in a run's result embeds its
seller— id, username, country, type, and rating — replacing the baresellerId, onGET /v1/optimizer/runs/{id}. - Optimizer (experimental) — each option in a run's result carries
modes, the list of objectives it satisfies (lowest_price,fewest_sellers,balanced), replacing the singlekind. When two modes produce the same solution, the run returns one option listing both — a run requestinglowest_priceandfewest_sellersthat converges on one cart now says so explicitly. The same shape applies to the in-progress options returned while a run issolvingand to the options summary on theoptimizer.run.completedwebhook event. - Optimizer (experimental) —
POST /v1/optimizer/runs/{id}/applytakes the option to apply asmode(waskind). Any mode listed on an option selects that option.
0.5.1 — 2026-07-16
Buy and sell through the API: mark sales shipped or cancel them, browse a product's Marketplace listings, and fill a shopping cart — item by item or straight from an optimizer run.
Added
- Sales — act on the orders you're selling.
POST /v1/sales/{orderNumber}/mark-shippedmarks an order shipped and registers its tracking number (the carrier is detected automatically); allowed while the order is awaiting dispatch or has an open cancellation request.POST /v1/sales/{orderNumber}/cancelcancels an order awaiting dispatch, with a reason code and a note to the buyer — the buyer is refunded in full and the cards return to your inventory. Both return the updated sale and require thesales:writescope. - Products —
GET /v1/products/{productId}/listingsreturns a product's Marketplace listings: each listing's price, quantity for sale, condition, language, finish, grading, seller note, and the seller behind it. Sorted cheapest first, cursor-paginated, and filterable bycondition,language,finish, andregion; passdeliveryCountryto keep only sellers who ship to you, with each seller's shipping charge. Any valid API key works — no scope; requests count against a dedicatedproduct-listingsrate limit of 120 per hour, raisable per account through support. - Cart — a shopping cart, under the new
cart:readandcart:writescopes.GET /v1/cartreturns it grouped by seller with per-seller shipping.POST /v1/cart/itemsadds up to 1000 listings bylistingIdin one call, each item succeeding or failing on its own; thedeliveryCountryyou send applies on every call.PATCH /v1/cart/items/{listingId}sets an item's quantity (0removes it),DELETE /v1/cart/items/{listingId}removes an item, andDELETE /v1/cartclears the cart. Every call returns the updated cart. - Optimizer (experimental) —
POST /v1/optimizer/runs/{id}/applyreplaces your cart with one of a finished run's options in one step. To add a run's items to your cart instead, pass theirlistingIdandquantitytoPOST /v1/cart/items.
Fixed
- Pricing — looking up a product that doesn't exist on
GET /v1/products/{productId}/prices,GET /v1/products/{productId}/prices/history, andGET /v1/products/{productId}/salesnow returns404 PRODUCT_NOT_FOUND, as documented. It previously returned a500. - Sales / Purchases —
shipping.carrieris nownulluntil the carrier has been detected from the tracking number, matching its documentation. It previously showed the textunknown. - Bulk operations — the request samples for
POST /v1/inventory/bulk/import,POST /v1/inventory/bulk/import/cardmarket,POST /v1/inventory/bulk/import/tcgplayer, andPOST /v1/inventory/bulk/import/tcgpowertoolsshowed a JSON body with the file as a string. They now show amultipart/form-datarequest with the file in thefilefield, matching what the endpoints accept. The endpoints themselves are unchanged. - Lines — the same fix on
PUT /v1/inventory/{inventoryId}/media: the request sample is nowmultipart/form-data.
0.5.0 — 2026-07-13
One price shape everywhere: every price is a cardmarket, tcgplayer, or cardnexus block, under a map named pricesByFinish.
Added
- Products — product responses now include the listings live on the CardNexus marketplace: a
cardnexusblock sits next tocardmarketandtcgplayerin each card'spricesByFinishand each sealed product'sprices, onGET /v1/products/{productId}andPOST /v1/products/search— the same blockGET /v1/products/{productId}/pricesreturns. - External ids —
externalIdsnow also carries the game's card-database references next to the Cardmarket / TCGplayer ids:scryfallIdandscryfallOracleIdfor Magic: The Gathering,konamiIdfor Yu-Gi-Oh!,fabIdfor Flesh and Blood,dreambornIdfor Disney Lorcana, andriotIdfor Riftbound. Returned everywhere products are —GET /v1/products/{productId},POST /v1/products/search, and the catalog feed.
Changed
- Products — a card's
pricesByFinishand a sealed product'spricesare now keyed by source —cardmarket(EUR),tcgplayer(USD),cardnexus— with the currency as a field inside each block, instead of by currency (eur/usd). Every pricing surface now uses this one block shape. - Pricing — the response map of
GET /v1/products/{productId}/pricesis namedpricesByFinish(wasfinishes).finishesnow always means one thing: the list of finishes a product exists in. - Prices feed — the same rename on every line: the price map is
pricesByFinish(wasfinishes). See the prices feed guide. - Pricing — the blocks on
GET /v1/products/{productId}/pricesand on product responses no longer carry adate— they reflect the moment of the request. The prices feed keepsdateon itscardmarket/tcgplayerblocks (the calendar day of that marketplace snapshot), and price history keeps its per-daydate. - Catalog feed —
attributesnow carries exactly the game's gameplay fields, the same keys and values the Products endpoints return. The card-database ids that used to sit there moved intoexternalIds.
0.4.0 — 2026-07-13
Feed records now use the same field names as the API endpoints, and an experimental cart optimizer arrives.
Added
- Cart optimizer (experimental) — optimize a basket of cards into the cheapest, fewest-seller, or balanced ways to buy it across the marketplace:
POST /v1/optimizer/runsstarts a run in the background andGET /v1/optimizer/runs/{id}polls it; theoptimizer.run.completedandoptimizer.run.failedwebhook events fire when a run finishes. These endpoints are experimental, even by this preview API's standards — expect their request and response shapes to change more than the rest of the API. - Catalog feed — each product line now carries
rarity,imageBackUrl,productCategory, andexternalIds(the product's Cardmarket and TCGplayer ids, one per finish). See the catalog feed guide. - Prices feed — the marketplace price blocks now carry the full daily snapshot:
low,mid, andhightiers,currency, and the snapshotdate, alongsidemarketValueand the trend percentages. See the prices feed guide. - Catalog changelog — with
includeChanges=true,GET /v1/feeds/{gameId}/changelogentries now identify each changed product and expansion by id as well as slug:productIdon product entries,expansionIdon expansion entries.
Changed
- Catalog feed — each product line's identifier is now
id(wasproductId) and its kind isproductType(wastype), matching the fields of the same name onGET /v1/products/{productId}. - Expansions feed — each expansion line's identifier is now
id(wasexpansionId), matchingGET /v1/games/{gameId}/expansions. - Prices feed — each line's price blocks now live under
finishes(wasprices), keyed by source —cardmarket,tcgplayer,cardnexus— instead of by currency, the same shape asGET /v1/products/{productId}/prices. - Sealed products —
eanandskuare no longer returned on sealed products acrossGET /v1/products,GET /v1/products/{productId}, andPOST /v1/products/search.
0.3.0 — 2026-07-08
Match cards to every printing by name slug.
Added
- List items — each card in
GET /v1/lists/{listId}now carriesnameSlug, the URL-friendly slug of the product name. Pass it as thenameSlugfilter onPOST /v1/products/searchto list every printing of that card. - Catalog feed — each product line now carries
nameSlug. See the catalog feed guide andGET /v1/feeds/{gameId}/catalog.
0.2.0 — 2026-07-08
Build and manage your lists — decks, want lists, and for-sale lists.
Added
- Lists — create, read, update, and delete your lists:
GET /v1/lists,POST /v1/lists,GET /v1/lists/{listId},PATCH /v1/lists/{listId}, andDELETE /v1/lists/{listId}. See the Lists guide. - List items — add cards to a list, update them, and remove them:
POST /v1/lists/{listId}/itemsandDELETE /v1/lists/{listId}/items/{itemId}. See the Lists guide. - Scopes — two new API-key scopes gate the endpoints above:
lists:readandlists:write.
0.1.0 — 2026-07-07
Organise your inventory with tags and locations.
Added
- Tags — attach free-form labels to inventory lines to group them however you like. Create, list, rename, and delete them:
GET /v1/inventory/tags,POST /v1/inventory/tags,PATCH /v1/inventory/tags/{tagName}, andDELETE /v1/inventory/tags/{tagName}. See Tags & locations. - Locations — label where you keep your stock; a line sits in at most one. Create, list, rename, and delete them:
GET /v1/inventory/locations,POST /v1/inventory/locations,PATCH /v1/inventory/locations/{locationName}, andDELETE /v1/inventory/locations/{locationName}. See Tags & locations.
Changed
- The inventory write endpoints —
POST /v1/inventoryandPATCH /v1/inventory/{inventoryId}— now acceptlocationandtags, referenced by name. See Managing lines. - The bulk endpoints carry a line's
locationandtagsin both directions — through bulk update and bulk import, and out through bulk export.