Skip to main content
A card list is an explicit set of specific cards — a binder, a shoebox, the stack going to a convention. Where a saved search re-evaluates against whatever matches today, a list is fixed membership: these exact cards. Lists compose with the rest of the API through the list:<slug> search token, so pricing rules, label printing, and inventory/order filters all work on a list with no extra machinery. All of these endpoints are mounted at /api/lists (not /api/v1/lists) and support Bearer authentication (Authorization: Bearer YOUR_API_KEY) for AI assistants and integrations. Reading needs mcp:read; every write needs mcp:write.

List your lists

GET /api/lists
Returns each list with membership counts and the search handle that reads it:
{
  "lists": [
    {
      "id": 12,
      "name": "Con Box",
      "slug": "con-box",
      "position": 0,
      "card_count": 240,
      "active_count": 231,
      "gone_count": 9,
      "query": "list:con-box"
    }
  ],
  "max_cards_per_list": 1000
}
active_count is members still active in inventory; gone_count is members that have since sold or been delisted (a sold card stays on the list and is reachable via list:con-box is:gone). Pass ?tcgplayer_id= or ?card_id= to add a member boolean to each row indicating whether that card is on the list.

Read a list’s members

The query field is the search token — note it goes in the search param (an unrecognized param name is ignored and returns your whole inventory, not an error). Hand it to any search-aware endpoint:
GET /api/cards?search=list:con-box
Add is:gone to see only the tombstoned members — cards that have sold through or been delisted but that the list still remembers (search=list:con-box is:gone). Plain list:<slug> returns the full membership; everything else hides gone rows.

Create a list

POST /api/lists
{ "name": "Con Box" }
Returns the created list (201). The slug is minted once at creation and is stable across renames, so list:<slug> references stay attached. A seller may have up to 100 lists; a duplicate name returns 422.

Rename a list

PATCH /api/lists/{id}
{ "name": "GP Box" }
Rename only — the slug does not change. Pricing rules written against the list name (list:"Old Name") re-resolve so their matches empty out instead of silently repricing a stale set.

Delete a list

DELETE /api/lists/{id}
Removes the list and its memberships ({ "deleted": true }). The cards themselves are untouched.

Add cards to a list

POST /api/lists/{id}/cards
The body accepts the same filter contract as GET /api/cards, in precedence order:
FieldMeaning
tcgplayer_idsSeller SKU ids — the dashboard’s lingua franca. Custom (photo/Collectr) listings carry a negative synthetic SKU id and are addable here too.
card_idsInventory-card primary keys — the id field on each card row from GET /api/cards. Use this when you already hold the row id.
search + game/product_line/status/…The Cards-tab filter — adds exactly the cards you’ve filtered to, resolved server-side
{ "tcgplayer_ids": [11542, 88311] }
The search form accepts the full inventory DSL, including price and quantity predicates — so “add every Magic card worth $20+” is one server-side call, no client-side paging:
{ "search": "price>=20", "game": "Magic" }
Returns { "list": { … }, "added": 2, "requested": 2, "truncated": false }. requested is how many of your cards the payload resolved to, so you can tell a true no-op (added: 0, requested: 0 — nothing matched) from “already on the list” (added: 0, requested: 2). Adds are idempotent. The per-list cap is max_cards_per_list (1000); an over-cap add returns truncated: true, and an already-full list returns 422 { "code": "list_full" }. Ids that belong to another seller are silently skipped.

Remove cards from a list

DELETE /api/lists/{id}/cards
{ "tcgplayer_ids": [11542] }
The target ids travel in the request body. Returns { "list": { … }, "removed": 1 }; removing a non-member is a no-op.

Over the MCP

The hosted MCP wraps every endpoint above in a typed hoard.lists.* binding, so an AI assistant manages lists without composing raw HTTP. Reads run in hoard_read; the mutations run in hoard_write (they throw in read mode). Run hoard.describe('lists') to print these signatures live.
BindingEffectMaps to
hoard.lists.list()readGET /api/lists
hoard.lists.create(name)writePOST /api/lists
hoard.lists.rename(id, name)writePATCH /api/lists/{id}
hoard.lists.delete(id)writeDELETE /api/lists/{id}
hoard.lists.addCards(id, payload)writePOST /api/lists/{id}/cards
hoard.lists.removeCards(id, payload)writeDELETE /api/lists/{id}/cards
payload is the same filter contract as the REST add/remove body ({ tcgplayer_ids } | { card_ids } | { search, game, … }). name must be a string — passing an object throws rather than creating a junk-named list.
// Build a con list and read its members back, all over the MCP.
const { id } = hoard.lists.create("GP Vegas");                 // hoard_write
hoard.lists.addCards(id, { search: "is:foil", game: "Magic" }); // hoard_write
hoard.inventory.listCards({ search: "list:gp-vegas" });         // hoard_read — the members
Reading a list’s members goes through the list:<slug> token (the query field on each list row), which any search-aware surface accepts — hoard.inventory.listCards({ search: "list:<slug>" }) or GET /api/cards?search=list:<slug>. Compose with is:gone to audit what has sold or delisted off a list.