> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tryhoard.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Card lists

> Create, read, and manage card lists — explicit membership sets that compose with inventory search.

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:

```json theme={null}
{
  "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:

| Field                                       | Meaning                                                                                                                                          |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tcgplayer_ids`                             | Seller SKU ids — the dashboard's lingua franca. Custom (photo/Collectr) listings carry a **negative** synthetic SKU id and are addable here too. |
| `card_ids`                                  | Inventory-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                                                         |

```json theme={null}
{ "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:

```json theme={null}
{ "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.

| Binding                                | Effect | Maps to                        |
| -------------------------------------- | ------ | ------------------------------ |
| `hoard.lists.list()`                   | read   | `GET /api/lists`               |
| `hoard.lists.create(name)`             | write  | `POST /api/lists`              |
| `hoard.lists.rename(id, name)`         | write  | `PATCH /api/lists/{id}`        |
| `hoard.lists.delete(id)`               | write  | `DELETE /api/lists/{id}`       |
| `hoard.lists.addCards(id, payload)`    | write  | `POST /api/lists/{id}/cards`   |
| `hoard.lists.removeCards(id, payload)` | write  | `DELETE /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.

```js theme={null}
// 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.
