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.13.0 — 2026-08-13
Search a card across several expansions at once. Carry your own references and fulfillment states on your sales, and filter your sales by them. Read your account id from your own account, and re-approving an application now changes what it can do.
Breaking changes
expansionId takes a list. On POST /v1/products/search and POST /v1/inventory/search, expansionId now takes an array of up to 200 expansion ids instead of a single id, and matches any of them. Wrap the id you send today in an array: "expansionId": 42 becomes "expansionId": [42].
The expansion game filter is gone. gameFilters.filters.expansion no longer exists on POST /v1/products/search, POST /v1/inventory/search and POST /v1/inventory/bulk/export. It matched on ids that are not the expansion ids these endpoints return, so no value you could read from the API ever matched. Send expansionId instead. A request that still carries the filter is accepted and the filter is ignored, so results widen to every expansion.
Added
Sales metadata — every sale has a metadata object of your own key/value pairs. CardNexus never reads or changes them, and the buyer never sees them.
PATCH /v1/sales/{orderNumber}/metadatawrites them. The write merges: keys you send are set, keys you send asnullare removed, keys you leave out keep their value — so two of your systems can each own their keys on the same sale. Works in any status, including on completed and cancelled sales.metadataonGET /v1/salesfilters your sales on exact key/value pairs —?metadata[fulfillment_stage]=packed, repeatable for up to 10 keys, AND-combined.POST /v1/sales/{orderNumber}/mark-shippedtakes the samemetadataobject, so a sale can be shipped and stamped in one call. The two either both apply or neither does.metadatais returned on every sales response, and on the seller's copy of theorder.status.changedwebhook underdata.metadata—nullon the buyer's copy.
Keys start with a letter and may contain letters, digits, _, . and -, up to 64 characters. Values are strings of up to 500 characters. A sale holds at most 50 keys; a write that would go past that stores nothing and returns 422 METADATA_LIMIT_EXCEEDED. See Track sales in your own system.
idonGET /v1/account/me— your account id. It is the same idPOST /v1/connect/exchangereturns to an application, and the one that goes in theCardNexus-Accountheader.
Fixed
- Sending
gameFilterswith any filter in it returned500onPOST /v1/products/search. Filtering on a game's own attributes — rarity, colour, finish — now works. - When an account goes through the connect flow again, the permissions it approves now replace the ones it granted before. Previously the second approval was ignored and the account kept its original permissions, so an application whose permissions had been widened never received the new ones even after the account re-approved.
POST /v1/connect/exchangereturns the permissions currently in force. See Building an integration.
0.10.0 — 2026-08-07
The fixed 10-unit-per-listing cart cap is gone — each seller now sets their own per-listing limit, and most set none.
Changed
- Cart —
quantityonPOST /v1/cart/itemsandPATCH /v1/cart/items/{listingId}no longer has a fixed maximum of 10. A cart line can hold any quantity the listing has for sale, up to any per-listing limit the seller sets. - Cart — the
QUANTITY_LIMIT_EXCEEDEDrejection code onPOST /v1/cart/itemsnow means the seller's per-listing limit was exceeded, anddata.maxcarries that seller's limit instead of the former fixed 10. Sellers without a limit never return it.
0.9.0 — 2026-08-07
Third-party applications can act on accounts on their owners' behalf. Search by name and print number together, in another language, and find the cheapest copy that can actually reach you. Trace an exported row back to the line it came from. See exactly which fields a rejected request failed on.
Breaking changes
Some list parameters now have a maximum length. Sending more entries than the maximum returns 400. Requests within the limits are unaffected.
| Endpoint | Parameter | Maximum |
|---|---|---|
POST /v1/products/search |
productIds |
200 |
GET /v1/inventory, GET /v1/listings |
productId |
200 |
GET /v1/inventory, GET /v1/listings |
tags |
50 |
POST /v1/inventory/search |
language.values |
50 |
POST /v1/inventory/bulk/export |
filters.language.values |
50 |
POST /v1/optimizer/runs |
sellers.include, sellers.exclude |
200 |
POST /v1/optimizer/runs |
sellers.countries |
250 |
To look up more products than one call takes, split the ids across several calls. The maximum for each parameter is listed on its field in the reference.
Added
Integrations — an approved application can act on an account on its owner's behalf: it authenticates with its own credential and names the account in the CardNexus-Account header. Every endpoint works unchanged, bounded by the permissions that account granted. See Building an integration.
POST /v1/connect/exchange— exchange the one-time code from the connect redirect for the account id and its granted permissions.POST /v1/accounts— create an account for a seller who isn't on CardNexus yet, staged until they finish onboarding. Requires the managed-account privilege. Capped per day; past the cap you getQUOTA_EXCEEDEDwithlimitandresetAt.POST /v1/account/onboarding-link— issue a hosted-onboarding link so a managed account's seller can accept the terms, set up payouts, and go live.- Webhooks carry an
accountfield, so one endpoint can serve every account you manage.
Products — POST /v1/products/search takes a listings object and answers with what you can actually buy. Every result gains an availability block: listingCount, plus cheapest with its price, quantity, condition, language, seller, and a listingId for POST /v1/cart/items. Filter with deliveryCountry, condition, language, finish, and inStock. deliveryCountry applies the checkout rule, so anything returned can be added to a cart. See Listing-aware search.
Search filters — printNumber and nameSlug on both POST /v1/products/search and POST /v1/inventory/search, and in the filters of POST /v1/inventory/bulk/export. nameSlug returns one card across every printing and expansion.
Accept-Language (en, fr, it, es, de) on product search, inventory search, and bulk export — Blizzaroi with Accept-Language: fr finds Abomasnow. Product and expansion names come back translated where one exists.
Bulk operations — exported rows carry inventoryId and updatedAt. inventoryId is the line's own id, the same one GET /v1/inventory and GET /v1/listings return, so a row traces back to its line and can be passed to PATCH /v1/inventory/{inventoryId}. productId names the card, but several of your lines can share one. Both columns are ignored on import.
Sales — every item of GET /v1/sales carries shippingAddress, so a batch of parcels can be prepared from one call.
Lists — bannerUrl on the list endpoints, or null when a list has no banner.
Validation errors — a 400 names each rejected field under data.issues: field is a dotted path into your body (lines.3.quantity), message says what's wrong. Up to 100 per response. See Error handling.
Changed
- Products / Lines —
namefree-text understands a print number before or after the name (sephiroth 44), with or without padding and prefixes (38finds038,44findsST02-44), and an expansion code plus print number (msh-54,msh 54,msh54). On product search an expansion code alone (msh) surfaces that expansion after any name matches. - Products —
nameSlugis matched ignoring case. A slug differing only in case used to return nothing. - Bulk operations — Bulk import & export now states that a
jsonexport leaves out any field a line doesn't set, so a line with no tags has notagskey. Acsvexport always writes every column.
Removed
- The
products:read,pricing:read, andmarketplace:readscopes. Catalogue, pricing, and feed endpoints need no scope — any valid key reads them — so these granted nothing. They no longer appear when creating a key; existing keys carrying them work exactly as before.
Fixed
- All endpoints — every error response carries
data, as documented. Some errors previously left it out.
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.