{"openapi":"3.1.0","info":{"title":"slab API","summary":"Trading-card catalog, live market pricing, and your personal collection — one API.","description":"\nslab is a **trading-card API**: a normalized catalog of what exists, comps-derived market prices for\nwhat it's worth, and a write surface for tracking the copies *you* own and what they cost you.\n\nThe catalog is hockey today, but nothing in the model is hockey-specific — sport, league, and team\nare ordinary values, not schema. Everything below works the same for any trading card.\n\n---\n\n## Start here\n\n```bash\ncurl -s https://api.slab.dev-jeb.com/health                 # no key needed\ncurl -s -X POST https://api.slab.dev-jeb.com/cards/search \\\n  -H \"x-api-key: $SLAB_API_KEY\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"subject\": \"McDavid\", \"rookie\": true, \"limit\": 5}'\n```\n\nGet a key by signing in to the [portal](https://app.slab.dev-jeb.com) and minting one under\n**Account → API keys**. There is no anonymous data-plane access and no local bypass mode.\n\nPrefer not to write HTTP? `pip install slab-cli` gives you the same surface as a terminal client,\nand `slab-schemas` (pydantic, no server dependencies) gives you typed request/response models.\n\n---\n\n## Authentication\n\nTwo planes, two credentials. Nearly everything you care about is the **data plane**.\n\n| Plane | Credential | Header | Used by |\n| --- | --- | --- | --- |\n| **Data** | API key | `x-api-key: <key>` | CLI, scripts, third-party apps |\n| **Management** | Clerk session JWT | `Authorization: Bearer <jwt>` | the web portal only |\n| **Public** | none | — | `/health`, `/stats`, `/community`, `/glossary`, `/vocab`, `/custom-sets/popular` |\n\n**Data plane.** The key is hashed and resolved to the **account** that owns it — the tenant. A\nmissing, unknown, revoked, or disabled key is a `401`. Keys are shown exactly once, at creation;\nstore them like passwords and mint one per machine so a single revoke doesn't lock you out\neverywhere.\n\n**Management plane** (`/me`, `/account`, `/account/keys`, `/webhooks/clerk`) exists so the portal can\nmint and revoke keys. It authenticates humans, not programs — an API key will not open it, and you\nshould not build against it.\n\n### Accounts vs collectors\n\nAn **account** is the billing/auth tenant. A **collector** is a person whose cards are being\ntracked. A solo user is one account with one collector; a developer account can hold many collectors\n(one per end user of their app).\n\nBecause a key can span several collectors, write routes name the acting collector *separately* from\nthe key — in the path (`/collectors/{collector_uuid}/...`) or, on custom-set routes, as a\n`collector_uuid` field. That collector is checked against your account on every call.\n\n> **A collector you don't own returns `404`, not `403`.** This is deliberate. If non-ownership\n> answered differently from non-existence, the API would confirm which collector ids are real. Both\n> answer identically, so it can't be used to enumerate anyone.\n\nDon't have the collector id? `GET /account` returns the collectors behind the calling key plus a\ndefault — which is how the CLI works with only `SLAB_API_KEY` set.\n\n---\n\n## Identifiers\n\n**Every id on the wire is a UUID string.** Integer primary keys exist in the database and are never\nexposed. Path parameters are spelled `{card_uuid}`, `{copy_uuid}`, `{set_uuid}` — never `{card_id}` —\nso the shape is unambiguous at a glance. Reference fields are `*_uuid`; an object's own id is `uuid`.\n\nTreat them as opaque. Sets additionally carry a stable human-readable `slug`\n(`2025-26-upper-deck-series-1`) that is convenient in URLs and log lines; it is a filter value, not\na path id.\n\n---\n\n## Search is a POST, and the body is JSON\n<!-- Headings stay plain text: ReDoc builds its sidebar from them and shows any inline markup as\n     escaped HTML there (a backticked `POST` renders as literal <code>POST</code> in the nav). -->\n\n\nEvery search endpoint is a `POST` with a JSON body — `/cards/search`, `/sets/search`,\n`/custom-sets/search`, `/collectors/{collector_uuid}/collection/search`, and the collection's\nbreaks/lots searches.\n\n`POST` for a read looks odd until you see the filter grammar: it nests, it has list-valued fields\nwith any/all/none semantics, and it grows. Query strings degrade badly under all three (escaping,\nrepeated keys, length caps, no types). A JSON body stays typed and readable, and adding a filter\nnever breaks an existing caller.\n\n### Filter semantics — the rule for every list field\n\n- **Within one field, values are OR'd.** `{\"brand\": [\"Upper Deck\", \"O-Pee-Chee\"]}` → either brand.\n- **Across fields, conditions are AND'd.** Adding `{\"year\": 2025}` narrows the above.\n- **`*_all` requires every value.** `{\"attribute_all\": [\"Rookie\", \"Autograph\"]}` → rookie autos only.\n- **`*_not` excludes.** `{\"attribute_not\": [\"Memorabilia\"]}` → drop anything with a relic.\n- List fields also accept a comma-separated string (`\"Upper Deck,O-Pee-Chee\"`), which is what makes\n  the CLI's flags thin.\n\nThe catalog grammar (`CardFilter`) is **shared**: catalog search and collection search take the same\ncard filters, so a query you tuned against the catalog runs unchanged against your own cards, where\nit simply gains collection-only filters (status, grading company, cost) on top.\n\n### Paging and facets\n\nResponses carry `total`, `limit`, `offset`, and `items`. `total` is the full match count *ignoring*\npaging — page with `offset`, size with `limit` (max 200), and stop when `offset + len(items) >= total`.\n\nCard search also takes `facets` — ask for `[\"brand\", \"year\", \"subset\"]` and the response includes\nvalue/count pairs for each dimension **within the current filters**. That's a drill-down sidebar in\none round trip instead of N count queries. Facets are opt-in because they cost extra aggregation.\n\n---\n\n## Errors\n\nNon-2xx responses share one shape: `{\"detail\": \"...\"}`. Branch on the **status code**; show `detail`\nto a human, but never pattern-match it — the wording is not part of the contract.\n\n| Code | Means | Typical fix |\n| --- | --- | --- |\n| `401` | Missing, unknown, revoked, or disabled API key | Send `x-api-key`; mint a new key |\n| `403` | Authenticated, but the operation isn't available to your account type | e.g. only developer accounts may create collectors |\n| `404` | No such resource — **or** it isn't yours | Check the uuid; check you're using the right key |\n| `409` | Collides with existing state | e.g. a custom-set name already taken |\n| `422` | Body failed validation | See below |\n| `5xx` | Our problem | Retry with backoff |\n\n`422` is the exception to the shape: `detail` is a **list**, one entry per offending field, each with\n`loc` (the path to the field), `msg`, and `type`. That's FastAPI's validation format, kept as-is\nbecause it pinpoints the mistake.\n\n`204 No Content` is a success — every `DELETE` returns it with an empty body.\n\n---\n\n## Prices: what the numbers mean\n\nCard prices are **estimates derived from observed sales**, not listings and not appraisals we invent.\n\n- A **comp** is one real recorded sale of a specific printing at a specific grade.\n- A **price key** is what a comp is bucketed into: *(card, finish, grade)* — a PSA 10 Gold parallel\n  and a raw base card are different keys and never mix.\n- **FMV** (fair market value) is the **trimmed median** of a key's comps over a trailing **90 days**.\n  Trimmed, so one absurd sale can't move it; median, so the typical sale wins.\n- `sample_size` is how many comps stand behind a number and `low_confidence` flags a thin one.\n  **A price with a small sample is a hint, not a valuation** — show the sample size wherever you show\n  the price.\n- Every card's evidence is open: `GET /cards/{card_uuid}/comps` returns the actual sales behind\n  its FMV.\n\nSealed boxes and cases ride the same pipeline, priced per SKU with no grade axis\n(`GET /sealed/{product_uuid}/market`).\n\n### The two-lane rule — read this before you build a chart\n\nA **price point** is a daily snapshot of a key's 90-day trailing FMV. Two snapshots a month apart\nshare most of their underlying sales, so **subtracting them measures how the appraisal drifted, not\nwhat the market did**. It is smooth, lagged, and has ~90 days of memory.\n\nPick your lane by the question you're answering:\n\n| Question | Lane | Endpoints |\n| --- | --- | --- |\n| *\"What is this worth (as of a date)?\"* | **Snapshots** | `/cards/{uuid}/price-history`, `/collectors/{uuid}/portfolio/history`, FMV anywhere, most-valuable leaderboards |\n| *\"What happened in the market this month?\"* | **Comps** | `/cards/{uuid}/comps`, sales counts, dollar volume, hottest-player trends |\n\nFields like `price_change_30d` are snapshot drift and are labeled as such. Surfacing one is fine;\ncaptioning it *\"up 12% this month\"* is not — that sentence describes sales, and this number doesn't\nmeasure sales.\n\n### Your money\n\nEverything below is computed at query time from your own cost rows. Nothing is stored, so correcting\na cost immediately corrects every derived figure.\n\n| Field | Definition |\n| --- | --- |\n| `acquisition_cost` | What the card itself cost. From a break: the break's total ÷ cards pulled from it. Otherwise what you entered. |\n| `additional_costs` | Sum of everything you added after — grading, shipping, supplies. |\n| `cost_basis` | `acquisition_cost + additional_costs` — all-in. |\n| `fair_market_value` | FMV for this copy's exact printing and grade (see above). |\n| `unrealized_gain_loss` | `fair_market_value − cost_basis` — paper P&L, while you still hold it. |\n| `realized_gain_loss` | `sale_price − cost_basis` — actual P&L, once sold. |\n| `roi` | `(fair_market_value − cost_basis) / cost_basis`. |\n| `grading_uplift` | `FMV(graded) − FMV(raw)` for the same card — what the grade is worth. |\n| `grading_roi` | `grading_uplift / grading_cost` — whether grading it paid off. |\n\nMoney is serialized as a **decimal string** (`\"1234.56\"`), not a float. Parse it into a decimal type;\nbinary floats will lose cents on you.\n\n---\n\n## Two endpoints that keep you from hardcoding\n\nBoth are public, both are cheap to cache, and both exist so your UI can never go stale.\n\n- **`GET /vocab`** — every enumerable value the API accepts or returns: statuses, acquisition types,\n  cost categories, grades, sort keys, plus the *live* catalog dimensions (attributes, grading\n  companies) that grow as sets are seeded. Populate your dropdowns from this rather than copying a\n  list into your code; a new attribute lands in your UI on its own.\n- **`GET /glossary`** — plain-language `{label, summary, detail}` for every metric the API surfaces,\n  keyed by id (`community.most_valuable_graded`). Render these as your tooltips and your wording\n  matches the portal and the CLI exactly. Data responses embed their own relevant subset inline.\n\n---\n\n## Conventions worth knowing\n\n- **Dates** are `YYYY-MM-DD`; **timestamps** are ISO-8601 UTC.\n- **`PATCH` bodies are sparse** — send only the fields you're changing. An omitted field is left\n  alone; an explicit `null` clears a nullable field.\n- **The catalog is read-only.** Sets, cards, and parallels are seeded offline. What you write is your\n  *collection* (copies, breaks, lots, costs) and your *custom sets*.\n- **A parallel is just another card.** It has its own uuid, its own print run, and its own price —\n  and it points at its base card. `GET /cards/{card_uuid}/parallels` returns the whole rainbow.\n- **No rate limit is enforced today.** Be reasonable — cache the public endpoints, page rather than\n  poll — and expect per-key limits to arrive before that changes.\n","contact":{"name":"slab","url":"https://app.slab.dev-jeb.com/docs"},"license":{"name":"Proprietary"},"version":"0.1.0"},"servers":[{"url":"https://api.slab.dev-jeb.com","description":"Production"}],"paths":{"/health":{"get":{"tags":["meta"],"summary":"Health check","description":"Liveness probe — public, unauthenticated, and cheap.\n\nA `200` means the process is serving. Use it for uptime monitoring and load-balancer checks, and\nas the first thing to curl when a client can't connect (it separates \"the API is down\" from \"my\nkey is wrong\", which otherwise look alike).","operationId":"health","responses":{"200":{"description":"Always `{\"status\": \"ok\"}` when the service is up.","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health"}}}}}}},"/glossary":{"get":{"tags":["meta"],"summary":"Get plain-language text for every metric","description":"What every number the API surfaces actually *means*, in plain language — keyed by namespaced\nmetric id (`community.most_valuable_graded`), each entry a `label` (what to call it), a `summary`\n(one line), and a `detail` (the caveats).\n\n**Render these instead of writing your own captions.** There is exactly one source of truth for\nthis text, and the portal, the CLI, and this endpoint all read it — so a metric is described\nidentically everywhere, and a wording fix propagates without anyone editing a UI. Wire them to\nyour tooltips and they stay correct on their own.\n\nIt matters most for the numbers that are easy to misread: which FMVs are thinly sampled, and why\na price-history line tracks an *appraisal* rather than the month's sales.\n\nPublic, static, and safe to cache hard. Data responses also embed the subset relevant to\nthemselves (see `glossary` on `GET /community`), so you often don't need this call at all.","operationId":"glossary","responses":{"200":{"description":"Metric id → `{label, summary, detail}`.","content":{"application/json":{"schema":{"additionalProperties":{"$ref":"#/components/schemas/MetricInfo"},"type":"object","title":"Response Glossary"}}}}}}},"/vocab":{"get":{"tags":["meta"],"summary":"Get every enumerable value the API accepts","description":"Every value list in the API, served rather than documented: statuses, acquisition types, cost\ncategories, grades, sealed formats, the sort grammars — plus the **live** catalog dimensions\n(attributes, grading companies) that grow as new sets are seeded.\n\n**Populate your dropdowns and validators from this, not from a copy in your code.** A hardcoded\nlist is correct exactly until the catalog gains an attribute, and then it's silently wrong with\nnothing to warn you. Read it at startup, cache it for a day, and new values appear in your UI on\ntheir own.\n\nThat's why the split matters: the enum halves change only with an API release, while\n`attributes` and `grading_companies` change whenever data is ingested — which is precisely the\nhalf a static list gets wrong.\n\nPublic and cacheable, same as `/glossary` (which explains the *metrics*, where this enumerates\nthe *values*).","operationId":"vocab","responses":{"200":{"description":"Wire enums, sort keys, and the live catalog dimensions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VocabOut"}}}}}}},"/stats":{"get":{"tags":["stats"],"summary":"Get catalog totals","description":"How much is in the catalog: sets, cards, printings, players, and the breakdown by brand and\nseason.\n\n**Public — no API key.** Good for a landing-page counter, a coverage check after a seed run, or\na sanity check that you're pointed at the environment you think you are.\n\nCatalog-wide only: nothing here reflects anyone's collection. If you're rendering a community\npage, call `GET /community` instead — it embeds this exact block alongside the leaderboards, so\ncalling both is a wasted round trip.","operationId":"catalog_stats","responses":{"200":{"description":"Counts for the catalog as a whole.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogStats"}}}}}}},"/community":{"get":{"tags":["community"],"summary":"Get the whole community board","description":"The entire public community picture in **one call**. Deliberately a single fat payload rather\nthan six endpoints — an API-only client should be able to render the whole board without a\nfollow-up request.\n\nYou get:\n\n- **`stats`** — catalog totals, identical to `GET /stats`.\n- **`ticker`** — pre-composed scrolling lines: new sets in the catalog, the week's biggest public\n  sale, and recent collection activity.\n- **Leaderboards** — most valuable split into `most_valuable_raw` and `most_valuable_graded`\n  (mixing them would just rank slabs above everything), most-collected cards and players by\n  distinct-collector count, and rarest-owned by print run.\n- **`popular_sets`** — same as `GET /custom-sets/popular`.\n- **`glossary`** — `{label, summary, detail}` for every `community.*` metric here. Render these\n  as tooltips and your wording matches the portal and CLI exactly, for free.\n\n**Public — no API key**, and everything is aggregate and anonymized. No collector identity and no\npersonal purchase price appears anywhere: a ticker line says *a card was added*, never by whom or\nfor what. That's a hard property of the endpoint, not a display convention — collector-level data\nlives behind the collection routes and requires that collector's own key.\n\n`limit` sizes each leaderboard (not the payload as a whole).","operationId":"community_board","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Entries per leaderboard","default":10,"title":"Limit"},"description":"Entries per leaderboard"}],"responses":{"200":{"description":"Catalog stats, activity ticker, leaderboards, popular sets, and the glossary text for each metric — everything a community page needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommunityBoard"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/custom-sets/popular":{"get":{"tags":["custom-sets"],"summary":"List popular public sets","description":"The most-subscribed public chase sets — what the community is collectively chasing.\n\n**Public — no API key**, the one route in this group that needs none, so a landing page can show\nit to signed-out visitors. The same list is embedded in `GET /community` as `popular_sets`, so\ndon't call both when you're rendering that board.","operationId":"popular_custom_sets","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Top public custom sets, most-subscribed first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomSetOut"},"title":"Response Popular Custom Sets"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me":{"get":{"tags":["account"],"summary":"Get the signed-in user's account (portal)","description":"Bootstrap view for a signed-in portal user: their account, its collectors, and the default.\n\n**Management plane — a Clerk session JWT, not an API key.** This is the portal's first call after\nsign-in. Building a client? Use **`GET /account`**, which returns the same shape for the key\nyou're holding.\n\nProvisions the account (and its first collector) on first sight of a Clerk identity, so a brand-\nnew user is usable immediately rather than after a webhook lands.","operationId":"me","responses":{"200":{"description":"The account, its collectors, and which one is the default.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"detail":"invalid or revoked API key"}}}}},"security":[{"ClerkJWT":[]}]}},"/me/dashboard":{"get":{"tags":["account"],"summary":"Get the signed-in user's dashboard (portal)","description":"Collection dashboard for the signed-in portal user — identical payload to\n`GET /collectors/{collector_uuid}/dashboard`, reached with a Clerk token instead of an API key.\n\nIt exists so the portal never has to mint or hold a key just to render the user's own dashboard;\nthe management plane stays on the credential the browser already has.\n\nDefaults to the account's default collector. Pass `?collector=<uuid>` for a specific one — it\nmust belong to the account, or `404`.\n\n**Not a programmable endpoint.** Clients should call the data-plane route above.","operationId":"me_dashboard","security":[{"ClerkJWT":[]}],"parameters":[{"name":"collector","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Collector"}}],"responses":{"200":{"description":"The same dashboard as the data-plane route, for a Clerk-authenticated user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardStats"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/portfolio/history":{"get":{"tags":["account"],"summary":"Get the signed-in user's portfolio history (portal)","description":"Portfolio value over time for the signed-in portal user — the Clerk-token twin of\n`GET /collectors/{collector_uuid}/portfolio/history`, same payload and same snapshot-lane caveat.\n\nDefaults to the account's default collector; `?collector=<uuid>` selects another one it owns.\n\n**Not a programmable endpoint** — clients should call the data-plane route.","operationId":"me_portfolio_history","security":[{"ClerkJWT":[]}],"parameters":[{"name":"collector","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Collector"}},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Start date (YYYY-MM-DD)","title":"Start"},"description":"Start date (YYYY-MM-DD)"},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"End date (YYYY-MM-DD); defaults to today","title":"End"},"description":"End date (YYYY-MM-DD); defaults to today"},{"name":"interval","in":"query","required":false,"schema":{"type":"string","description":"daily, weekly, or monthly","default":"daily","title":"Interval"},"description":"daily, weekly, or monthly"}],"responses":{"200":{"description":"The same series as the data-plane route, for a Clerk-authenticated user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioHistory"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account":{"get":{"tags":["account"],"summary":"Get the account behind your API key","description":"**Who am I?** — the account behind the API key you just sent, its collectors, and which one is\nthe default.\n\nThe one route in this group built for programs rather than the portal, and usually a client's\n**first call**: nearly every collection route needs a `collector_uuid`, and this is where you get\none without making the user hunt for a uuid. It's how the CLI works with only `SLAB_API_KEY`\nset — and how you'd verify a key is live before relying on it.\n\nCheap and stable; resolve it once at startup rather than per request.","operationId":"account_context","responses":{"200":{"description":"The account, its collectors, and which one is the default.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"detail":"invalid or revoked API key"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/account/keys":{"get":{"tags":["account"],"summary":"List API keys (portal)","description":"The account's API keys as **metadata only** — name, prefix, created date, last used, revoked\nstate. Raw values are unrecoverable by design and never appear here.\n\n`last_used_dt` is the useful column: it's how you find the key nothing has touched in months,\nwhich is the one to revoke.","operationId":"list_keys","responses":{"200":{"description":"Key metadata — never the key values themselves.","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ApiKeyOut"},"type":"array","title":"Response List Keys"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"detail":"invalid or revoked API key"}}}}},"security":[{"ClerkJWT":[]}]},"post":{"tags":["account"],"summary":"Mint an API key (portal)","description":"Create a data-plane API key for the signed-in account.\n\n> **The raw key appears in this response and nowhere else, ever.** Only a hash is stored, so we\n> cannot show it again or recover it — if it's lost, revoke it and mint another.\n\nGive each key a `name` that says where it runs (\"laptop\", \"prod worker\"). One key per machine\nmeans a leak is one revoke, not a rotation across everything you own.\n\nManagement plane — the portal mints keys; keys can't mint keys.","operationId":"create_key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreate"}}},"required":true},"responses":{"201":{"description":"The new key — **including its raw value, shown this one time only**.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"detail":"invalid or revoked API key"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ClerkJWT":[]}]}},"/account/keys/{key_uuid}":{"delete":{"tags":["account"],"summary":"Revoke an API key (portal)","description":"Revoke a key. **Immediate and irreversible** — the next request carrying it gets a `401`, and\nit cannot be un-revoked. Mint a replacement first if something is running on it.\n\nIdentify the key by its `uuid` from the list route, never by its value (we don't have the value).","operationId":"revoke_key","security":[{"ClerkJWT":[]}],"parameters":[{"name":"key_uuid","in":"path","required":true,"schema":{"type":"string","title":"Key Uuid"}}],"responses":{"204":{"description":"Revoked. No content."},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cards/search":{"post":{"tags":["cards"],"summary":"Search the catalog","description":"Find cards anywhere in the catalog. This is the main entry point — most workflows start here\nand then follow a `uuid` into the per-card endpoints below.\n\n**Filters** are the shared card grammar (`CardFilter`): player, team, brand, set, season/year\nrange, subset, attributes, finish, and scarcity. Within a list field values are OR'd; across\nfields they're AND'd; `*_all` demands every value and `*_not` excludes. The same grammar drives\ncollection search, so a query you tune here runs unchanged against your own cards.\n\n```json\n{\"subject\": \"Bedard\", \"rookie\": true, \"is_numbered\": true, \"sort\": \"numbered\", \"limit\": 20}\n```\n\n**Parallels are ordinary results.** A search returns base cards and parallels alike, each its own\nrow with its own print run. Narrow with `base_only` or `parallel_only`, or filter `finish` by\nname.\n\n**`facets`** asks for value/count pairs per dimension (`brand`, `year`, `subset`, `finish`,\n`team`, `attribute`), computed *within the current filters* — a drill-down sidebar in one round\ntrip. Opt-in, because it costs extra aggregation.\n\n**`collector`** annotates every row with `owned_quantity`, turning a catalog listing into a\nhave/need view; add `owned: false` for exactly the cards that collector is missing.\n\n**`include_market`** adds a headline FMV to each row via one batched lookup over the page. Off by\ndefault — leave it off when you're just listing a checklist.\n\nPage with `limit` (max 200) and `offset`; `total` is the full match count, ignoring paging.","operationId":"search_cards","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardSearchQuery"}}},"required":true},"responses":{"200":{"description":"A page of matching cards, the full match count, and any requested facets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardSearchResult"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"detail":"invalid or revoked API key"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/cards/{card_uuid}/parallels":{"get":{"tags":["cards"],"summary":"List a card's full rainbow","description":"Every printing of the same slot — the base card and all of its parallels (\"the rainbow\").\n\nA parallel in slab is not an attribute of a card, it *is* a card: same player and card number in\na different finish, with its own uuid, print run, odds, and price. That's why chasing a rainbow\nis one call and each entry can be priced independently.\n\nWorks from any member of the slot — pass a parallel's uuid and you get the same list, base\nincluded. Ordering puts the base first, then parallels.","operationId":"get_parallels","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"card_uuid","in":"path","required":true,"schema":{"type":"string","title":"Card Uuid"}}],"responses":{"200":{"description":"Every printing of the slot — the base card plus each parallel.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CardOut"},"title":"Response Get Parallels"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cards/{card_uuid}/market":{"get":{"tags":["cards"],"summary":"Get a card's market value","description":"What this exact printing is worth right now, broken out by grade.\n\nEach entry is a **price key** — this card, this finish, one grade bucket (`RAW`, `PSA-10`, …) —\nvalued at the **trimmed median of its sales over the trailing 90 days**. Trimmed so a single\noutlier can't move it; median so the typical sale wins. Grades never blend: a PSA 10 and a raw\ncopy are separate numbers because they're separate markets.\n\n**Read `sample_size` before you trust a price.** Two sales and twenty sales produce the same\nfield but not the same confidence, which is what `low_confidence` flags. A card with no recent\nsales has no value here at all, and that's the honest answer — it is not worth zero, it is\nunpriced.\n\nWant the evidence? `GET /cards/{card_uuid}/comps` returns the individual sales behind these\nnumbers. Want the trend? `/price-history`, keeping the two-lane rule in mind.","operationId":"get_card_market","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"card_uuid","in":"path","required":true,"schema":{"type":"string","title":"Card Uuid"}}],"responses":{"200":{"description":"Fair market value per grade bucket, with sample size and confidence.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardMarket"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cards/{card_uuid}/comps":{"get":{"tags":["cards"],"summary":"List the sales behind a card's value","description":"The actual recorded sales this card's value is computed from — price, date, grade, and the\noriginal listing title.\n\nUse it to **audit an FMV**. If a number looks wrong, the sales are right here: you can see\nwhether it rests on one lucky auction, whether the grade mix is skewed, or whether a\nsimilarly-titled card slipped in.\n\nThis is also the right endpoint for **\"what happened in the market recently?\"** Every sale falls\nin exactly one window, so counting and summing comps measures real activity — unlike differencing\nprice snapshots, which measures how a 90-day average drifted. (See the two-lane rule in the API\noverview.)\n\n`include_unmatched=true` adds sales our matcher couldn't confidently pin to a specific printing.\nThey're excluded from FMV by design; include them only when you're investigating coverage gaps,\nnever when you're pricing something.","operationId":"get_card_comps","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"card_uuid","in":"path","required":true,"schema":{"type":"string","title":"Card Uuid"}},{"name":"grade_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Only this grade bucket, e.g. \"RAW\", \"PSA-10\"","title":"Grade Key"},"description":"Only this grade bucket, e.g. \"RAW\", \"PSA-10\""},{"name":"include_unmatched","in":"query","required":false,"schema":{"type":"boolean","description":"Also include ambiguous/unmatched sales (default: matched only)","default":false,"title":"Include Unmatched"},"description":"Also include ambiguous/unmatched sales (default: matched only)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Max comps to return","default":50,"title":"Limit"},"description":"Max comps to return"}],"responses":{"200":{"description":"Recent observed sales resolved to this card, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardComps"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cards/{card_uuid}/price-history":{"get":{"tags":["cards"],"summary":"Get a card's price history","description":"The value series behind a price chart: one point per day (or week/month) for a single\ngrade/finish combination.\n\nEach point is that day's **90-day trailing appraisal**, not that day's sale. Which leads to the\none thing worth internalizing before you plot it:\n\n> **This line is smooth and lagged on purpose.** Consecutive points share nearly all of their\n> underlying sales, so the curve shows how the *appraisal* moved, with ~90 days of memory. It is\n> a legitimate answer to *\"what was this worth back then?\"* and a misleading answer to *\"how much\n> did the market move last month?\"* — for that, count comps in the window instead.\n\nLabel it accordingly (\"estimated value\", not \"market change\") and it will never mislead a reader.\n\nOne series per call: pick the `grade_key` and, for a parallel, the `finish` (omit for the base\nprinting). Coarser `interval` values give you fewer, cleaner points for long ranges.","operationId":"get_card_price_history","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"card_uuid","in":"path","required":true,"schema":{"type":"string","title":"Card Uuid"}},{"name":"grade_key","in":"query","required":false,"schema":{"type":"string","description":"Grade bucket: \"RAW\", \"PSA-10\", etc.","default":"RAW","title":"Grade Key"},"description":"Grade bucket: \"RAW\", \"PSA-10\", etc."},{"name":"finish","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Parallel finish name; omit for base","title":"Finish"},"description":"Parallel finish name; omit for base"},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Start date (YYYY-MM-DD)","title":"Start"},"description":"Start date (YYYY-MM-DD)"},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"End date (YYYY-MM-DD)","title":"End"},"description":"End date (YYYY-MM-DD)"},{"name":"interval","in":"query","required":false,"schema":{"type":"string","description":"daily, weekly, or monthly","default":"daily","title":"Interval"},"description":"daily, weekly, or monthly"}],"responses":{"200":{"description":"A dated series of appraisal snapshots for one grade/finish.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardPriceHistory"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sets/search":{"post":{"tags":["sets"],"summary":"Search sets (products)","description":"Browse releases — a *set* is one purchasable product (2025-26 Upper Deck Series 1), the thing\na box contains.\n\nUsually step one of two: find the set, take its `slug`, then run `POST /cards/search` with\n`set_slug` to work through its checklist. From a set you can also reach its sealed SKUs and its\ntop cards (below).\n\nFilter by name substring (`q`), `brand`, `year`, or `sport`. Each result carries more than\nidentity:\n\n- `card_count` — printings in the set, base plus parallels.\n- `priced_count` and `sales_90d` — how much of the set actually trades. A large set with few\n  recent sales is thinly covered, and its individual card values will be sparse.\n- `box_price` — latest hobby-box market value, or `null` when the set's sealed products aren't\n  catalogued or priced yet.","operationId":"search_sets","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetSearchQuery"}}},"required":true},"responses":{"200":{"description":"A page of matching sets with size and market-activity counts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetSearchResult"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"detail":"invalid or revoked API key"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/sets/{set_uuid}/sealed":{"get":{"tags":["sets"],"summary":"List a set's sealed products","description":"The unopened formats this set was sold in — hobby box, hobby case, blaster, tin, and so on —\neach with its pack configuration and current market value.\n\nOne row per (set, format): hobby and retail of the same release are genuinely different products\nwith different contents, odds, and prices, so they are never merged.\n\nAn **empty list doesn't mean the product never existed** — sealed SKUs are created only when a\nset's overview data has been ingested. A checklist-only set simply has no sealed catalog yet.\n\nFrom here, `GET /sealed/{product_uuid}/market` gives the full pricing view for one SKU.","operationId":"get_set_sealed","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}}],"responses":{"200":{"description":"Every sealed SKU of the set, each with its latest market value.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SealedProductOut"},"title":"Response Get Set Sealed"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sets/{set_uuid}/top-cards":{"get":{"tags":["sets"],"summary":"List a set's most valuable cards","description":"The chase cards — this set's most expensive printings by headline fair market value.\n\nThis is what makes a box worth opening, so it pairs naturally with `/sealed`: the sealed price is\nwhat you pay, these are what you're paying *for*.\n\nEach card is valued at its RAW price where one exists, otherwise its best-sampled grade, so the\nlines are comparable without you having to pick a grade first. Only printings with recent sales\ncan appear — an unpriced card is absent rather than ranked at zero.","operationId":"get_set_top_cards","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"How many cards to return","default":10,"title":"Limit"},"description":"How many cards to return"}],"responses":{"200":{"description":"The set's highest-FMV printings, most expensive first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetTopCards"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sealed/{product_uuid}/market":{"get":{"tags":["sealed"],"summary":"Get a sealed product's market value","description":"*What is this box worth?* — current market value for one sealed SKU, with the sales it's\nderived from included in the same response.\n\nSame pipeline as cards (trimmed median of trailing-90-day sales) with **one axis instead of\nthree**: a sealed box has no grade and no finish, so there's a single number rather than a table.\n\n**Only factory-sealed sales count.** Break spots, empty boxes, and mixed lots are filtered out\nduring matching — they're far more numerous than real box sales and would drag the number toward\nthe price of a slot rather than the price of a box.\n\nPair this with `GET /sets/{set_uuid}/top-cards` for the two halves of a rip decision: what the\nbox costs, and what's inside worth chasing. Get `product_uuid` from\n`GET /sets/{set_uuid}/sealed`.","operationId":"get_sealed_market","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"product_uuid","in":"path","required":true,"schema":{"type":"string","title":"Product Uuid"}},{"name":"comps_limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Max comps to return","default":25,"title":"Comps Limit"},"description":"Max comps to return"}],"responses":{"200":{"description":"Current value for the SKU plus the recent sales behind it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SealedMarket"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sealed/{product_uuid}/price-history":{"get":{"tags":["sealed"],"summary":"Get a sealed product's price history","description":"How a sealed box or case has been valued over time — the series behind a price chart.\n\nSealed prices have a life cycle worth seeing: pre-release, release-day supply, the long post-rip\ndecline as boxes are opened, and sometimes a late climb as unopened product gets scarce.\n\nSame caveat as card history — **each point is a 90-day trailing appraisal, not that day's sale**.\nNeighbouring points share most of their underlying sales, so the curve answers *\"what was a box\nworth then?\"*, not *\"how much did boxes move last month?\"* For the latter, count sales in the\nwindow via the comps on `/market`.","operationId":"get_sealed_price_history","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"product_uuid","in":"path","required":true,"schema":{"type":"string","title":"Product Uuid"}},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Start date (YYYY-MM-DD)","title":"Start"},"description":"Start date (YYYY-MM-DD)"},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"End date (YYYY-MM-DD)","title":"End"},"description":"End date (YYYY-MM-DD)"},{"name":"interval","in":"query","required":false,"schema":{"type":"string","description":"daily, weekly, or monthly","default":"daily","title":"Interval"},"description":"daily, weekly, or monthly"}],"responses":{"200":{"description":"A dated series of appraisal snapshots for the SKU.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SealedPriceHistory"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors":{"post":{"tags":["collection"],"summary":"Create a collector","description":"Provision a new collector under the calling account.\n\n**This is a developer-account capability.** A developer building on slab creates one collector\nper end user of their app, so each user's cards stay separate under a single key.\n\nA **`collector` account cannot use this and gets a `403`** — not an oversight. That account type\nreceives its one collector at portal sign-up, and collector identity is born there so it stays\ntied to a real person rather than being mintable in bulk. If you're a solo user looking for your\ncollector's uuid, it's in `GET /account`.","operationId":"create_collector","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectorCreate"}}},"required":true},"responses":{"201":{"description":"The new collector, including the `uuid` every other route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectorOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"detail":"invalid or revoked API key"}}}},"403":{"description":"Authenticated, but this operation isn't available to your account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"detail":"creating collectors is a developer-account capability"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/collectors/{collector_uuid}":{"patch":{"tags":["collection"],"summary":"Rename a collector","description":"Change a collector's display name.\n\nCosmetic only — the `uuid` is the identity and never changes, so renaming breaks no references\nand invalidates no stored ids.","operationId":"rename_collector","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectorUpdate"}}}},"responses":{"200":{"description":"The updated collector.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectorOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/dashboard":{"get":{"tags":["collection"],"summary":"Get the collection dashboard","description":"The whole collection summarized in one call — the \"how am I doing?\" view.\n\nHeadline money (copies owned, total cost basis, total market value, unrealized gain/loss),\nbreakdowns by status/brand/grading company, standout cards, and recent break and lot activity.\nBuilt as one payload so a dashboard renders without fanning out.\n\n**Values are computed live from your cost rows and current FMVs.** Nothing is cached, so fixing a\nwrong purchase price corrects every total here immediately — and figures shift on their own as\nthe market moves, even on a day you add nothing.\n\nCards without recent sales contribute cost basis but no market value, so a collection of thinly\ntraded cards will show a total that looks pessimistic. That's under-coverage, not loss.","operationId":"dashboard","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"responses":{"200":{"description":"Headline totals, breakdowns, highlights, and recent activity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardStats"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/sources":{"get":{"tags":["collection"],"summary":"List acquisition sources used","description":"Every acquisition source this collector has typed before, across breaks and copies —\n\"eBay\", \"LCS\", \"Dave's Cards\", whatever they use.\n\nA small convenience with a real payoff: offer these as suggestions and the same shop gets spelled\nthe same way every time, which keeps source-based filtering and grouping meaningful. Free-text\nfields fragment fast otherwise. One call, so a picker can populate without scanning the\ncollection.","operationId":"list_sources","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"responses":{"200":{"description":"Distinct source names, for reuse in a picker.","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"},"title":"Response List Sources"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/portfolio/history":{"get":{"tags":["collection"],"summary":"Get portfolio value over time","description":"What the collection has been worth over time — two lines per point: **cost basis** (what you\nput in) and **market value** (what it was worth then). The gap between them is unrealized P&L,\nand it's the chart most collectors actually want.\n\nEvery point is **as-of its date**: it values the copies you owned *then*, priced at that date's\nappraisal. A card counts from its acquisition date onward and not a day earlier — cards carry\nprice history that predates your purchase, so counting today's holdings across all of history\nwould have last month's value rise every time you buy something. Cost basis moves with it, so\nthe gap between the two lines is the paper P&L you actually had on that day.\n\nA consequence worth knowing before you difference two points: the line steps up when you **buy**\nas well as when the market moves. For pure price movement on a fixed set of cards, use the\ndashboard's `portfolio_change_7d` (same holdings on both ends) or the comps endpoints.\n\nThe date range is a viewport, not a filter: a card whose last sale predates the range still\ncounts at its most recent appraisal (carried forward, flat until a new sale updates it). A\nlow-volume card is priced at its last known value, never treated as worthless — only a card\nwith no recorded sale ever is excluded.\n\n**Snapshot lane, so the same caveat applies:** each price is a 90-day trailing appraisal. This\nline is smooth and lagged by design and answers *\"what was it worth then?\"*. It is not a\nmeasurement of monthly market moves; label it \"estimated value\" and it will never mislead.\n\nCoarser `interval` values give fewer, cleaner points over long ranges.","operationId":"portfolio_history","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Start date (YYYY-MM-DD)","title":"Start"},"description":"Start date (YYYY-MM-DD)"},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"End date (YYYY-MM-DD); defaults to today","title":"End"},"description":"End date (YYYY-MM-DD); defaults to today"},{"name":"interval","in":"query","required":false,"schema":{"type":"string","description":"daily, weekly, or monthly","default":"daily","title":"Interval"},"description":"daily, weekly, or monthly"}],"responses":{"200":{"description":"A dated series of cost basis vs market value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioHistory"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/breaks":{"post":{"tags":["collection"],"summary":"Record a break","description":"Record opening sealed product — a box, a case, or a paid slot in someone else's break.\n\nA break exists to **solve the cost problem**. You spent $250 on one box and pulled forty cards;\nnone of them has a purchase price of its own. So record the break with its `total_cost`, attach\nthe copies you pulled, and each one's `acquisition_cost` becomes the break's total divided by the\ncopies from it.\n\nThat division is **live**, which has a consequence worth knowing: attaching a forty-first card\nreprices the other forty. Under-recording your pulls makes the ones you did record look more\nexpensive than they were, so log the commons too — even a bulk row keeps the denominator honest.\n\nAdd the copies with `POST /collectors/{collector_uuid}/copies`, passing this `break_uuid`.","operationId":"create_break","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakCreate"}}}},"responses":{"201":{"description":"The new break, whose `uuid` you attach copies to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/breaks/search":{"post":{"tags":["collection"],"summary":"Search breaks","description":"Find this collector's breaks — filter by set, break type, or date range, and page the results.\n\nThe rip-EV question lives here: each break carries what it cost and what came out of it, so\ncomparing the two across breaks tells you which products have actually paid off for you rather\nthan which ones felt good.","operationId":"search_breaks","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakSearchQuery"}}}},"responses":{"200":{"description":"A page of matching breaks with their costs and pull counts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakSearchResult"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/breaks/{break_uuid}":{"get":{"tags":["collection"],"summary":"Get a break","description":"One break in full: what it cost, what came out, and how that division currently stands.\n\nThe per-card cost you see here is derived at read time from the copies attached *right now* — so\nit moves when you attach or remove one, and always reflects the current state rather than a\nfigure frozen at creation.","operationId":"get_break","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"break_uuid","in":"path","required":true,"schema":{"type":"string","title":"Break Uuid"}}],"responses":{"200":{"description":"The break, its cost, and what came out of it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["collection"],"summary":"Update a break","description":"Correct a break's details. Sparse — send only the fields you're changing.\n\n**Editing `total_cost` reprices every copy attached to it**, and therefore their cost basis, ROI,\nand the collection totals that include them. That's the point (fix the receipt once, not forty\ntimes), but it does mean a typo here moves a lot of numbers.","operationId":"update_break","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"break_uuid","in":"path","required":true,"schema":{"type":"string","title":"Break Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakUpdate"}}}},"responses":{"200":{"description":"The updated break.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["collection"],"summary":"Delete a break","description":"Remove a break.\n\n**The copies pulled from it survive** — they're your cards, and deleting the paperwork shouldn't\ndelete your collection. What they lose is the cost the break was supplying, so they fall back to\nwhatever acquisition cost they carry themselves (often none). Expect cost basis and ROI across\nthose cards to change.","operationId":"delete_break","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"break_uuid","in":"path","required":true,"schema":{"type":"string","title":"Break Uuid"}}],"responses":{"204":{"description":"Deleted. No content."},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/lots":{"post":{"tags":["collection"],"summary":"Record a lot purchase","description":"Record buying several cards for **one price** — a bulk eBay listing, a collection off a table,\na trade-in pile.\n\nSame cost-splitting job as a break, different origin: a break is sealed product you opened, a lot\nis singles someone else already opened. Both answer \"one receipt, many cards\", and both divide\ntheir total across the copies attached to them.\n\nKeep them distinct even though the math matches — break EV analysis is only meaningful over\nactual rips, and folding lot purchases in would quietly corrupt it.","operationId":"create_lot","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LotCreate"}}}},"responses":{"201":{"description":"The new lot, whose `uuid` you attach copies to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LotOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/lots/search":{"post":{"tags":["collection"],"summary":"Search lots","description":"Find this collector's lot purchases — filter and page the same way as breaks.\n\nUseful for the \"did buying in bulk work?\" question: a lot's total against the current value of\nthe cards that came out of it is the whole answer.","operationId":"search_lots","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LotSearchQuery"}}}},"responses":{"200":{"description":"A page of matching lots with their costs and card counts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LotSearchResult"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/lots/{lot_uuid}":{"get":{"tags":["collection"],"summary":"Get a lot","description":"One lot in full: what it cost and which copies came from it, with the per-card split derived\nfrom the copies attached right now.","operationId":"get_lot","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"lot_uuid","in":"path","required":true,"schema":{"type":"string","title":"Lot Uuid"}}],"responses":{"200":{"description":"The lot, its cost, and the cards attached to it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LotOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["collection"],"summary":"Update a lot","description":"Correct a lot's details. Sparse — send only what's changing.\n\nAs with a break, **editing the total reprices every copy attached to it**, and everything derived\nfrom those copies moves with it.","operationId":"update_lot","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"lot_uuid","in":"path","required":true,"schema":{"type":"string","title":"Lot Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LotUpdate"}}}},"responses":{"200":{"description":"The updated lot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LotOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["collection"],"summary":"Delete a lot","description":"Remove a lot. **The copies from it survive** and simply lose the cost it was supplying — same\ntrade-off as deleting a break.","operationId":"delete_lot","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"lot_uuid","in":"path","required":true,"schema":{"type":"string","title":"Lot Uuid"}}],"responses":{"204":{"description":"Deleted. No content."},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/copies":{"post":{"tags":["collection"],"summary":"Add a card to the collection","description":"Add one physical card you own. **The central write of the API** — everything financial\ndownstream is built from these rows.\n\nA copy points at a catalog card by `card_uuid` and carries what the catalog can't know: which\nserial you got (`serial_number` — the \"54\" of /100), its grade, what it cost you, where it lives,\nand its status.\n\n**Point at the exact printing.** A parallel is its own catalog card with its own uuid, so a Gold\n/10 is a different `card_uuid` than the base — not the base plus a note. Get it wrong and the\ncopy is priced against the wrong market entirely. `GET /cards/{card_uuid}/parallels` lists the\nwhole slot so you can pick the right one.\n\n**Where cost comes from**, in priority order:\n\n- `break_uuid` or `lot_uuid` → the copy's share of that purchase, computed live. An\n  `acquisition_cost` sent alongside one of these is ignored, not added to it.\n- otherwise `acquisition_cost` → what you paid for this single card.\n- a pull or a gift can legitimately have neither. Cost basis is then zero and ROI is\n  undefined, which is correct — free cards have no return, they just have value.\n\n**Grades:** `self_grade` is your own honest assessment and the professional grade\n(`grading_company` + `grade` + `cert_number`) is the slab's. Both can coexist, and the response's\n`grade_delta` shows how close your eye was — genuinely useful before you spend money submitting.\n\n**`quantity`** collapses identical fungible raw duplicates into one row. Never use it for cards\nthat differ in serial, grade, or cost — those need separate rows to be priced correctly.","operationId":"add_copy","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardCopyCreate"}}}},"responses":{"201":{"description":"The new copy, with cost basis and market value already computed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardCopyOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/copies/{copy_uuid}":{"patch":{"tags":["collection"],"summary":"Update a copy","description":"Change anything about a copy you own. Sparse — send only the fields you're changing; an\nomitted field is untouched, an explicit `null` clears a nullable one.\n\nThe two transitions that matter:\n\n**Selling.** Set `status` to `sold` along with `sale_price` and `sold_date`. That flips the card\nfrom unrealized to **realized** P&L (`sale_price − cost_basis`) and takes it out of your holdings\nfor portfolio purposes. The row stays — sold cards are your track record, and deleting them would\nerase the only evidence of how you've actually done.\n\n**Getting a card back from grading.** Set `grading_company`, `grade`, and `cert_number`. The copy\nthen reprices against the graded market instead of the raw one, which is usually a large move and\nexactly what `grading_uplift` was there to predict.","operationId":"update_copy","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"copy_uuid","in":"path","required":true,"schema":{"type":"string","title":"Copy Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardCopyUpdate"}}}},"responses":{"200":{"description":"The updated copy with recomputed financials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardCopyOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["collection"],"summary":"Delete a copy","description":"Remove a copy from the collection, along with its cost rows.\n\n**Deleting is for mistakes, not for sales.** A card you sold should be updated to `status: sold`\nwith its `sale_price` — that preserves the realized gain and keeps your history intact. Deleting\nit instead makes the money vanish from every total as though the card was never owned.\n\nIf the copy came from a break or lot, removing it **reprices its siblings**: the same total now\ndivides across fewer cards, so each remaining one gets more expensive.","operationId":"delete_copy","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"copy_uuid","in":"path","required":true,"schema":{"type":"string","title":"Copy Uuid"}}],"responses":{"204":{"description":"Deleted. No content."},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/copies/{copy_uuid}/costs":{"post":{"tags":["collection"],"summary":"Add a cost to a copy","description":"Record money spent on a card **after** acquiring it — grading fees, shipping both ways,\na case, an authentication.\n\nThese are what separate a real cost basis from the sticker price. A $40 card with a $30 grading\nsubmission is a $70 card, and only the second number tells you whether grading it was worth\ndoing. Every cost added here flows straight into `cost_basis`, and from there into\n`unrealized_gain_loss`, `roi`, `grading_roi`, and the collection totals.\n\nCosts are separate rows rather than one lump so you keep the itemization — you can see *what* you\nspent on, not just how much, and `category` makes \"how much have I spent on grading this year?\"\nanswerable.","operationId":"add_cost","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"copy_uuid","in":"path","required":true,"schema":{"type":"string","title":"Copy Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardCopyCostCreate"}}}},"responses":{"201":{"description":"The new cost row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardCopyCostOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/copies/{copy_uuid}/costs/{cost_uuid}":{"patch":{"tags":["collection"],"summary":"Update a cost","description":"Correct a cost's amount, category, or note. Sparse.\n\nSince nothing financial is stored, the copy's cost basis and every figure derived from it are\nright again on the next read — no recompute step to trigger.","operationId":"update_cost","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"copy_uuid","in":"path","required":true,"schema":{"type":"string","title":"Copy Uuid"}},{"name":"cost_uuid","in":"path","required":true,"schema":{"type":"string","title":"Cost Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardCopyCostUpdate"}}}},"responses":{"200":{"description":"The updated cost row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardCopyCostOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["collection"],"summary":"Delete a cost","description":"Remove a cost row. The copy's cost basis drops by that amount, which raises its apparent ROI —\nso delete duplicates, not inconvenient receipts.","operationId":"delete_cost","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"copy_uuid","in":"path","required":true,"schema":{"type":"string","title":"Copy Uuid"}},{"name":"cost_uuid","in":"path","required":true,"schema":{"type":"string","title":"Cost Uuid"}}],"responses":{"204":{"description":"Deleted. No content."},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/collection/search":{"post":{"tags":["collection"],"summary":"Search the collection","description":"Search the cards you actually own — and get the money for the matched set, not just the rows.\n\n**The catalog grammar works here unchanged.** Every `CardFilter` field from `POST /cards/search`\n(player, team, set, year range, attributes, finish, scarcity) means the same thing, so a query\nyou tuned against the catalog runs as-is against your collection. On top of it you get the\ncollection-only dimensions: `status`, `grading_company`, `acquisition_type`, and cost/value\nfilters.\n\n**The `summary` is the reason to prefer this over paging everything.** It covers the entire match\n— total cost basis, market value, and unrealized gain/loss — so *\"what are my graded rookies\nworth?\"* is one call with `limit: 1`, not a client-side sum over every page.\n\nFacets work the same way, counting within your collection instead of the catalog: ask for\n`status` or `grading_company` to see the shape of what you own.\n\nEach row carries its own financials (cost basis, FMV, gain/loss, ROI) alongside the catalog\ndetails, so nothing needs a second lookup.","operationId":"search_collection","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionSearchQuery"}}}},"responses":{"200":{"description":"A page of owned copies, facet counts, and a financial summary of the whole matched set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionResult"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/custom-sets":{"get":{"tags":["collection"],"summary":"List a collector's custom sets","description":"This collector's chase sets — the ones they created and the public ones they subscribed to,\nprivate sets included (it's their own key asking).\n\nEach carries its completion stats, so this is the \"what am I chasing, and how close am I?\" list.\nFull contents and the per-card have/need breakdown live on `GET /custom-sets/{set_uuid}`.","operationId":"collector_custom_sets","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"responses":{"200":{"description":"Every custom set this collector created or subscribed to.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomSetOut"},"title":"Response Collector Custom Sets"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["custom-sets"],"summary":"Create a custom set","description":"Create a **chase set** — the set *you* decided to complete, when no manufacturer made it.\nEvery Bedard rookie. One autograph per team. Your personal top 100.\n\nPick the kind up front, because it decides how cards get in:\n\n- **`curated`** — you add cards one at a time (`POST .../cards`). Total control; use it when the\n  list is a judgement call, like \"the twenty cards I want most\".\n- **`dynamic`** — you supply a `filter_json` (a `CardFilter`, the same grammar as\n  `POST /cards/search`) and membership is computed at query time. Use it when the list is a\n  *rule*: \"every McDavid rookie-year parallel\" stays complete on its own as new cards are\n  catalogued, where a curated list would silently go stale.\n\n**`visibility`** is `private` by default. Make it `public` and it becomes searchable,\nsubscribable, and eligible for the popular leaderboard — a way to share a chase, not just track\none.\n\nCompletion is always computed against your collection, never stored, so it's correct the moment\nyou add a card.","operationId":"create_custom_set","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetCreate"}}}},"responses":{"201":{"description":"The new custom set, with completion stats already computed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Collides with existing state, e.g. a name already in use.","content":{"application/json":{"example":{"detail":"a custom set named 'My Top 20 Rookies' already exists"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/custom-sets/{set_uuid}":{"patch":{"tags":["custom-sets"],"summary":"Update a custom set","description":"Change a set's name, description, cover card, visibility, or (for dynamic sets) its filter.\nSparse — send only what's changing. **Creator only.**\n\nTwo edits have visible consequences: flipping `visibility` to `private` hides the set from search\nand from anyone who subscribed to it, and editing `filter_json` redefines a dynamic set's\nmembership, so completion is recalculated against a different card list entirely.","operationId":"update_custom_set","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetUpdate"}}}},"responses":{"200":{"description":"The updated custom set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Collides with existing state, e.g. a name already in use.","content":{"application/json":{"example":{"detail":"a custom set named 'My Top 20 Rookies' already exists"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["custom-sets"],"summary":"Delete a custom set","description":"Delete a custom set and its entries. **Creator only.**\n\nThis removes a wish list, **not any cards** — your copies are untouched, since a custom set only\never referenced the catalog. If it was public, existing subscribers lose it too.","operationId":"delete_custom_set","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}}],"responses":{"204":{"description":"Deleted. No content."},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/custom-sets/{set_uuid}/cards":{"post":{"tags":["custom-sets"],"summary":"Add a card to a curated set","description":"Add one card to a **curated** set. (Dynamic sets take their members from a filter and reject\nmanual entries.) **Creator only.**\n\n**`match_mode` is the interesting decision** — it defines what would actually satisfy this slot:\n\n- **`exact`** *(default)* — this printing and no other. The Gold /10 means the Gold /10.\n- **`any_printing`** — any parallel of the same slot counts. \"I want a Bedard Young Guns\" is\n  satisfied by the base or by any parallel of it.\n- **`exact_serial`** — this printing *and* a specific `serial_number`. For chasing a jersey\n  number or a birth year: /97 out of 100, not just any /100.\n\nGet this right and completion means what you intended; get it wrong and a set reads as complete\nwhile the card you actually wanted is still missing. `position` orders the display.","operationId":"add_card","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetCardAdd"}}}},"responses":{"201":{"description":"The new entry, including whether you already own a matching copy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetCardOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Collides with existing state, e.g. a name already in use.","content":{"application/json":{"example":{"detail":"a custom set named 'My Top 20 Rookies' already exists"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/custom-sets/{set_uuid}/cards/{entry_uuid}":{"patch":{"tags":["custom-sets"],"summary":"Update a card entry","description":"Change an entry's `match_mode`, `serial_number`, or `position`. Sparse. **Creator only.**\n\nLoosening `exact` to `any_printing` can flip an entry to owned immediately if you already hold a\ndifferent parallel of that slot — often the fastest way to make a set reflect what you'd truly\naccept.","operationId":"update_card","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}},{"name":"entry_uuid","in":"path","required":true,"schema":{"type":"string","title":"Entry Uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetCardUpdate"}}}},"responses":{"200":{"description":"The updated entry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetCardOut"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["custom-sets"],"summary":"Remove a card from a curated set","description":"Drop an entry from a curated set. **Creator only.** Removes the target, never a card you own —\nand the set's completion percentage recalculates against the smaller list.","operationId":"remove_card","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}},{"name":"entry_uuid","in":"path","required":true,"schema":{"type":"string","title":"Entry Uuid"}}],"responses":{"204":{"description":"Removed. No content."},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/collectors/{collector_uuid}/custom-sets/{set_uuid}/subscribe":{"post":{"tags":["custom-sets"],"summary":"Subscribe to a public set","description":"Follow someone else's public chase set, so it appears in your own list and tracks completion\nagainst **your** collection.\n\nYou're following a definition, not copying it: the creator still owns the contents, and cards\nthey add show up as new targets for you. Want to diverge? Create your own set instead.\n\nSubscriptions are also the popularity signal behind `GET /custom-sets/popular`. Idempotent —\nsubscribing twice is fine.","operationId":"subscribe","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}}],"responses":{"204":{"description":"Subscribed. No content."},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["custom-sets"],"summary":"Unsubscribe from a set","description":"Stop following a public set.\n\nIt leaves your list and stops counting toward the set's subscriber total. **Nothing you own is\naffected** — you were tracking someone else's definition, not storing cards under it, so the\ncopies that happened to complete it stay exactly where they are.\n\nIdempotent: unsubscribing when you weren't subscribed succeeds quietly.","operationId":"unsubscribe","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"collector_uuid","in":"path","required":true,"schema":{"type":"string","title":"Collector Uuid"}},{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}}],"responses":{"204":{"description":"Unsubscribed. No content."},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/custom-sets/search":{"post":{"tags":["custom-sets"],"summary":"Search custom sets","description":"Find chase sets — every **public** set, plus your **own** private ones. Other collectors'\nprivate sets are never visible here, whatever you filter on.\n\nDiscovery, mostly: someone has probably already defined the chase you're contemplating, and\nsubscribing to theirs beats rebuilding it. Pass a `collector_uuid` to get each result personalized\nwith your completion and whether you're already subscribed.","operationId":"search_custom_sets","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetSearchQuery"}}},"required":true},"responses":{"200":{"description":"A page of matching public sets, plus your own private ones.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetSearchResult"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"detail":"invalid or revoked API key"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyAuth":[]}]}},"/custom-sets/{set_uuid}":{"get":{"tags":["custom-sets"],"summary":"Get a custom set with its cards","description":"One set in full: its cards (curated entries, or the live matches of a dynamic filter) with the\n`match_mode` for each.\n\n**Pass `collector_uuid` to make it a checklist.** Without it you get the set's definition; with\nit, every entry gains `owned` and `owned_printing` (\"Rainbow /99\", \"PSA 10\") and the set gains\ncompletion stats — which is the difference between reading a list and working one. It must be a\ncollector your key owns.\n\nPublic sets are readable by anyone with a valid key; private ones only by their creator.","operationId":"get_custom_set","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"set_uuid","in":"path","required":true,"schema":{"type":"string","title":"Set Uuid"}},{"name":"collector_uuid","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Collector UUID for completion tracking","title":"Collector Uuid"},"description":"Collector UUID for completion tracking"}],"responses":{"200":{"description":"The set, its full card list, and per-card ownership when a collector is given.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomSetDetail"}}}},"401":{"description":"Missing, unknown, revoked, or disabled API key.","content":{"application/json":{"example":{"detail":"invalid or revoked API key"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such resource — or it belongs to another account (the two are indistinguishable on purpose, so ids can't be probed).","content":{"application/json":{"example":{"detail":"collector 0d0f8f2e-1a4b-4c77-9f6e-2b8d3a1c5e90 not found"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AccountOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Account UUID"},"account_type":{"type":"string","title":"Account Type","description":"Account lane: 'collector' or 'developer'"},"default_collector_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Collector Uuid","description":"Collector used when a caller omits a collector id"}},"type":"object","required":["uuid","account_type"],"title":"AccountOut"},"AcquisitionType":{"type":"string","enum":["purchase","pack_pull","trade","gift","other"],"title":"AcquisitionType"},"ApiKeyCreate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Optional label for the key"}},"type":"object","title":"ApiKeyCreate","description":"Request to mint a new API key."},"ApiKeyCreated":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"API key UUID (stable handle for revocation)"},"raw_key":{"type":"string","title":"Raw Key","description":"The secret key. Shown once — store it now; it cannot be recovered."},"key_prefix":{"type":"string","title":"Key Prefix","description":"Leading chars, e.g. sk_live_a1b2"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["uuid","raw_key","key_prefix"],"title":"ApiKeyCreated","description":"Returned once, immediately after minting — the only time the raw key is ever shown."},"ApiKeyOut":{"properties":{"uuid":{"type":"string","title":"Uuid"},"key_prefix":{"type":"string","title":"Key Prefix"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"created_dt":{"type":"string","format":"date-time","title":"Created Dt"},"last_used_dt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used Dt"},"revoked_dt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked Dt"}},"type":"object","required":["uuid","key_prefix","created_dt"],"title":"ApiKeyOut","description":"A key as listed in the portal (no secret material)."},"AttributeOut":{"properties":{"name":{"type":"string","title":"Name"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["name"],"title":"AttributeOut"},"BreakCreate":{"properties":{"set_uuid":{"type":"string","title":"Set Uuid","description":"UUID of the product (set) that was opened"},"break_type":{"anyOf":[{"$ref":"#/components/schemas/BreakType"},{"$ref":"#/components/schemas/SealedFormat"}],"title":"Break Type","description":"What was opened: a specific sealed format (e.g. blaster_box — preferred when the set's SKUs are known, see GET /sets/{set_uuid}/sealed) or a generic pack/box/case/other"},"total_cost":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Total Cost","description":"Total amount paid for the sealed product"},"break_date":{"type":"string","format":"date","title":"Break Date","description":"Date the product was opened (required)"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source","description":"Where purchased, e.g. \"eBay\", \"LCS\""},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["set_uuid","break_type","total_cost","break_date"],"title":"BreakCreate","description":"Record a break (opening sealed product).","examples":[{"break_date":"2026-07-14","break_type":"box","set_uuid":"3fa85f64-5717-4562-b3fc-2c963f66afa6","source":"LCS","total_cost":"249.99"}]},"BreakOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Break UUID"},"collector_uuid":{"type":"string","title":"Collector Uuid","description":"Collector UUID"},"set_uuid":{"type":"string","title":"Set Uuid","description":"Set UUID"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"break_type":{"type":"string","title":"Break Type"},"total_cost":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Cost"},"break_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Break Date"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"copy_count":{"type":"integer","title":"Copy Count","description":"Number of card copies cataloged from this break"},"cost_per_card":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Cost Per Card","description":"total_cost / copy_count (None when copy_count is 0)"}},"type":"object","required":["uuid","collector_uuid","set_uuid","break_type","total_cost","copy_count"],"title":"BreakOut"},"BreakSearchQuery":{"properties":{"set_slug":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Set Slug","description":"Set slug(s) — any-of"},"break_type":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Break Type","description":"Break type(s) — any-of"},"break_after":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Break After","description":"Break date >= this"},"break_before":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Break Before","description":"Break date <= this"},"sort":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort","description":"Sort key, prefix with - for descending. Options: break_date, total_cost, cost_per_card"},"limit":{"type":"integer","maximum":200.0,"minimum":1.0,"title":"Limit","default":50},"offset":{"type":"integer","minimum":0.0,"title":"Offset","default":0}},"type":"object","title":"BreakSearchQuery","description":"Filter and browse breaks."},"BreakSearchResult":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/BreakOut"},"type":"array","title":"Items"}},"type":"object","required":["total","limit","offset"],"title":"BreakSearchResult"},"BreakStat":{"properties":{"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"break_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Break Date"},"break_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Break Type"},"total_cost":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Cost"},"cards":{"type":"integer","title":"Cards"},"cost_per_card":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Cost Per Card"}},"type":"object","required":["total_cost","cards"],"title":"BreakStat","description":"A break (box/pack opened) and what came of it."},"BreakType":{"type":"string","enum":["pack","box","case","other"],"title":"BreakType","description":"Generic what-was-opened fallbacks. A break's ``break_type`` preferably carries a specific\n``SealedFormat`` value (blaster_box, hobby_case, …); these cover a set with no sealed SKUs\nin the catalog, or a product outside them."},"BreakUpdate":{"properties":{"break_type":{"anyOf":[{"$ref":"#/components/schemas/BreakType"},{"$ref":"#/components/schemas/SealedFormat"},{"type":"null"}],"title":"Break Type"},"total_cost":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Total Cost"},"break_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Break Date"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"BreakUpdate","description":"Partial update — only set fields are applied."},"CardComps":{"properties":{"card_uuid":{"type":"string","title":"Card Uuid"},"card_number":{"type":"string","title":"Card Number"},"subjects":{"items":{"type":"string"},"type":"array","title":"Subjects"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"subset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subset"},"finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish","description":"The card's own finish (None = base)"},"total":{"type":"integer","title":"Total","description":"Total comps matched to this card, before the limit","default":0},"comps":{"items":{"$ref":"#/components/schemas/CompOut"},"type":"array","title":"Comps"}},"type":"object","required":["card_uuid","card_number"],"title":"CardComps","description":"Recent comps for one catalog card — the raw sales behind its market value.\n\nOrdered most-recent first. ``total`` is the full count matched to the card; ``comps`` is the\n(possibly limited) page actually returned."},"CardCopyCostCreate":{"properties":{"cost_category":{"$ref":"#/components/schemas/CostCategory"},"amount":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Amount"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"incurred_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Incurred Date"},"vendor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vendor","description":"Who was paid, e.g. \"PSA\", \"USPS\""}},"type":"object","required":["cost_category","amount"],"title":"CardCopyCostCreate","description":"Add a post-acquisition cost to a card copy.","examples":[{"amount":"28.00","cost_category":"grading","description":"PSA Value submission","incurred_date":"2026-05-19","vendor":"PSA"},{"amount":"6.45","cost_category":"shipping","vendor":"USPS"}]},"CardCopyCostOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Cost entry UUID"},"cost_category":{"type":"string","title":"Cost Category"},"amount":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Amount"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"incurred_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Incurred Date"},"vendor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vendor"}},"type":"object","required":["uuid","cost_category","amount"],"title":"CardCopyCostOut"},"CardCopyCostUpdate":{"properties":{"cost_category":{"anyOf":[{"$ref":"#/components/schemas/CostCategory"},{"type":"null"}]},"amount":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Amount"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"incurred_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Incurred Date"},"vendor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vendor"}},"type":"object","title":"CardCopyCostUpdate","description":"Partial update — only set fields are applied."},"CardCopyCreate":{"properties":{"card_uuid":{"type":"string","title":"Card Uuid","description":"UUID of the catalog card"},"break_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Break Uuid","description":"Break UUID; when set, acquisition cost is derived from the break"},"lot_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lot Uuid","description":"Purchase-lot UUID; when set, acquisition cost is derived from the lot"},"quantity":{"type":"integer","minimum":1.0,"title":"Quantity","default":1},"serial_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Serial Number"},"self_grade":{"anyOf":[{"$ref":"#/components/schemas/Grade"},{"type":"null"}],"description":"Your own grade assessment (1-10 rung)"},"grading_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grading Company","description":"PSA / BGS / SGC / CGC"},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade","description":"e.g. \"10\", \"9.5\", \"Authentic\""},"cert_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cert Number"},"acquisition_type":{"anyOf":[{"$ref":"#/components/schemas/AcquisitionType"},{"type":"null"}]},"acquisition_cost":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Acquisition Cost","description":"Manual acquisition cost; ignored when break_uuid or lot_uuid is set (derived instead)"},"acquired_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Acquired Date"},"acquired_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acquired Source"},"storage_location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storage Location"},"status":{"$ref":"#/components/schemas/CopyStatus","default":"in_collection"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["card_uuid"],"title":"CardCopyCreate","description":"Add one physical copy to a collection. `grading_company` is a name (e.g. \"PSA\"); the API\nresolves it to the lookup. `self_grade` is your own rung assessment (set it for any card, raw\nor graded); the grading fields are the professional verdict — both may be present at once.\n\n`grading_company` and `grade` must be provided together (or both omitted): the pair is what\nforms the copy's price key, and a half-entered slab would be silently appraised at the RAW\nmarket price.","examples":[{"acquired_date":"2026-06-30","acquired_source":"eBay","acquisition_cost":"85.00","acquisition_type":"purchase","card_uuid":"3fa85f64-5717-4562-b3fc-2c963f66afa6","self_grade":"NM-MT","storage_location":"Box 2"},{"acquisition_type":"pack_pull","break_uuid":"9c858901-8a57-4791-81fe-4c455b099bc9","card_uuid":"3fa85f64-5717-4562-b3fc-2c963f66afa6","serial_number":54},{"acquisition_cost":"420.00","acquisition_type":"purchase","card_uuid":"3fa85f64-5717-4562-b3fc-2c963f66afa6","cert_number":"78451236","grade":"10","grading_company":"PSA","self_grade":"GEM-MT"}]},"CardCopyOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Card copy UUID"},"collector_uuid":{"type":"string","title":"Collector Uuid","description":"Collector UUID"},"card_uuid":{"type":"string","title":"Card Uuid","description":"Catalog card UUID"},"quantity":{"type":"integer","title":"Quantity"},"serial_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Serial Number"},"self_grade":{"anyOf":[{"$ref":"#/components/schemas/Grade"},{"type":"null"}],"description":"Your own grade assessment (1-10 rung)"},"grading_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grading Company"},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade"},"cert_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cert Number"},"grade_delta":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Grade Delta","description":"Pro grade minus self-grade on the 1-10 scale (computed); - = optimistic (overrated), + = pessimistic (underrated), 0 = spot on. None unless both a self-grade and a numeric pro grade exist."},"acquisition_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acquisition Type"},"acquired_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Acquired Date"},"acquired_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acquired Source"},"break_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Break Uuid","description":"Break UUID (if from a break)"},"lot_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lot Uuid","description":"Purchase-lot UUID (if from a lot)"},"acquisition_cost":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Acquisition Cost","description":"Cost to acquire: break-derived, lot-derived, or user-supplied"},"additional_costs":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Additional Costs","description":"Sum of post-acquisition expenses (grading, shipping, etc.)"},"cost_basis":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Cost Basis","description":"Total all-in cost: acquisition_cost + additional_costs"},"market":{"anyOf":[{"$ref":"#/components/schemas/MarketValue"},{"type":"null"}],"description":"Market pricing data when available (FMV, unrealized gain/loss, ROI)"},"costs":{"items":{"$ref":"#/components/schemas/CardCopyCostOut"},"type":"array","title":"Costs","description":"Individual post-acquisition expenses"},"sale_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Sale Price","description":"What the card sold for"},"sold_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Sold Date","description":"Date the card was sold"},"realized_gain_loss":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Realized Gain Loss","description":"sale_price - cost_basis (actual P&L from a completed sale)"},"storage_location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storage Location"},"status":{"type":"string","title":"Status","default":"in_collection"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"card":{"anyOf":[{"$ref":"#/components/schemas/CardOut"},{"type":"null"}]}},"type":"object","required":["uuid","collector_uuid","card_uuid","quantity"],"title":"CardCopyOut","description":"A physical copy with all identity, grading, acquisition, and financial data.\n\nFinancial fields (all computed at query time):\n- acquisition_cost: from the break (total_cost / copies) or user-supplied\n- additional_costs: sum of post-acquisition expenses\n- cost_basis: acquisition_cost + additional_costs"},"CardCopyUpdate":{"properties":{"break_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Break Uuid"},"lot_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lot Uuid"},"quantity":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Quantity"},"serial_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Serial Number"},"self_grade":{"anyOf":[{"$ref":"#/components/schemas/Grade"},{"type":"null"}]},"grading_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grading Company"},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade"},"cert_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cert Number"},"acquisition_type":{"anyOf":[{"$ref":"#/components/schemas/AcquisitionType"},{"type":"null"}]},"acquisition_cost":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Acquisition Cost"},"acquired_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Acquired Date"},"acquired_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acquired Source"},"sale_price":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Sale Price"},"sold_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Sold Date"},"storage_location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storage Location"},"status":{"anyOf":[{"$ref":"#/components/schemas/CopyStatus"},{"type":"null"}]},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"CardCopyUpdate","description":"Partial update — only set fields are applied.","examples":[{"sale_price":"310.00","sold_date":"2026-07-21","status":"sold"},{"cert_number":"78451236","grade":"9","grading_company":"PSA"},{"notes":"PC keeper","storage_location":"Safe deposit"}]},"CardMarket":{"properties":{"card_uuid":{"type":"string","title":"Card Uuid"},"card_number":{"type":"string","title":"Card Number"},"subjects":{"items":{"type":"string"},"type":"array","title":"Subjects"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"subset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subset"},"finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish","description":"The card's own finish (None = base)"},"as_of_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"As Of Date","description":"Most recent snapshot date across all price points"},"price_points":{"items":{"$ref":"#/components/schemas/PricePointOut"},"type":"array","title":"Price Points"}},"type":"object","required":["card_uuid","card_number"],"title":"CardMarket","description":"All current pricing for one catalog card — every finish/grade combination that has data.\n\nReturned by ``GET /cards/{uuid}/market``. This is the 'what is this card worth?' view,\nindependent of whether you own it."},"CardOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Card UUID"},"card_number":{"type":"string","title":"Card Number"},"brand":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brand"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"set_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Slug"},"season":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Season"},"year":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year"},"subset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subset"},"finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish"},"parent_card_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Card Uuid","description":"Parent (base) card UUID; None for base cards"},"release_set_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Release Set Slug","description":"If this is an UPDATE card carried into a later product: that release's set slug (its set_name/year above remain the card's own design year). None for normal cards."},"release_set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Release Set Name","description":"Display name of the release set, if any"},"print_run":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Print Run"},"odds":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Odds"},"subjects":{"items":{"$ref":"#/components/schemas/SubjectOut"},"type":"array","title":"Subjects"},"attributes":{"items":{"$ref":"#/components/schemas/AttributeOut"},"type":"array","title":"Attributes"},"owned_quantity":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Owned Quantity","description":"Copies the queried collector owns (only set when `collector` is given)"},"market":{"anyOf":[{"$ref":"#/components/schemas/FmvSummary"},{"type":"null"}],"description":"Headline fair market value for this printing (only set when `include_market` is given)"}},"type":"object","required":["uuid","card_number"],"title":"CardOut"},"CardPriceHistory":{"properties":{"card_uuid":{"type":"string","title":"Card Uuid"},"card_number":{"type":"string","title":"Card Number"},"subjects":{"items":{"type":"string"},"type":"array","title":"Subjects"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"grade_key":{"type":"string","title":"Grade Key","description":"Grade bucket: \"RAW\", \"PSA-10\", etc."},"finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish","description":"Parallel finish; None = base"},"start_date":{"type":"string","format":"date","title":"Start Date"},"end_date":{"type":"string","format":"date","title":"End Date"},"interval":{"$ref":"#/components/schemas/TimeInterval","default":"daily"},"points":{"items":{"$ref":"#/components/schemas/CardPricePoint"},"type":"array","title":"Points"}},"type":"object","required":["card_uuid","card_number","grade_key","start_date","end_date"],"title":"CardPriceHistory","description":"Price history for a single catalog card at a specific grade/finish."},"CardPricePoint":{"properties":{"date":{"type":"string","format":"date","title":"Date"},"price_median":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Price Median"},"price_low":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Low"},"price_high":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price High"},"sample_size":{"type":"integer","title":"Sample Size","default":0},"low_confidence":{"type":"boolean","title":"Low Confidence","description":"True when the snapshot was computed from fewer sales than the aggregator trusts (thin market) — treat the value as indicative, not solid","default":false}},"type":"object","required":["date","price_median"],"title":"CardPricePoint","description":"One data point in a card's price history."},"CardSearchQuery":{"properties":{"q":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q","description":"Free text: player name, set name, or card number"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject","description":"Player name (substring match, case-insensitive)"},"subject_exact":{"type":"boolean","title":"Subject Exact","description":"Match `subject` exactly instead of substring","default":false},"subject_all":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Subject All","description":"Card must feature ALL listed players (for combo/dual cards)"},"card_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Card Number","description":"Card number (exact match, e.g. YG-201)"},"set_slug":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Set Slug","description":"Set slug(s) — any-of. PRECISE catalog membership (id_set only)"},"release":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Release","description":"Set UUID(s) treated as a PRODUCT/BOX: matches cards whose home set OR release set is one of these — exactly what was physically pulled (update cards carried in are included; synthesized bases, never in a pack, are not). Use this to scope to a box you opened (set_slug stays a precise catalog filter)."},"brand":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Brand","description":"Brand name(s) — any-of"},"subset":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Subset","description":"Subset name(s) — any-of"},"year":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year","description":"Exact season-start year (e.g. 2025 for 2025-26)"},"year_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year Min","description":"Minimum season-start year (inclusive)"},"year_max":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year Max","description":"Maximum season-start year (inclusive)"},"sport":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Sport","description":"Sport name(s) — any-of"},"league":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"League","description":"League name(s) — any-of"},"team":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Team","description":"Team name(s) — any-of"},"attribute":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attribute","description":"Attribute name(s) — any-of"},"attribute_all":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attribute All","description":"Attribute name(s) — must have ALL"},"attribute_not":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attribute Not","description":"Attribute name(s) — exclude cards with any of these"},"rookie":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rookie","description":"Shortcut: true adds attribute=Rookie"},"auto":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Auto","description":"Shortcut: true adds attribute=Autograph"},"relic":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Relic","description":"Shortcut: true adds attribute=Memorabilia"},"finish":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Finish","description":"Finish name(s) — any-of"},"base_only":{"type":"boolean","title":"Base Only","description":"Only base printings (no finish)","default":false},"parallel_only":{"type":"boolean","title":"Parallel Only","description":"Only parallels (has a finish)","default":false},"numbered_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numbered Min","description":"Print run >= this value"},"numbered_max":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numbered Max","description":"Print run <= this value"},"is_numbered":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Numbered","description":"Has a serial-numbered print run"},"is_1of1":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is 1Of1","description":"One-of-one (print run = 1)"},"collector":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Collector","description":"Collector UUID — annotates each card with owned_quantity"},"owned":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Owned","description":"With `collector`: true = only owned, false = only missing"},"facets":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Facets","description":"Comma-separated facet dimensions to include in the response. Options: brand, year, subset, finish, team, attribute"},"include_market":{"type":"boolean","title":"Include Market","description":"Annotate each returned card with a headline FMV (`market`). Off by default; when on, adds one batched price lookup scoped to the current page.","default":false},"sort":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort","description":"Sort key, prefix with - for descending. Options: year, card_number, numbered, subject, brand, set"},"limit":{"type":"integer","maximum":200.0,"minimum":1.0,"title":"Limit","default":50},"offset":{"type":"integer","minimum":0.0,"title":"Offset","default":0}},"type":"object","title":"CardSearchQuery","description":"Catalog search: shared card filters + collector context (have/need) + facets + sort + paging.","examples":[{"limit":20,"rookie":true,"subject":"McDavid"},{"facets":["finish","team"],"limit":50,"set_slug":["2025-26-upper-deck-series-1"],"sort":"card_number","subset":["Young Guns"]},{"attribute_all":["Rookie","Autograph"],"attribute_not":["Memorabilia"],"include_market":true,"is_numbered":true,"numbered_max":99,"sort":"-numbered","year_min":2023},{"collector":"8f14e45f-ceea-467a-9a6b-1d6e0b0e5c21","limit":100,"owned":false,"team":["Chicago Blackhawks"]}]},"CardSearchResult":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/CardOut"},"type":"array","title":"Items"},"facets":{"anyOf":[{"$ref":"#/components/schemas/Facets"},{"type":"null"}],"description":"Faceted counts for the requested dimensions (only set when `facets` is given)"}},"type":"object","required":["total","limit","offset"],"title":"CardSearchResult"},"CatalogStats":{"properties":{"total_cards":{"type":"integer","title":"Total Cards","default":0},"base_cards":{"type":"integer","title":"Base Cards","default":0},"parallels":{"type":"integer","title":"Parallels","default":0},"sets":{"type":"integer","title":"Sets","default":0},"players":{"type":"integer","title":"Players","default":0},"teams":{"type":"integer","title":"Teams","default":0},"rookies":{"type":"integer","title":"Rookies","default":0},"autos":{"type":"integer","title":"Autos","default":0},"relics":{"type":"integer","title":"Relics","default":0},"numbered":{"type":"integer","title":"Numbered","default":0},"one_of_ones":{"type":"integer","title":"One Of Ones","default":0},"biggest_sets":{"items":{"$ref":"#/components/schemas/LabeledCount"},"type":"array","title":"Biggest Sets"}},"type":"object","title":"CatalogStats","description":"Slab-wide overview — the breadth of the whole catalog. Deliberately lean; grow as needed."},"CollectedCard":{"properties":{"card":{"$ref":"#/components/schemas/CommunityCard"},"collector_count":{"type":"integer","title":"Collector Count","description":"Distinct collectors who own at least one copy"},"copy_count":{"type":"integer","title":"Copy Count","description":"Total copies held across the community"}},"type":"object","required":["card","collector_count","copy_count"],"title":"CollectedCard","description":"One entry in the most-collected ranking — a card and how widely it's held."},"CollectedPlayer":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Subject UUID"},"name":{"type":"string","title":"Name"},"collector_count":{"type":"integer","title":"Collector Count","description":"Distinct collectors who own a card of this player"},"copy_count":{"type":"integer","title":"Copy Count","description":"Total copies of this player held across the community"}},"type":"object","required":["uuid","name","collector_count","copy_count"],"title":"CollectedPlayer","description":"One entry in the most-collected-players ranking — a subject and how widely they're held."},"CollectionFacets":{"properties":{"brand":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Brand"},"year":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Year"},"subset":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Subset"},"finish":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Finish"},"team":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Team"},"attribute":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Attribute"},"status":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Status"},"grading_company":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Grading Company"},"acquisition_type":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Acquisition Type"}},"type":"object","title":"CollectionFacets","description":"Card-level facets plus collection-specific ones."},"CollectionResult":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/CardCopyOut"},"type":"array","title":"Items"},"summary":{"anyOf":[{"$ref":"#/components/schemas/PortfolioSummary"},{"type":"null"}],"description":"Aggregate financials for the full filtered set (always included)"},"facets":{"anyOf":[{"$ref":"#/components/schemas/CollectionFacets"},{"type":"null"}],"description":"Faceted counts (only set when `facets` is given)"}},"type":"object","required":["total","limit","offset"],"title":"CollectionResult"},"CollectionSearchQuery":{"properties":{"q":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q","description":"Free text: player name, set name, or card number"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject","description":"Player name (substring match, case-insensitive)"},"subject_exact":{"type":"boolean","title":"Subject Exact","description":"Match `subject` exactly instead of substring","default":false},"subject_all":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Subject All","description":"Card must feature ALL listed players (for combo/dual cards)"},"card_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Card Number","description":"Card number (exact match, e.g. YG-201)"},"set_slug":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Set Slug","description":"Set slug(s) — any-of. PRECISE catalog membership (id_set only)"},"release":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Release","description":"Set UUID(s) treated as a PRODUCT/BOX: matches cards whose home set OR release set is one of these — exactly what was physically pulled (update cards carried in are included; synthesized bases, never in a pack, are not). Use this to scope to a box you opened (set_slug stays a precise catalog filter)."},"brand":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Brand","description":"Brand name(s) — any-of"},"subset":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Subset","description":"Subset name(s) — any-of"},"year":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year","description":"Exact season-start year (e.g. 2025 for 2025-26)"},"year_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year Min","description":"Minimum season-start year (inclusive)"},"year_max":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year Max","description":"Maximum season-start year (inclusive)"},"sport":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Sport","description":"Sport name(s) — any-of"},"league":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"League","description":"League name(s) — any-of"},"team":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Team","description":"Team name(s) — any-of"},"attribute":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attribute","description":"Attribute name(s) — any-of"},"attribute_all":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attribute All","description":"Attribute name(s) — must have ALL"},"attribute_not":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Attribute Not","description":"Attribute name(s) — exclude cards with any of these"},"rookie":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rookie","description":"Shortcut: true adds attribute=Rookie"},"auto":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Auto","description":"Shortcut: true adds attribute=Autograph"},"relic":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Relic","description":"Shortcut: true adds attribute=Memorabilia"},"finish":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Finish","description":"Finish name(s) — any-of"},"base_only":{"type":"boolean","title":"Base Only","description":"Only base printings (no finish)","default":false},"parallel_only":{"type":"boolean","title":"Parallel Only","description":"Only parallels (has a finish)","default":false},"numbered_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numbered Min","description":"Print run >= this value"},"numbered_max":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numbered Max","description":"Print run <= this value"},"is_numbered":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Numbered","description":"Has a serial-numbered print run"},"is_1of1":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is 1Of1","description":"One-of-one (print run = 1)"},"status":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Status","description":"Copy status — any-of"},"graded":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Graded","description":"Only graded (true) / only raw (false)"},"grading_company":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Grading Company","description":"Grading company name(s) — any-of"},"self_grade":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Self Grade","description":"Self-assessed grade rung(s) — any-of"},"acquisition_type":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Acquisition Type","description":"Acquisition type(s) — any-of"},"has_break":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Break","description":"Only copies from a break (true) / without (false)"},"break_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Break Uuid","description":"Only copies from this specific break (UUID)"},"has_lot":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Lot","description":"Only copies from a purchase lot (true) / without (false)"},"lot_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lot Uuid","description":"Only copies from this specific purchase lot (UUID)"},"acquisition_cost_min":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Acquisition Cost Min","description":"Manual acquisition cost >= this"},"acquisition_cost_max":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Acquisition Cost Max","description":"Manual acquisition cost <= this"},"acquired_after":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Acquired After"},"acquired_before":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Acquired Before"},"storage_location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storage Location","description":"Storage location (substring match)"},"serial":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Serial","description":"The owned copy's serial number"},"facets":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Facets","description":"Facet dimensions to include. Card-level: brand, year, subset, finish, team, attribute. Collection-level: status, grading_company, acquisition_type"},"sort":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort","description":"Sort key, prefix with - for descending. Options: acquired_date, acquisition_cost, numbered, year, card_number, brand, set, fmv, unrealized, roi"},"limit":{"type":"integer","maximum":200.0,"minimum":1.0,"title":"Limit","default":50},"offset":{"type":"integer","minimum":0.0,"title":"Offset","default":0}},"type":"object","title":"CollectionSearchQuery","description":"Search your collection: every catalog card filter + collection-only facets, sort, paging.","examples":[{"limit":20,"sort":"-fmv"},{"facets":["brand","status"],"graded":true,"grading_company":["PSA"],"rookie":true,"sort":"-roi"},{"acquired_after":"2026-01-01","set_slug":["2025-26-upper-deck-series-1"],"status":["in_collection"]},{"is_numbered":true,"limit":1,"numbered_max":25}]},"CollectorCreate":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"CollectorCreate"},"CollectorOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Collector UUID"},"name":{"type":"string","title":"Name"}},"type":"object","required":["uuid","name"],"title":"CollectorOut"},"CollectorUpdate":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"CollectorUpdate","description":"Rename a collector. `name` is a slab-wide-unique handle; the collector's UUID is unchanged,\nso every reference to it (copies, breaks, trades) survives the rename."},"CommunityBoard":{"properties":{"stats":{"$ref":"#/components/schemas/CatalogStats","description":"Slab-wide catalog overview (same data as GET /stats)"},"ticker":{"items":{"$ref":"#/components/schemas/TickerItem"},"type":"array","title":"Ticker","description":"Scrolling 'what's happening' feed: new catalog sets, the week's biggest public sale, and anonymized collection activity (adds/breaks/grades/listings)."},"most_valuable_raw":{"items":{"$ref":"#/components/schemas/ValuableCard"},"type":"array","title":"Most Valuable Raw"},"most_valuable_graded":{"items":{"$ref":"#/components/schemas/ValuableCard"},"type":"array","title":"Most Valuable Graded"},"most_collected":{"items":{"$ref":"#/components/schemas/CollectedCard"},"type":"array","title":"Most Collected"},"most_collected_players":{"items":{"$ref":"#/components/schemas/CollectedPlayer"},"type":"array","title":"Most Collected Players"},"hottest_players":{"items":{"$ref":"#/components/schemas/HotPlayer"},"type":"array","title":"Hottest Players"},"rarest_owned":{"items":{"$ref":"#/components/schemas/RarestOwnedCard"},"type":"array","title":"Rarest Owned"},"popular_sets":{"items":{"$ref":"#/components/schemas/CustomSetOut"},"type":"array","title":"Popular Sets","description":"Popular custom (chase) sets (same data as GET /custom-sets/popular)"},"glossary":{"additionalProperties":{"$ref":"#/components/schemas/MetricInfo"},"type":"object","title":"Glossary","description":"Plain-language explanation of each leaderboard, keyed by metric (e.g. 'community.most_valuable_graded') — the same text the portal shows in its info tooltips, so an API consumer can render or read it too. Full catalog at GET /glossary."}},"type":"object","required":["stats"],"title":"CommunityBoard","description":"The complete public community picture in ONE payload — so an API-only consumer gets exactly\nwhat the portal's community page renders from a single ``GET /community`` call, with no\nfollow-up requests.\n\nBundles the catalog overview (``stats``), the leaderboards, and the popular chase sets. All\nleaderboards are aggregate — card/player names and counts only, never collector identities.\nEmpty lists are the honest answer for a board with no pricing yet (the ``most_valuable_*``\nboards) or no collections yet (the ``*_collected`` / ``rarest_owned`` boards).\n\nMost-valuable is split by grade so the graded premium is legible: ``most_valuable_raw`` ranks\nprintings by their RAW FMV, ``most_valuable_graded`` by their best-sampled graded FMV."},"CommunityCard":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Card UUID"},"card_number":{"type":"string","title":"Card Number"},"subjects":{"items":{"type":"string"},"type":"array","title":"Subjects"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"season":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Season"},"subset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subset"},"finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish","description":"The printing's finish; None = base card"},"print_run":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Print Run"}},"type":"object","required":["uuid","card_number"],"title":"CommunityCard","description":"A catalog card as it appears on a community leaderboard — enough to identify and link it."},"CompOut":{"properties":{"sold_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Sold Date","description":"When the sale closed (may be absent)"},"sale_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Sale Price"},"currency":{"type":"string","title":"Currency","default":"USD"},"grade_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade Key","description":"Grade bucket, e.g. \"RAW\", \"PSA-10\""},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade"},"finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish","description":"Resolved parallel finish; None = base"},"marketplace":{"type":"string","title":"Marketplace","description":"Where the sale happened, e.g. ebay"},"sale_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sale Type","description":"auction | fixedprice | bestoffer"},"title":{"type":"string","title":"Title","description":"Seller's listing title"},"match_status":{"type":"string","title":"Match Status","description":"matched | ambiguous | unmatched"}},"type":"object","required":["marketplace","title","match_status"],"title":"CompOut","description":"One observed sale (comp) resolved to a card — the raw market evidence an FMV is built from.\n\nReturned by ``GET /cards/{uuid}/comps``. This is the ingest side of a comp (the sale as\nreported), trimmed to what's useful to a human eyeballing recent sales."},"CompletionStats":{"properties":{"total_cards":{"type":"integer","title":"Total Cards"},"owned_cards":{"type":"integer","title":"Owned Cards"},"completion_pct":{"type":"number","title":"Completion Pct","description":"0.0 to 100.0"}},"type":"object","required":["total_cards","owned_cards","completion_pct"],"title":"CompletionStats","description":"Completion tracking for a collector against a custom set."},"CopyStatus":{"type":"string","enum":["in_collection","for_trade","for_sale","sold"],"title":"CopyStatus"},"CostCategory":{"type":"string","enum":["grading","shipping","insurance","authentication","other"],"title":"CostCategory"},"CustomSetCardAdd":{"properties":{"card_uuid":{"type":"string","title":"Card Uuid","description":"UUID of the card to add"},"match_mode":{"$ref":"#/components/schemas/MatchMode","default":"exact"},"serial_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Serial Number","description":"Only for exact_serial — the specific serial number wanted"},"position":{"type":"integer","title":"Position","description":"Ordering within the set","default":0}},"type":"object","required":["card_uuid"],"title":"CustomSetCardAdd","description":"Add a card entry to a curated set.","examples":[{"card_uuid":"3fa85f64-5717-4562-b3fc-2c963f66afa6","match_mode":"any_printing","position":1},{"card_uuid":"3fa85f64-5717-4562-b3fc-2c963f66afa6","match_mode":"exact_serial","position":2,"serial_number":97}]},"CustomSetCardOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Card entry UUID"},"card":{"$ref":"#/components/schemas/CardOut"},"match_mode":{"type":"string","title":"Match Mode"},"serial_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Serial Number"},"position":{"type":"integer","title":"Position"},"owned":{"type":"boolean","title":"Owned","description":"Whether the requesting collector owns a matching copy"},"owned_printing":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owned Printing","description":"What the collector actually owns (e.g. 'Rainbow /99', 'Base', 'PSA 10') — None if not owned"}},"type":"object","required":["uuid","card","match_mode","position","owned"],"title":"CustomSetCardOut","description":"A card entry in a custom set, with its ownership status for the requesting collector."},"CustomSetCardUpdate":{"properties":{"match_mode":{"anyOf":[{"$ref":"#/components/schemas/MatchMode"},{"type":"null"}]},"serial_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Serial Number"},"position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Position"}},"type":"object","title":"CustomSetCardUpdate","description":"Partial update of a card entry."},"CustomSetCreate":{"properties":{"name":{"type":"string","title":"Name","description":"Display name for the set"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"visibility":{"$ref":"#/components/schemas/Visibility","default":"private"},"set_type":{"$ref":"#/components/schemas/CustomSetType"},"filter_json":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Filter Json","description":"Serialized CardFilter for dynamic sets; must be None for curated sets"},"cover_card_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cover Card Uuid","description":"UUID of a representative card for display"}},"type":"object","required":["name","set_type"],"title":"CustomSetCreate","description":"Create a new custom set. The creator is identified by the collector_id path parameter.","examples":[{"description":"The twenty rookie cards I want most.","name":"My Top 20 Rookies","set_type":"curated","visibility":"private"},{"description":"Auto-tracks anything new that matches.","filter_json":{"is_numbered":true,"parallel_only":true,"subject":"Bedard"},"name":"Every Bedard Numbered Parallel","set_type":"dynamic","visibility":"public"}]},"CustomSetDetail":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Custom set UUID"},"creator_uuid":{"type":"string","title":"Creator Uuid","description":"Creator's collector UUID"},"creator_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Creator Name"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"visibility":{"type":"string","title":"Visibility"},"set_type":{"type":"string","title":"Set Type"},"cover_card":{"anyOf":[{"$ref":"#/components/schemas/CardOut"},{"type":"null"}]},"card_count":{"type":"integer","title":"Card Count","description":"Number of cards in this set","default":0},"subscriber_count":{"type":"integer","title":"Subscriber Count","description":"Number of collectors tracking this set","default":0},"is_subscribed":{"type":"boolean","title":"Is Subscribed","description":"Whether the requesting collector is subscribed (False if no collector context)","default":false},"cards":{"items":{"$ref":"#/components/schemas/CustomSetCardOut"},"type":"array","title":"Cards"},"completion":{"anyOf":[{"$ref":"#/components/schemas/CompletionStats"},{"type":"null"}],"description":"Completion stats for the requesting collector (None if no collector context)"}},"type":"object","required":["uuid","creator_uuid","name","visibility","set_type"],"title":"CustomSetDetail","description":"Full detail view — includes cards with per-card ownership and completion stats."},"CustomSetOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Custom set UUID"},"creator_uuid":{"type":"string","title":"Creator Uuid","description":"Creator's collector UUID"},"creator_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Creator Name"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"visibility":{"type":"string","title":"Visibility"},"set_type":{"type":"string","title":"Set Type"},"cover_card":{"anyOf":[{"$ref":"#/components/schemas/CardOut"},{"type":"null"}]},"card_count":{"type":"integer","title":"Card Count","description":"Number of cards in this set","default":0},"subscriber_count":{"type":"integer","title":"Subscriber Count","description":"Number of collectors tracking this set","default":0},"is_subscribed":{"type":"boolean","title":"Is Subscribed","description":"Whether the requesting collector is subscribed (False if no collector context)","default":false}},"type":"object","required":["uuid","creator_uuid","name","visibility","set_type"],"title":"CustomSetOut","description":"Summary view of a custom set — used in search results and list views."},"CustomSetSearchQuery":{"properties":{"q":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q","description":"Search by name (substring match)"},"creator_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Creator Uuid","description":"Only sets by this creator"},"collector_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Collector Uuid","description":"Requesting collector — includes their private sets and is_subscribed flag"},"visibility":{"anyOf":[{"$ref":"#/components/schemas/Visibility"},{"type":"null"}],"description":"Filter by visibility (default: show public + own private)"},"set_type":{"anyOf":[{"$ref":"#/components/schemas/CustomSetType"},{"type":"null"}],"description":"Filter by curated or dynamic"},"subscribed_only":{"type":"boolean","title":"Subscribed Only","description":"Only sets the collector_uuid is subscribed to","default":false},"sort":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort","description":"Sort key, prefix with - for descending. Options: subscribers, name, newest"},"limit":{"type":"integer","maximum":200.0,"minimum":1.0,"title":"Limit","default":50},"offset":{"type":"integer","minimum":0.0,"title":"Offset","default":0}},"type":"object","title":"CustomSetSearchQuery","description":"Discover custom sets. Public sets are always visible; your own private sets are included\nwhen collector_uuid is provided."},"CustomSetSearchResult":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/CustomSetOut"},"type":"array","title":"Items"}},"type":"object","required":["total","limit","offset"],"title":"CustomSetSearchResult"},"CustomSetType":{"type":"string","enum":["curated","dynamic"],"title":"CustomSetType"},"CustomSetUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"visibility":{"anyOf":[{"$ref":"#/components/schemas/Visibility"},{"type":"null"}]},"cover_card_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cover Card Uuid"},"filter_json":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Filter Json"}},"type":"object","title":"CustomSetUpdate","description":"Partial update — only set fields are applied. Creator only."},"DashboardStats":{"properties":{"collector":{"type":"string","title":"Collector"},"total_cards":{"type":"integer","title":"Total Cards","default":0},"players":{"type":"integer","title":"Players","default":0},"teams":{"type":"integer","title":"Teams","default":0},"breaks":{"type":"integer","title":"Breaks","default":0},"lots":{"type":"integer","title":"Lots","default":0},"total_cost_basis":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Total Cost Basis"},"avg_cost_per_card":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Avg Cost Per Card"},"base_count":{"type":"integer","title":"Base Count","default":0},"parallel_count":{"type":"integer","title":"Parallel Count","default":0},"rookies":{"type":"integer","title":"Rookies","default":0},"autos":{"type":"integer","title":"Autos","default":0},"relics":{"type":"integer","title":"Relics","default":0},"numbered":{"type":"integer","title":"Numbered","default":0},"one_of_ones":{"type":"integer","title":"One Of Ones","default":0},"graded_count":{"type":"integer","title":"Graded Count","default":0},"raw_count":{"type":"integer","title":"Raw Count","default":0},"break_stats":{"items":{"$ref":"#/components/schemas/BreakStat"},"type":"array","title":"Break Stats"},"lot_stats":{"items":{"$ref":"#/components/schemas/LotStat"},"type":"array","title":"Lot Stats"},"portfolio_value":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Portfolio Value","description":"Sum of FMV across all priced copies"},"total_unrealized_gain_loss":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Total Unrealized Gain Loss","description":"portfolio_value - total_cost_basis"},"portfolio_roi":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Portfolio Roi","description":"(portfolio_value - total_cost_basis) / total_cost_basis"},"priced_coverage":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Priced Coverage","description":"Fraction of copies with FMV data (0.0 - 1.0)"},"portfolio_change_7d":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Portfolio Change 7D","description":"Appraisal drift: the portfolio's appraised value vs 7 days ago, over the SAME holdings on both ends — buying a card doesn't move it. Each copy's FMV is a 90-day rolling median, so this lags the live market — it is not the week's sales activity. (The portfolio series is as-of instead: it steps up when you buy.)"},"portfolio_series":{"items":{"$ref":"#/components/schemas/PortfolioPoint"},"type":"array","title":"Portfolio Series","description":"Compact recent portfolio-value trend (same point shape AND same as-of semantics as /portfolio/history — each point values only the copies owned that day), embedded so a dashboard sparkline/chart needs no second call. Empty when the collection has no price history yet."},"top_sets":{"items":{"$ref":"#/components/schemas/LabeledCount"},"type":"array","title":"Top Sets","description":"Sets you own the most cards from (label = set name, count = copies owned), ranked descending. The collection's counterpart to the catalog's biggest_sets."},"most_valuable":{"items":{"$ref":"#/components/schemas/HighlightCard"},"type":"array","title":"Most Valuable","description":"Your highest-value copies by current FMV, each valued at its own grade (a graded copy at that grade's price, a raw copy at the ungraded price). Only priced copies appear."},"rarest":{"items":{"$ref":"#/components/schemas/HighlightCard"},"type":"array","title":"Rarest"},"glossary":{"additionalProperties":{"$ref":"#/components/schemas/MetricInfo"},"type":"object","title":"Glossary","description":"Plain-language explanations of this payload's metrics, keyed by namespaced id (dashboard.*) — embedded so a consumer renders correct wording with no extra call."}},"type":"object","required":["collector"],"title":"DashboardStats","description":"A collection overview. Deliberately lean — what you have, what it cost, what's in it, your\nbreaks and purchases, and your best cards. Distributions grow back as they earn their place."},"ErrorResponse":{"properties":{"detail":{"type":"string","title":"Detail","description":"Human-readable explanation of why the request failed."}},"type":"object","required":["detail"],"title":"ErrorResponse","description":"A failed request. Branch on the status code; show ``detail`` to a human.\n\nCarries no model-level example on purpose: one model backs 401, 403, 404, and 409, so a single\nexample would be printed under all four headings and be wrong under three of them. The API\nattaches a per-status example instead (see ``slab_api.openapi._error``)."},"FacetCount":{"properties":{"value":{"type":"string","title":"Value"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["value","count"],"title":"FacetCount","description":"A single value + how many results match it within the current filter set."},"Facets":{"properties":{"brand":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Brand"},"year":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Year"},"subset":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Subset"},"finish":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Finish"},"team":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Team"},"attribute":{"anyOf":[{"items":{"$ref":"#/components/schemas/FacetCount"},"type":"array"},{"type":"null"}],"title":"Attribute"}},"type":"object","title":"Facets","description":"Drill-down facet counts, scoped to the current filter set. Only requested dimensions are\npopulated; the rest are None."},"FmvSummary":{"properties":{"fair_market_value":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Fair Market Value","description":"Trimmed-median sale price for this printing"},"grade_key":{"type":"string","title":"Grade Key","description":"Grade bucket this FMV is for, e.g. \"RAW\", \"PSA-10\""},"low_confidence":{"type":"boolean","title":"Low Confidence","description":"True when the estimate is below the min-comps threshold","default":false},"sample_size":{"type":"integer","title":"Sample Size","description":"Comps behind this estimate","default":0},"as_of_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"As Of Date","description":"Snapshot date of this value"}},"type":"object","required":["fair_market_value","grade_key"],"title":"FmvSummary","description":"Slim headline market value for one catalog row — the RAW (ungraded) fair market value for\nthis exact printing, or the best-sampled available grade if RAW is unpriced.\n\nDeliberately lightweight (one number + context): the full per-finish/per-grade breakdown, trends,\nand comps live on ``GET /cards/{uuid}/market``. Only populated when a card search is run with\n``include_market=true``."},"Grade":{"type":"string","enum":["GEM-MT","MINT","NM-MT","NM","EX-MT","EX","VG-EX","VG","GD","PR"],"title":"Grade","description":"The shared 1-10 condition ladder. A collector uses it to *self-assess* a raw card, and the\nsame rungs are the human-readable names a professional numeric grade maps onto — so a\nself-grade and a pro grade sit on one comparable axis (see `grade_delta`). Values are the hobby\nabbreviations; `GRADE_ORDINAL` gives each rung its 1-10 number (10 = best)."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HighlightCard":{"properties":{"uuid":{"type":"string","title":"Uuid"},"card_number":{"type":"string","title":"Card Number"},"subjects":{"items":{"type":"string"},"type":"array","title":"Subjects"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"subset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subset"},"finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish"},"print_run":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Print Run"},"grade_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade Key","description":"The grade bucket this copy is valued at (e.g. 'PSA-10', 'RAW') — a graded copy's FMV is grade-specific, so the number is meaningless without it."},"cost_basis":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Cost Basis"},"fair_market_value":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Fair Market Value"},"unrealized_gain_loss":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Unrealized Gain Loss"}},"type":"object","required":["uuid","card_number"],"title":"HighlightCard","description":"A card surfaced in a highlight list (most valuable, rarest, recent)."},"HotPlayer":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Subject UUID"},"name":{"type":"string","title":"Name"},"sales_30d":{"type":"integer","title":"Sales 30D","description":"Matched sales of this player's cards in the last 30 days"},"sales_prev_30d":{"type":"integer","title":"Sales Prev 30D","description":"Same count for the 30 days before that (momentum reference)"},"dollar_volume_30d":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Dollar Volume 30D","description":"Sum of those sales' prices — separates heat from churn"},"distinct_cards_30d":{"type":"integer","title":"Distinct Cards 30D","description":"How many different cards traded (breadth of demand)"},"price_trend_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Price Trend Pct","description":"Price direction from actual sales, percent: per card, the median ungraded sale price over the last 30 days vs the prior 30, then the median across the player's cards; None when no card has sales in both windows"}},"type":"object","required":["uuid","name","sales_30d","sales_prev_30d","dollar_volume_30d","distinct_cards_30d"],"title":"HotPlayer","description":"One entry in the hottest-players ranking — market ACTIVITY, not value or appreciation.\n\nRanked by matched sales in the trailing 30 days (real sale dates) — hockey cards trade too\nthin for a 7-day window to mean much. Guards keep it honest: at least 3 sales across at\nleast 2 distinct cards, so one card churning or a two-sale blip can't chart.\n``sales_prev_30d`` is the momentum reference (this 30 days vs the prior 30), and\n``price_trend_pct`` shows direction separately — a player being sold off at falling prices\nstill ranks hot, and the arrow is how a reader tells the difference."},"LabeledCount":{"properties":{"label":{"type":"string","title":"Label"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["label","count"],"title":"LabeledCount","description":"One bucket in a distribution (e.g. a team and how many cards are from it)."},"LotCreate":{"properties":{"total_cost":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"}],"title":"Total Cost","description":"Total amount paid for the whole purchase"},"lot_date":{"type":"string","format":"date","title":"Lot Date","description":"Date the purchase was made (required)"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source","description":"Where bought, e.g. \"eBay\", a seller, \"card show\""},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["total_cost","lot_date"],"title":"LotCreate","description":"Record a purchase lot — one price paid for one-or-more cards. The sibling of a break: its\ntotal_cost splits across the cards attached to it, but it is tied to no set (its cards can span\nany number of products).","examples":[{"lot_date":"2026-07-02","notes":"40-card rookie lot","source":"eBay","total_cost":"120.00"}]},"LotOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Lot UUID"},"collector_uuid":{"type":"string","title":"Collector Uuid","description":"Collector UUID"},"total_cost":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Cost"},"lot_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Lot Date"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"copy_count":{"type":"integer","title":"Copy Count","description":"Number of card copies attached to this lot"},"cost_per_card":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Cost Per Card","description":"total_cost / copy_count (None when copy_count is 0)"}},"type":"object","required":["uuid","collector_uuid","total_cost","copy_count"],"title":"LotOut"},"LotSearchQuery":{"properties":{"lot_after":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Lot After","description":"Lot date >= this"},"lot_before":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Lot Before","description":"Lot date <= this"},"sort":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort","description":"Sort key, prefix with - for descending. Options: lot_date, total_cost, cost_per_card"},"limit":{"type":"integer","maximum":200.0,"minimum":1.0,"title":"Limit","default":50},"offset":{"type":"integer","minimum":0.0,"title":"Offset","default":0}},"type":"object","title":"LotSearchQuery","description":"Filter and browse purchase lots."},"LotSearchResult":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/LotOut"},"type":"array","title":"Items"}},"type":"object","required":["total","limit","offset"],"title":"LotSearchResult"},"LotStat":{"properties":{"lot_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Lot Date"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"total_cost":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Total Cost"},"cards":{"type":"integer","title":"Cards"},"cost_per_card":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Cost Per Card"}},"type":"object","required":["total_cost","cards"],"title":"LotStat","description":"A purchase (a single multi-card buy) and how many cards were attached to it."},"LotUpdate":{"properties":{"total_cost":{"anyOf":[{"type":"number","minimum":0.0},{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Total Cost"},"lot_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Lot Date"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"LotUpdate","description":"Partial update — only set fields are applied."},"MarketValue":{"properties":{"fair_market_value":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Fair Market Value","description":"FMV: price_median for this copy's price key"},"price_low":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Low"},"price_high":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price High"},"sample_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Size"},"low_confidence":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Low Confidence"},"as_of_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"As Of Date"},"unrealized_gain_loss":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Unrealized Gain Loss","description":"FMV - cost_basis (paper P&L)"},"roi":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Roi","description":"(FMV - cost_basis) / cost_basis as decimal (0.25 = 25%)"},"holding_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Holding Days","description":"Days since acquired_date"},"grading_uplift":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Grading Uplift","description":"FMV(this grade) - FMV(RAW) for same card — implied value added by grading"},"grading_cost":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Grading Cost","description":"Sum of grading-category costs for this copy"},"grading_roi":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Grading Roi","description":"grading_uplift / grading_cost — return on the grading investment"},"price_change_7d":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Change 7D","description":"FMV appraisal drift vs 7 days ago (current - prior)"},"price_change_30d":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Change 30D","description":"FMV appraisal drift vs 30 days ago (current - prior)"}},"type":"object","title":"MarketValue","description":"Per-copy market data, embedded on CardCopyOut. All fields computed server-side from the\nlatest PricePoint matching this copy's price key (card, finish, grade_key)."},"MatchMode":{"type":"string","enum":["any_printing","exact","exact_serial"],"title":"MatchMode"},"MeOut":{"properties":{"account":{"$ref":"#/components/schemas/AccountOut"},"collectors":{"items":{"$ref":"#/components/schemas/CollectorOut"},"type":"array","title":"Collectors"},"default_collector_uuid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Default Collector Uuid"}},"type":"object","required":["account","collectors"],"title":"MeOut","description":"The portal/CLI bootstrap view: who am I, which collectors do I own, what's my default."},"MetricInfo":{"properties":{"label":{"type":"string","title":"Label","description":"Human title for the metric, e.g. 'Most Valuable — Graded'"},"summary":{"type":"string","title":"Summary","description":"One-line description shown under the metric's title"},"detail":{"type":"string","title":"Detail","description":"Longer plain-language explanation of the metric's nuance"}},"type":"object","required":["label","summary","detail"],"title":"MetricInfo","description":"A consumer-facing explanation of one metric — plain language, no developer jargon.\n\n``summary`` is the one-liner shown under a title; ``detail`` is the longer \"info button\" text\nthat explains the nuance (how it's computed, what it does and doesn't mean)."},"PortfolioHistory":{"properties":{"collector_uuid":{"type":"string","title":"Collector Uuid"},"start_date":{"type":"string","format":"date","title":"Start Date"},"end_date":{"type":"string","format":"date","title":"End Date"},"interval":{"$ref":"#/components/schemas/TimeInterval","default":"daily"},"points":{"items":{"$ref":"#/components/schemas/PortfolioPoint"},"type":"array","title":"Points"}},"type":"object","required":["collector_uuid","start_date","end_date"],"title":"PortfolioHistory","description":"Portfolio value over time — the time series behind the dashboard chart."},"PortfolioPoint":{"properties":{"date":{"type":"string","format":"date","title":"Date"},"portfolio_value":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Portfolio Value","description":"Sum of FMV across the priced copies OWNED on this date (a card counts only from its acquisition date, even where its price history runs earlier)"},"cost_basis":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Cost Basis","description":"What the copies owned on this date cost — steps up with each acquisition; null when the collection has no cost data"},"priced_copies":{"type":"integer","title":"Priced Copies","description":"How many of the copies owned on this date had pricing","default":0},"total_copies":{"type":"integer","title":"Total Copies","description":"Copies owned on this date","default":0}},"type":"object","required":["date","portfolio_value"],"title":"PortfolioPoint","description":"One data point in a portfolio value time series — an AS-OF view: every field describes the\ncopies owned on this date, not today's collection. Copies bought later are absent, so the\nseries steps up when you buy rather than rewriting the past."},"PortfolioSummary":{"properties":{"total_acquisition_cost":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Total Acquisition Cost","description":"Sum of acquisition costs across all matching copies"},"total_additional_costs":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Total Additional Costs","description":"Sum of post-acquisition expenses across all matching copies"},"total_cost_basis":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Total Cost Basis","description":"Sum of cost basis across all matching copies"},"portfolio_value":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Portfolio Value","description":"Sum of FMV across all copies with pricing"},"total_unrealized_gain_loss":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Total Unrealized Gain Loss","description":"portfolio_value - total_cost_basis"},"portfolio_roi":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Portfolio Roi","description":"(portfolio_value - total_cost_basis) / total_cost_basis as decimal"},"priced_copies":{"type":"integer","title":"Priced Copies","description":"Number of copies that have FMV data","default":0},"unpriced_copies":{"type":"integer","title":"Unpriced Copies","description":"Number of copies without market data","default":0}},"type":"object","title":"PortfolioSummary","description":"Collection-level financials extended with market data. Backward-compatible: all\nCollectionSummary fields remain, and the new market fields are additive.\n\nLives here (not in pricing.py) so CollectionResult.summary can be typed as this subclass —\notherwise FastAPI's response_model serialization coerces it down to the CollectionSummary base\nand silently drops every market field. Re-exported from pricing.py for its original import path."},"PricePointOut":{"properties":{"grade_key":{"type":"string","title":"Grade Key","description":"Grade bucket: \"RAW\", \"PSA-10\", \"BGS-9.5\""},"finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish","description":"Parallel finish name; None = base"},"as_of_date":{"type":"string","format":"date","title":"As Of Date"},"window_days":{"type":"integer","title":"Window Days","default":90},"sample_size":{"type":"integer","title":"Sample Size","description":"Number of comps used in this estimate"},"price_median":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Price Median"},"price_low":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Low"},"price_high":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price High"},"low_confidence":{"type":"boolean","title":"Low Confidence","description":"True when sample_size is below the min-comps threshold","default":false},"method":{"type":"string","title":"Method","default":"trimmed_median"},"currency":{"type":"string","title":"Currency","default":"USD"},"price_change_7d":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Change 7D","description":"FMV appraisal drift vs 7 days ago (current - prior). FMV is a 90-day rolling trimmed median, so this moves slowly and lags the live market — it tracks the appraisal, not the week's sales"},"price_change_30d":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Change 30D","description":"FMV appraisal drift vs 30 days ago (current - prior). Same smoothed-appraisal semantics as price_change_7d — not a window-of-sales comparison"}},"type":"object","required":["grade_key","as_of_date","sample_size","price_median"],"title":"PricePointOut","description":"One price-point snapshot for a specific grade bucket and finish."},"RarestOwnedCard":{"properties":{"card":{"$ref":"#/components/schemas/CommunityCard"},"collector_count":{"type":"integer","title":"Collector Count","description":"Distinct collectors who own a copy"}},"type":"object","required":["card","collector_count"],"title":"RarestOwnedCard","description":"One entry in the rarest-owned ranking — a low-print-run card actually held in a collection."},"SealedCompOut":{"properties":{"sold_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Sold Date","description":"When the sale closed (may be absent)"},"sale_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Sale Price"},"currency":{"type":"string","title":"Currency","default":"USD"},"marketplace":{"type":"string","title":"Marketplace","description":"Where the sale happened, e.g. ebay"},"sale_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sale Type","description":"auction | fixedprice | bestoffer"},"title":{"type":"string","title":"Title","description":"Seller's listing title"},"match_status":{"type":"string","title":"Match Status","description":"matched | ambiguous | unmatched"}},"type":"object","required":["marketplace","title","match_status"],"title":"SealedCompOut","description":"One observed sealed sale — the raw market evidence, trimmed for human eyeballing."},"SealedFormat":{"type":"string","enum":["hobby_box","hobby_case","retail_box","retail_case","blaster_box","mega_box","tin","hobby_pack","retail_pack","hanger","fat_pack","starter"],"title":"SealedFormat","description":"The purchasable sealed configurations of a set (SealedProduct.format). Closed, code-owned\nvocabulary — a format is an app-validated string in the DB, not a lookup table.\n\nThe seeder mirrors this list in `seeder/sealed_formats.py` (it stays decoupled from this\npackage); add a new format in both places."},"SealedMarket":{"properties":{"product":{"$ref":"#/components/schemas/SealedProductOut"},"as_of_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"As Of Date","description":"Most recent snapshot date across the price points"},"price_points":{"items":{"$ref":"#/components/schemas/SealedPricePointOut"},"type":"array","title":"Price Points"},"total_comps":{"type":"integer","title":"Total Comps","description":"Total comps matched to this product","default":0},"comps":{"items":{"$ref":"#/components/schemas/SealedCompOut"},"type":"array","title":"Comps"}},"type":"object","required":["product"],"title":"SealedMarket","description":"Current pricing + recent comps for one sealed product.\n\nReturned by ``GET /sealed/{uuid}/market`` — the 'what is this box worth?' view."},"SealedPriceHistory":{"properties":{"product_uuid":{"type":"string","title":"Product Uuid"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"format":{"$ref":"#/components/schemas/SealedFormat"},"start_date":{"type":"string","format":"date","title":"Start Date"},"end_date":{"type":"string","format":"date","title":"End Date"},"interval":{"$ref":"#/components/schemas/TimeInterval","default":"daily"},"points":{"items":{"$ref":"#/components/schemas/CardPricePoint"},"type":"array","title":"Points"}},"type":"object","required":["product_uuid","format","start_date","end_date"],"title":"SealedPriceHistory","description":"Price history for one sealed product — the time series behind charts.\n\nPoints reuse ``CardPricePoint`` (date + median/low/high + sample size): the snapshot shape\nis identical, only the key differs."},"SealedPricePointOut":{"properties":{"as_of_date":{"type":"string","format":"date","title":"As Of Date"},"window_days":{"type":"integer","title":"Window Days","default":90},"sample_size":{"type":"integer","title":"Sample Size","description":"Number of comps used in this estimate"},"price_median":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Price Median"},"price_low":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Low"},"price_high":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price High"},"low_confidence":{"type":"boolean","title":"Low Confidence","description":"True when sample_size is below the min-comps threshold","default":false},"method":{"type":"string","title":"Method","default":"trimmed_median"},"currency":{"type":"string","title":"Currency","default":"USD"},"price_change_7d":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Change 7D","description":"FMV appraisal drift vs 7 days ago (current - prior). FMV is a 90-day rolling trimmed median, so this moves slowly and lags the live market — it tracks the appraisal, not the week's sales"},"price_change_30d":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Change 30D","description":"FMV appraisal drift vs 30 days ago (current - prior). Same smoothed-appraisal semantics as price_change_7d — not a window-of-sales comparison"}},"type":"object","required":["as_of_date","sample_size","price_median"],"title":"SealedPricePointOut","description":"One sealed price-point snapshot — a PricePointOut without the grade/finish axes."},"SealedProductOut":{"properties":{"uuid":{"type":"string","title":"Uuid"},"set_uuid":{"type":"string","title":"Set Uuid"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"format":{"$ref":"#/components/schemas/SealedFormat"},"cards_per_pack":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cards Per Pack"},"packs_per_box":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Packs Per Box"},"boxes_per_case":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Boxes Per Case"},"msrp":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Msrp"},"configuration":{"additionalProperties":true,"type":"object","title":"Configuration"},"price_median":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price Median"},"as_of_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"As Of Date"},"sample_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Size"},"low_confidence":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Low Confidence"}},"type":"object","required":["uuid","set_uuid","format"],"title":"SealedProductOut","description":"One sealed SKU of a set, e.g. the hobby box of 2024-25 Upper Deck Series 1.\n\nReturned by ``GET /sets/{uuid}/sealed`` (with a headline price when one exists)."},"SetOut":{"properties":{"uuid":{"type":"string","title":"Uuid","description":"Set UUID"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"slug":{"type":"string","title":"Slug"},"brand":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brand"},"season":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Season"},"year":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year"},"sport":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sport"},"league":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"League"},"card_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Card Count"},"priced_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priced Count","description":"Printings (base + parallels) with at least one recorded sale in the past 90 days — the cards that currently carry a market value"},"sales_90d":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sales 90D","description":"Recorded sales across the whole set in the past 90 days — market activity"},"box_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Box Price","description":"Latest hobby-box market value (median of recent sealed sales); None when the set's sealed products aren't catalogued or priced yet"}},"type":"object","required":["uuid","slug"],"title":"SetOut"},"SetSearchQuery":{"properties":{"q":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q","description":"Set name (substring match)"},"brand":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Brand","description":"Brand name(s) — any-of"},"year":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Year"},"sport":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sport"},"limit":{"type":"integer","maximum":200.0,"minimum":1.0,"title":"Limit","default":50},"offset":{"type":"integer","minimum":0.0,"title":"Offset","default":0}},"type":"object","title":"SetSearchQuery","description":"Browse/find products (so you can then drill into a set's cards).","examples":[{"q":"Upper Deck Series 1","year":2025},{"brand":["O-Pee-Chee"],"limit":25,"sport":"Hockey"}]},"SetSearchResult":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/SetOut"},"type":"array","title":"Items"}},"type":"object","required":["total","limit","offset"],"title":"SetSearchResult"},"SetTopCard":{"properties":{"card_uuid":{"type":"string","title":"Card Uuid"},"card_number":{"type":"string","title":"Card Number"},"subjects":{"items":{"type":"string"},"type":"array","title":"Subjects"},"subset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subset"},"finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finish","description":"The printing's finish; None = base"},"print_run":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Print Run"},"market":{"$ref":"#/components/schemas/FmvSummary"}},"type":"object","required":["card_uuid","card_number","market"],"title":"SetTopCard","description":"One entry in a set's most-valuable ranking — a printing plus its headline FMV."},"SetTopCards":{"properties":{"set_uuid":{"type":"string","title":"Set Uuid"},"set_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Set Name"},"season":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Season"},"total_priced":{"type":"integer","title":"Total Priced","description":"Printings in this set with any pricing","default":0},"cards":{"items":{"$ref":"#/components/schemas/SetTopCard"},"type":"array","title":"Cards"}},"type":"object","required":["set_uuid"],"title":"SetTopCards","description":"The most expensive cards of one set, by headline FMV (RAW preferred, else the\nbest-sampled grade — same convention as card search's ``include_market``).\n\nReturned by ``GET /sets/{uuid}/top-cards``. ``total_priced`` is how many of the set's\nprintings have any market data at all — context for how deep the ranking is."},"SubjectOut":{"properties":{"name":{"type":"string","title":"Name"},"team":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Team"},"subject_type":{"type":"string","title":"Subject Type","description":"What kind of thing this subject is — values from SubjectType: \"person\" (default) or \"team\" (a team-as-subject card, e.g. a franchise insert)","default":"person"}},"type":"object","required":["name"],"title":"SubjectOut"},"TickerItem":{"properties":{"kind":{"type":"string","title":"Kind","description":"Category: 'catalog' | 'sale' | 'collection_stat' | 'collection_event'"},"icon":{"type":"string","title":"Icon","description":"Leading emoji"},"text":{"type":"string","title":"Text","description":"Pre-composed display line (server-owned, all consumers match)"}},"type":"object","required":["kind","icon","text"],"title":"TickerItem","description":"One item in the community ticker — a short, pre-composed line about something happening\nacross the catalog or collections.\n\n``text`` is composed server-side (like the glossary) so the portal marquee and any API consumer\nshow identical wording. Collection items are always ANONYMIZED — an event never names a\ncollector, and personal figures (what someone paid, their realized P&L) never appear here; the\nonly sale shown is the public marketplace ``sale`` highlight."},"TimeInterval":{"type":"string","enum":["daily","weekly","monthly"],"title":"TimeInterval"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"ValuableCard":{"properties":{"card":{"$ref":"#/components/schemas/CommunityCard"},"market":{"$ref":"#/components/schemas/FmvSummary"}},"type":"object","required":["card","market"],"title":"ValuableCard","description":"One entry in a catalog-wide most-valuable ranking — a printing plus the FMV it's ranked by.\n\nThe board it sits on decides which value ``market`` carries: the RAW (ungraded) FMV on the raw\nboard, or the printing's best-sampled GRADED FMV on the graded board (``market.grade_key`` names\nwhich grade, e.g. ``PSA-10``)."},"Visibility":{"type":"string","enum":["public","private"],"title":"Visibility"},"VocabOut":{"properties":{"copy_statuses":{"items":{"type":"string"},"type":"array","title":"Copy Statuses","description":"CardCopy.status values"},"acquisition_types":{"items":{"type":"string"},"type":"array","title":"Acquisition Types","description":"How a copy was acquired"},"break_types":{"items":{"type":"string"},"type":"array","title":"Break Types","description":"Break.break_type values — every sealed format plus the generic pack/box/case/other fallbacks"},"cost_categories":{"items":{"type":"string"},"type":"array","title":"Cost Categories","description":"Post-acquisition cost categories"},"sealed_formats":{"items":{"type":"string"},"type":"array","title":"Sealed Formats","description":"Sealed product formats"},"grades":{"items":{"type":"string"},"type":"array","title":"Grades","description":"The shared 1-10 condition ladder, best first"},"match_modes":{"items":{"type":"string"},"type":"array","title":"Match Modes","description":"Custom-set card match modes"},"custom_set_types":{"items":{"type":"string"},"type":"array","title":"Custom Set Types","description":"Custom-set kinds"},"visibilities":{"items":{"type":"string"},"type":"array","title":"Visibilities","description":"Custom-set visibility values"},"subject_types":{"items":{"type":"string"},"type":"array","title":"Subject Types","description":"Subject kinds (person, team)"},"card_sort_keys":{"items":{"type":"string"},"type":"array","title":"Card Sort Keys","description":"POST /cards/search sort keys (prefix - for desc)"},"collection_sort_keys":{"items":{"type":"string"},"type":"array","title":"Collection Sort Keys","description":"Collection search sort keys, including computed fmv/unrealized/roi"},"attributes":{"items":{"type":"string"},"type":"array","title":"Attributes","description":"Every card attribute in the catalog today"},"grading_companies":{"items":{"type":"string"},"type":"array","title":"Grading Companies","description":"Every grading company in the catalog today"}},"type":"object","required":["copy_statuses","acquisition_types","break_types","cost_categories","sealed_formats","grades","match_modes","custom_set_types","visibilities","subject_types","card_sort_keys","collection_sort_keys","attributes","grading_companies"],"title":"VocabOut","description":"Every enumerable value the API accepts or serves, in one public payload.\n\nTwo kinds of list, deliberately mixed: *wire enums* (fixed by the contract — a new value means\na schemas release) and *catalog dimensions* (rows that grow as sets are seeded — attributes,\ngrading companies). Consumers should not care which is which; both are \"the values that exist\nright now\"."}},"securitySchemes":{"ClerkJWT":{"type":"http","description":"Management-plane credential: a Clerk session JWT, issued to a signed-in human by the web portal. An API key will NOT work on these routes, and they are not intended for programmatic use — build against the data plane (`x-api-key`) instead.","scheme":"bearer"},"ApiKeyAuth":{"type":"apiKey","description":"Data-plane credential. Mint one in the portal under Account → API keys; the raw value is shown once and never again. It identifies the ACCOUNT (tenant) making the call — the acting collector is named separately, in the path or body, and checked against it.","in":"header","name":"x-api-key"}}},"tags":[{"name":"meta","description":"Liveness plus the two self-describing endpoints — **`/vocab`** (every enumerable value and sort key, including catalog dimensions that grow over time) and **`/glossary`** (plain-language text for every metric). Public, cacheable, and the reason a client never needs to hardcode a value list or invent its own wording for a number."},{"name":"stats","description":"Totals for the whole catalog — how many sets, cards, and printings exist. Public. Use it for a landing-page counter or to confirm a seed run landed. (`GET /community` returns this same block plus leaderboards, so don't call both.)"},{"name":"community","description":"The entire public community picture in **one** payload: catalog totals, a pre-composed activity ticker, leaderboards (most valuable raw and graded, most collected, rarest owned), popular custom sets, and the glossary text for each metric. Built so a client can render the whole board with no follow-up calls.\n\n**Everything here is aggregate and anonymized.** No collector identity and no personal purchase price ever appears — a ticker line reports *that* a card was added, never by whom or for what."},{"name":"cards","description":"The catalog: search it, then drill into one card's rainbow, market, comps, and price history.\n\n`POST /cards/search` is the workhorse and carries the full filter grammar (player, team, set, year range, attributes, finish, scarcity) plus optional facet counts. The per-card endpoints answer the follow-ups: **`/parallels`** for every printing of the same slot, **`/market`** for value across finishes and grades, **`/comps`** for the actual sales behind that value, **`/price-history`** for the series behind a chart.\n\nRead-only — cards are seeded offline."},{"name":"sets","description":"Find a product, then drill in. `POST /sets/search` browses releases; from a set you can reach its **sealed SKUs** (box, case, blaster — each with a current market value) and its **top cards** (the most expensive printings, i.e. what's actually driving box price). Scope a card search to a set with the `set_slug` filter."},{"name":"sealed","description":"What an unopened box or case is worth. Same comps pipeline as cards, minus the grade axis — a sealed SKU is priced as one thing. Browse a set's SKUs via `GET /sets/{set_uuid}/sealed`, then use these for the market view and price history.\n\nPrices cover **factory-sealed** product only; break spots and lots are filtered out during matching, since they'd otherwise swamp the real sales."},{"name":"collection","description":"Your cards, and what they cost you — the write side of the API.\n\nA **copy** is one physical card you own, pointing at a catalog card and carrying what the catalog can't: serial number, grade (professional and your own), acquisition, storage, status. A **break** or a **lot** is a multi-card purchase whose cost is split across the copies pulled from it. **Costs** are what you spent afterward (grading, shipping), and they roll into cost basis automatically.\n\nReads answer the money questions: **collection search** (the catalog grammar plus status/grade/cost filters and a financial summary), **dashboard** (aggregates), **portfolio history** (value over time).\n\nEvery route names the acting collector in the path and verifies it belongs to your account — a collector that isn't yours returns `404`."},{"name":"custom-sets","description":"**Chase sets** — the set *you* decide to complete, when no manufacturer made it: every Bedard rookie, one auto per team, a personal top-100.\n\nCurate one card at a time, or define it by a rule that matches the catalog automatically. Either way slab tracks completion against your collection. Sets can be private or public; public ones are discoverable via search, subscribable by other collectors, and ranked in `GET /custom-sets/popular` (the one route here needing no key)."},{"name":"account","description":"**Management plane — the web portal's surface, not a programmable one.** These routes authenticate a human via a Clerk session JWT (`Authorization: Bearer`), so an API key will not open them; they exist so the portal can mint and revoke keys.\n\nOne exception is built for you: **`GET /account`** takes your API key and returns the account behind it, its collectors, and a default — how a client resolves a collector when the user has configured nothing but a key.\n\nA minted key's raw value is returned **once** and is unrecoverable afterward; only its hash is stored."}]}