{"openapi":"3.1.0","info":{"title":"crate-api","version":"1.10.0","description":"# crate API\n\ncrate is the **aggregation gateway** for a fleet of music-data producers. You ask it about an artist, a release, a label, or a festival; it joins every signal the fleet can see about that entity and hands you back one composed picture — collector behavior, editorial coverage, live-circuit demand, breakout momentum, web presence — each piece labelled with where it came from and how fresh it is.\n\nTwo things make crate different from a typical catalogue API, and the rest of this doc exists to teach them:\n\n1. **Identity is a `cluster_id`, not a Discogs/MusicBrainz/Bandcamp id.** The same artist scattered across those platforms collapses to one canonical key. You resolve to it once, then key everything off it.\n2. **An empty answer is a first-class answer.** crate returns HTTP `200` and tells you plainly what it *can't* see (`present:false`, a null field, `state:\"honest_gap\"`) instead of `404`-ing or fabricating data. Only `4xx`/`5xx` are errors.\n\nIf you do nothing else, internalize the cold-start recipe: **resolve a name or pasted link → `cluster_id` → dossier.** `GET /api/v1` returns this recipe live, and `GET /api/v1/resolve?q=<name>` is the front door.\n\n## Authentication\n\nThe API is keyed: every operation requires an `X-API-Key` header (`ck_(live|test)_<…>` format) **except** the two public front-door endpoints that opt out — `GET /api/v1` (the root index) and `GET /api/v1/openapi.json` (this spec). One family of endpoints uses a *different* credential entirely — see **beacons** below.\n\n## Concepts\n\n### `cluster_id` — the canonical artist identity\n\n`cluster_id` is crate's prime key for an artist: a `pe-norm-v1` hex string derived from the artist's name, designed so that the same artist's Discogs page, MusicBrainz entry, and Bandcamp profile all **collapse to one `cluster_id`**. Key *all* artist data off it.\n\n- It is an **opaque string** — pass it through verbatim. Never numericize it, parse it, or assume structure.\n- `cluster_id: null` is an **honest gap**, not an error: crate couldn't resolve a canonical identity for that lookup. Roughly half of the long-tail booking artists have neither a Discogs id nor an MBID, so `null` is normal.\n- You get one by calling `GET /api/v1/resolve` (from a name, a pasted link, or a foreign id). You then address the artist dossier directly as `GET /api/v1/artist/{key}`, where `{key}` is the 64-hex `cluster_id` **or** a human slug.\n- A 64-hex key resolves identity *directly* from the cluster — it deliberately skips the Discogs lookup so a hex address never silently re-anchors onto a same-name Discogs row. Foreign locators (`discogs:<id>`, `mbid:<uuid>`) are **not** canonical addresses: convert them via `/resolve` first, or `/artist/{key}` returns `400`.\n\n### dossier · grains — the composed per-entity picture\n\nA **dossier** is the full picture crate composes for one entity by joining every fleet signal. There are four **grains**:\n\n| grain | addressed by | what it is |\n|---|---|---|\n| `artist` | `GET /api/v1/artist/{key}` or `GET /api/v1/dossier/artist/{slug}` | identity + collector behavior + editorial + emergence + live presence + web + compositions… |\n| `master` | `GET /api/v1/dossier/master/{id}` | a Discogs **release-group** — what a consumer calls a \"release\"; keyed by integer `master_id` |\n| `label` | `GET /api/v1/dossier/label/{slug}` | label identity + sublabel→parent lineage + collector behavior |\n| `festival` | `GET /api/v1/dossier/festival/{slug}` | de-fragmented festival identity + consolidated editions ⋈ lineup |\n\nEvery dossier facet carries a classified **`state`** (e.g. `present`, `empty`/absent, `honest_gap`) and the dossier ships a **provenance manifest** — an array where each entry names the `producer`, `sourceTable`, `refreshCadence`, `tier`, and `honestGapState` for a field. Read `GET /api/v1/dossier/manifest` (the data dictionary) to discover the entire field surface across grains — including grains that are deliberately *unavailable* (e.g. song, because the fleet has no track key) — without hitting every entity endpoint.\n\n> Note `master` ≠ `masters`: `GET /api/v1/dossier/master/{id}` is the exhaustive dossier; `GET /api/v1/masters/{id}` (and `POST /api/v1/masters/batch`) is the older, lighter *enrichment* shape.\n\n### honest gap — empty is `200`, not `404`\n\nThis is crate's defining principle: **crate shows what it can see and is explicit about what it can't, rather than 404-ing or faking data.** An unresolved or empty lookup returns **HTTP `200`** with one of:\n\n- `present: false` (e.g. `/bandcamp/release` with no match),\n- a `null` field (`cluster_id: null`, `identity: null`),\n- `state: \"honest_gap\"` on a dossier facet.\n\nBranch on the body, not the status, for \"did I get data?\". An unresolved artist slug returns `identity:null` at `200` — never `404`. Reserve your error handling for genuine **`4xx`/`5xx`**.\n\n### `resolved_via` · `resolved_from` — binding tier and match method\n\nWhen crate resolves an identity it tells you two orthogonal things:\n\n- **`resolved_via`** = the *binding tier*, i.e. how trustworthy the identity is:\n  - `'discogs'` — canonical, Discogs-bound (verified).\n  - `'cluster'` — **observed/unverified**: the identity came from the booking graph with no Discogs bind. Surface it flagged as unverified, never as canonical.\n  - `null` — did not resolve.\n- **`resolved_from`** = *how* you addressed it on `/resolve`: `'url'` (a pasted link), `'name'`, or `'locator'` (a foreign id). `matched_on` names the surface that matched; `note` explains a recognized-but-unresolved link (e.g. a Twitter URL crate recognizes but does not yet cross-reference).\n\nA 64-hex `cluster_id` address always yields `resolved_via: 'cluster'` (observed tier) by design.\n\n### the cube · `cube_quadrant` — the behavioral-signal model\n\nThe **cube** is crate's behavioral model of a master: a **3-bit code** (string, e.g. `\"101\"`) placing the release on three independent behavioral axes — **who OWNS it (collector), who PLAYS it (DJ), who WRITES about it (critic)**. Each bit is `0`/`1`, so the eight quadrants run `\"000\"` (\"No signal\") through `\"111\"` (\"Full intersection\"): `\"100\"` = collector-only, `\"010\"` = DJ-only, `\"101\"` = collector + critic, and so on. The collector-vs-DJ split — *who owns it vs who plays it* — is the heart of the model. `cube_quadrant: null` means the master isn't yet classified (an honest gap). The companion counts `owner_count` / `dj_count` / `critic_count` give the magnitude behind the bits, and `link_to_cube` deep-links the master in crate's cube explorer.\n\n### tastemakers · breakouts — discovery surfaces\n\nTwo read-only discovery surfaces, served from offline-published snapshots (no DB checkout) and **fail-soft**: each returns a `state` of `present` / `empty` / `degraded`, where `degraded` is a `200` honest-gap (a read failure never `500`s), and `stale:true` flags a snapshot older than 7 days.\n\n- **tastemakers** (`GET /api/v1/tastemakers`, `…/ones-to-watch`) — influential curators and the richest artist-grain analytics crate has: rank, own-tier, brokerage score, corroborating axes, lead-times, Bandcamp demand. `?limit=` bounds each array (`1..200`).\n- **breakouts** (`GET /api/v1/breakouts`) — emerging artists on the rise (\"ones to watch\"): booking-momentum signal cross-validated against press. `?tier=breakout|rising` and `?corroboration=corroborated|booking_ahead` filter it; `?limit=` is clamped to `200`.\n\n### beacons — search-event telemetry (different credential)\n\n**Beacons** are client-side telemetry about search behavior: `POST /api/v1/search-events/observed` (a result was served from cache) and `…/refined` (the user changed facets). They are **not** authenticated with your `X-API-Key`. Each search response issues a **short-lived per-search JWT** bound to one `search_event_id`; send it as `Authorization: Bearer <token>`, and the token must match the body's `search_event_id`. Bodies are capped at 512 bytes and beacons are idempotent (a duplicate is a `204` no-op). Beacon `400`s carry a Zod *flattened* `details` object (not the array shape of normal validation errors), so they use a distinct error schema.\n\n### Bandcamp ids and links — opaque, and not playable\n\nThe Bandcamp surfaces (`/api/v1/bandcamp`, `/api/v1/bandcamp/{artistKey}`, `/api/v1/bandcamp/release`) carry two gotchas worth stating up front:\n\n- **`bandcamp_item_id`** is a Bandcamp release id — a `bigint` rendered **as a string**. Treat it as opaque; never numericize it (the same discipline as `cluster_id`).\n- **`track_url`** is a Bandcamp track *page* URL, **not** a playable/stream URL. Bandcamp audio streams are tokenized and expiring (and out of ToS bounds), so crate does not store them. The same link-only posture applies to artwork: every artwork item is a hotlink `url` with `rehost:false` — crate never fetches or re-hosts bytes; a Cover Art Archive URL is best-effort and may `404` if no cover exists.\n\n### `next_cursor` — opaque keyset pagination\n\nThe bulk Bandcamp feed (`GET /api/v1/bandcamp?source=…`) paginates by **keyset cursor**. `next_cursor` is an **opaque string** — pass it back verbatim on the next request; never construct or decode it. `next_cursor: null` means the last page. Calling `GET /api/v1/bandcamp` with *no* params returns a self-describing manifest of every available source and how to page it.\n\n### Opaque ids — the universal rule\n\n`cluster_id`, `bandcamp_item_id`, and pagination cursors are **always strings, always opaque**. Round-trip them verbatim; do not parse, increment, numericize, or infer structure from them. This keeps your client correct across id-scheme changes.\n\n### Versioning\n\nThe **API major version lives in the URL path** (`/api/v1`). The spec's `info.version` (currently `1.4.0`) bumps on **every** spec change and is drift-guarded: the document is generated from code, so docs cannot drift from runtime behavior. Operations carry stable `operationId`s for codegen, and keyed `2xx` responses declare `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers (back off on `429` using `retry_after_seconds` / `Retry-After`).","contact":{"name":"crate support","email":"support@crate.hosaka.fm"},"license":{"name":"Commercial — see Terms of Service"},"x-concepts":[{"term":"cluster_id","eli5":"crate's one true name for an artist — a pe-norm-v1 hex string that collapses the same artist's Discogs, MusicBrainz, and Bandcamp identities into a single canonical key. Key all artist data off it. It's an opaque string: pass it through verbatim, never numericize it. cluster_id:null is an honest gap (couldn't resolve), not an error.","see":"IdentityResolution (getResolve, GET /api/v1/resolve); ArtistDossierContract.cluster_id (GET /api/v1/artist/{key})"},{"term":"dossier","eli5":"The full picture crate composes for one entity by joining every fleet signal. Each facet carries a classified state plus a provenance manifest (producer, source table, refresh cadence, tier). Read /dossier/manifest to discover the whole field surface without calling every endpoint.","see":"DossierManifest (GET /api/v1/dossier/manifest); ArtistDossierContract / MasterDossierContract / LabelDossierContract / FestivalDossierContract"},{"term":"grain","eli5":"Which kind of entity a dossier is about: artist, master (a Discogs release-group, i.e. a 'release' in the consumer sense), label, or festival. The dossier manifest also lists deliberately-unavailable grains (e.g. song — no fleet track key).","see":"GET /api/v1/dossier/artist/{slug}, /dossier/master/{id}, /dossier/label/{slug}, /dossier/festival/{slug}; DossierManifest.unavailable_grains"},{"term":"honest gap","eli5":"An unresolved or empty lookup returns HTTP 200 with present:false, a null field, or state:\"honest_gap\" — this is NORMAL, not an error. crate shows what it can see and is explicit about what it can't, rather than 404-ing or faking data. Only 4xx/5xx are errors; branch on the body, not the status.","see":"Error schema (the inverse: only 4xx/5xx); ArtistDossierContract.identity:null; BandcampReleaseResponse present:false variant"},{"term":"resolved_via","eli5":"The binding TIER of a resolved identity: 'discogs' = canonical Discogs-bound (verified); 'cluster' = OBSERVED/unverified from the booking graph (surface flagged-unverified, never as canonical); null = unresolved. A 64-hex cluster_id address is always 'cluster' by design.","see":"IdentityResolution.resolved_via; ArtistDossierContract.resolved_via"},{"term":"resolved_from","eli5":"How you addressed the artist on /resolve: 'url' (pasted link), 'name', or 'locator' (a foreign id). Paired with matched_on (which surface matched) and note (why a recognized link didn't resolve).","see":"IdentityResolution.resolved_from (GET /api/v1/resolve)"},{"term":"cube / cube_quadrant","eli5":"crate's behavioral-signal model: a 3-bit string code (e.g. \"101\") placing a master on three axes — who OWNS it (collector), who PLAYS it (DJ), who WRITES about it (critic). Codes run \"000\" (no signal) to \"111\" (full intersection). null = not yet classified (honest gap). owner_count/dj_count/critic_count give the magnitude.","see":"ResultRow.cube_quadrant + owner_count/dj_count/critic_count; CubeQuadrant schema; MasterDossierContract.header.cube_quadrant"},{"term":"tastemakers","eli5":"A discovery surface of influential curators plus crate's richest artist-grain analytics (rank, own-tier, brokerage score, lead-times, Bandcamp demand). Served from an offline snapshot, fail-soft: state is present/empty/degraded (degraded = 200 honest-gap), stale=true if older than 7 days.","see":"TastemakersResponse (GET /api/v1/tastemakers); OnesToWatchResponse (GET /api/v1/tastemakers/ones-to-watch)"},{"term":"breakouts","eli5":"A discovery surface of emerging artists on the rise ('ones to watch') — booking momentum cross-validated against press. Filter by ?tier=breakout|rising and ?corroboration=corroborated|booking_ahead; ?limit= clamped to 200. Fail-soft state present/empty/degraded.","see":"BreakoutsResponse (GET /api/v1/breakouts)"},{"term":"beacons","eli5":"Client-side search-event telemetry (observed = cache hit, refined = facet change). Authenticated NOT with X-API-Key but with a short-lived per-search JWT bound to one search_event_id, sent as Authorization: Bearer. Bodies <=512 bytes, idempotent (duplicate = 204 no-op).","see":"ObservedBeaconRequest / RefinedBeaconRequest (POST /api/v1/search-events/observed, /refined); BeaconBearerAuth security scheme; BeaconError"},{"term":"bandcamp_item_id","eli5":"A Bandcamp release id — a bigint rendered AS A STRING. Opaque: pass it through verbatim, never numericize it. It's the per-release key you fetch the full dossier with (?item=).","see":"BandcampRelease.bandcamp_item_id; BandcampReleaseSummary; GET /api/v1/bandcamp/release"},{"term":"track_url","eli5":"A Bandcamp track PAGE url — NOT a playable/stream URL. Bandcamp streams are tokenized and expiring (and out of ToS), so crate never stores them. The same link-only posture covers artwork (rehost:false hotlinks crate never re-hosts).","see":"BandcampTrack.track_url; ArtworkItem (rehost:false)"},{"term":"next_cursor","eli5":"An opaque keyset-pagination cursor (string) for the bulk Bandcamp feed. Pass it back verbatim on the next request; never construct or decode it. null = last page. Call /bandcamp with no params for the self-describing source manifest.","see":"BandcampBulkPage.next_cursor (GET /api/v1/bandcamp?source=...)"},{"term":"opaque ids","eli5":"cluster_id, bandcamp_item_id, and pagination cursors are always strings and always opaque — round-trip them verbatim, never parse, increment, or numericize. This keeps clients correct across id-scheme changes.","see":"IdentityResolution.cluster_id; BandcampRelease.bandcamp_item_id; BandcampBulkPage.next_cursor"},{"term":"versioning","eli5":"The API major version is the URL path (/api/v1). The spec's info.version (1.4.0) bumps on every spec change and is drift-guarded because the document is generated from code. Operations carry stable operationIds; keyed 2xx responses declare X-RateLimit-* headers.","see":"info.version; GET /api/v1/openapi.json; RATE_LIMIT_RESPONSE_HEADERS on keyed operations"}]},"servers":[{"url":"https://crate.hosaka.fm","description":"Production"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Customer API key in `ck_(live|test)_<32-base62>` format"},"BeaconBearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Short-lived per-search beacon JWT (issued with the search response), sent as `Authorization: Bearer <token>`. Distinct from the X-API-Key customer key; the token is bound to a single search_event_id."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"Machine-readable error code (stable lowercase snake_case). The ONLY field guaranteed on every error body — switch on it programmatically, never on HTTP status alone (several codes share a status). See the code table in this schema's description.","example":"invalid_query"},"message":{"type":"string","description":"One-sentence human-readable statement of WHAT is wrong (developer-facing). Present only for catalogued codes. Describes the violated rule — not a fix (see hint/next)."},"hint":{"type":"string","description":"Actionable remediation in human terms — what to DO next, often naming the exact endpoint (a template with <placeholders>). The human counterpart to the machine-actionable `next`."},"doc_url":{"type":"string","description":"Deep link to this code's docs: https://crate.hosaka.fm/docs/api#error-<code>. Auto-populated for catalogued codes."},"param":{"type":"string","description":"The specific request parameter that caused the failure (e.g. \"key\", \"q\"), so a client can point at the offending input. Present only when the code declares one."},"next":{"type":"string","description":"A copy-pasteable, fully-formed corrected call (a concrete URL, NOT a template) an agent can fire verbatim to recover — the machine-actionable counterpart to `hint`. Present only when a handler supplies one."},"details":{"type":"array","items":{},"description":"Structured validation breakdown — on Zod 400s (invalid_query), an array of { path, message }, one per failed field. Present only when validation specifics are attached."},"retry_after_seconds":{"type":"number","description":"On a 429 rate_limited response, seconds to wait before retrying (mirrors the Retry-After header). Sleep at least this long, then re-issue the identical request."},"master_id":{"type":"number","description":"Echoed on master_not_found (404) — the master id that did not resolve."}},"required":["error"],"description":"The error envelope for all 4xx/5xx responses. `error` is the only guaranteed field — branch on it, never on HTTP status alone. An unresolved/empty lookup is NOT an error: it returns HTTP 200 with `present:false` / a null field / `state:\"honest_gap\"`.\n\n| code | HTTP | when thrown | fix |\n|---|---|---|---|\n| `invalid_artist_key` | 400 | `/artist/{key}` key is not a 64-hex cluster_id, `discogs:<id>`, or `mbid:<uuid>` | resolve by name first: `GET /api/v1/resolve?q=<name>`, then call `/artist/{cluster_id}` |\n| `use_resolve_for_locator` | 400 | `/artist/{key}` given a `discogs:`/`mbid:` locator (not a canonical address) | `GET /api/v1/resolve?discogs=<id>` (or `?mbid=`), then use the returned cluster_id |\n| `invalid_label_key` | 400 | `/label/{key}` key is not a 64-hex `label_cluster_id` or name-slug (e.g. a `discogs:`/`mbid:` locator — not resolvable for labels yet) | address a label by its 64-hex `label_cluster_id` or its name-slug (e.g. `/api/v1/label/warp`) |\n| `missing_locator` | 400 | `/resolve` called with none of q/cluster/discogs/mbid (or `/bandcamp/release` with none of item/url/cluster_id) | pass exactly one locator |\n| `invalid_locator` | 400 | a `/resolve` (or `/bandcamp/release`) locator is malformed for its type | fix the format, or fall back to `?q=<name>` |\n| `invalid_source_or_cursor` | 400 | `/bandcamp` given an unknown/non-paginable `?source=` or a bad `?cursor=` | `GET /api/v1/bandcamp` (no params) for the manifest; pass `next_cursor` back verbatim |\n| `invalid_query` | 400 | `/search` `?q=` missing/empty, or any Zod validation failure (`details[]` attached) | pass `?q=<text>`; fix each `details` entry |\n| `invalid_facet` | 400 | an unknown facet filter name was supplied | `GET /api/v1/facets` for valid names + values |\n| `master_not_found` | 404 | `/dossier/master/{id}` or `/masters/{id}` given an unknown master id | verify the id (e.g. via `GET /api/v1/search`) |\n| `rate_limited` | 429 | an IP/key/tier rate or concurrency cap was exceeded (`retry_after_seconds` + `Retry-After` + `X-RateLimit-*` set) | back off `retry_after_seconds`, then retry; batch via `POST /api/v1/masters/batch` (≤100 ids = 1 request) |"},"RateLimited":{"type":"object","properties":{"error":{"type":"string","enum":["rate_limited"]},"retry_after_seconds":{"type":"number"}},"required":["error","retry_after_seconds"]},"ArtworkItem":{"type":"object","properties":{"url":{"type":"string","description":"Hotlink-only artwork URL: a Bandcamp CDN string, or a deterministic Cover Art Archive release-group URL. crate never fetches or re-hosts the bytes; a CAA url is best-effort and may 404 if no cover exists."},"source":{"type":"string","enum":["bandcamp","coverartarchive"]},"grain":{"type":"string","enum":["artist","release"]},"license":{"type":"string"},"rehost":{"type":"boolean","enum":[false],"description":"A const literal — always the boolean false — declaring crate's link-only artwork posture. WHY IT MATTERS: it is a contractual promise that the `url` on the artwork item is a HOTLINK the caller dereferences directly (a Bandcamp CDN url or a deterministic Cover Art Archive url); crate never fetches, caches, or re-hosts the image bytes. GOTCHA: because crate never dereferences the url, it can be stale or 404 (a CAA url is best-effort and may not exist) — your client must handle a broken image gracefully. Treat rehost:false as a fixed flag, not a toggle; it will never be true.","example":"false"}},"required":["url","source","grain","license","rehost"]},"Freshness":{"type":"object","properties":{"ridden_lag_s":{"type":["number","null"]},"mirror_lag_s":{"type":["number","null"]}},"required":["ridden_lag_s","mirror_lag_s"]},"ResultRow":{"type":"object","properties":{"master_id":{"type":"integer"},"title":{"type":"string"},"artist":{"type":"string"},"year":{"type":["integer","null"]},"cube_quadrant":{"type":["string","null"]},"owner_count":{"type":["integer","null"]},"dj_count":{"type":["integer","null"]},"critic_count":{"type":["integer","null"]},"formats":{"type":"array","items":{"type":"string"}},"link_to_cube":{"type":"string"}},"required":["master_id","title","artist","year","cube_quadrant","owner_count","dj_count","critic_count","formats","link_to_cube"]},"SearchResponse":{"type":"object","properties":{"query":{"type":"object","properties":{"q":{"type":"string"},"filters":{"type":"array","items":{"type":"object","properties":{"facet":{"type":"string"},"value":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}},{"type":"number"}]},"mode":{"type":"string","enum":["and","or"]}},"required":["facet","value"]}}},"required":["filters"]},"pagination":{"type":"object","properties":{"page":{"type":"integer"},"total_pages":{"type":["integer","null"]},"total_results":{"type":"integer"},"total_results_mode":{"type":"string","enum":["exact","approximate_50k_sample","lower_bound"]}},"required":["page","total_pages","total_results","total_results_mode"]},"facets":{"type":"object","additionalProperties":{}},"freshness":{"$ref":"#/components/schemas/Freshness"},"results":{"type":"array","items":{"$ref":"#/components/schemas/ResultRow"}}},"required":["query","pagination","facets","freshness","results"]},"MasterEnrichment":{"type":"object","properties":{"master_id":{"type":"integer"},"title":{"type":"string"},"artist":{"type":"string"},"year":{"type":["integer","null"]},"cube_quadrant":{"type":["string","null"]},"owner_count":{"type":["integer","null"]},"dj_count":{"type":["integer","null"]},"critic_count":{"type":["integer","null"]},"formats":{"type":"array","items":{"type":"string"}},"freshness":{"$ref":"#/components/schemas/Freshness"},"link_to_cube":{"type":"string"},"insert_links":{"type":"null"}},"required":["master_id","title","artist","year","cube_quadrant","owner_count","dj_count","critic_count","formats","freshness","link_to_cube","insert_links"]},"BatchResponse":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/MasterEnrichment"}},"not_found":{"type":"array","items":{"type":"integer"}}},"required":["results","not_found"]},"UsageResponse":{"type":"object","properties":{"tier":{"type":"string","enum":["sync","self_serve_49","self_serve_99","self_serve_299","free"]},"month":{"type":"string","pattern":"^\\d{4}-\\d{2}$"},"calls_this_month":{"type":"integer"},"quota_monthly":{"type":"integer"},"remaining_this_month":{"type":"integer"},"burst_limit_per_minute":{"type":"integer"},"tier_metadata":{"type":"object","properties":{"endpoints_available":{"type":"array","items":{"type":"string"}}},"required":["endpoints_available"]}},"required":["tier","month","calls_this_month","quota_monthly","remaining_this_month","burst_limit_per_minute","tier_metadata"]},"FacetCounts":{"type":"object","additionalProperties":{}},"WayfindAnswerResponse":{"type":"object","additionalProperties":{}},"WayfindInterpretResponse":{"type":"object","additionalProperties":{}},"ObservedBeaconRequest":{"type":"object","properties":{"search_event_id":{"type":"string","format":"uuid"},"source":{"type":"string","enum":["swr-cache-hit","client-cache-hit"]},"timestamp":{"type":"string","format":"date-time"}},"required":["search_event_id","source","timestamp"],"additionalProperties":false},"RefinedBeaconRequest":{"type":"object","properties":{"search_event_id":{"type":"string","format":"uuid"},"changed_facets":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","enum":["genre","style","year_from","year_to","country","format","label","q"]},"from":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"array","items":{"type":"string"}},{"type":"null"},{"type":"null"}]},"to":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"array","items":{"type":"string"}},{"type":"null"},{"type":"null"}]}},"required":["name"],"additionalProperties":false},"maxItems":10},"timestamp":{"type":"string","format":"date-time"}},"required":["search_event_id","changed_facets","timestamp"],"additionalProperties":false},"BeaconError":{"type":"object","properties":{"error":{"type":"string","example":"invalid_payload"},"details":{},"skew_ms":{"type":"number"}},"required":["error"]},"BandcampLabel":{"type":"object","properties":{"name":{"type":["string","null"]},"url":{"type":["string","null"]}},"required":["name","url"]},"BandcampReleaseEconomics":{"type":"object","properties":{"minimum_price":{"type":["number","null"]},"set_price":{"type":["number","null"]},"is_set_price":{"type":["boolean","null"]},"default_price":{"type":["number","null"]},"currency":{"type":["string","null"]},"download_pref":{"type":["integer","null"],"description":"Bandcamp enum: 1 = free, 2 = name-your-price / paid."},"free_download":{"type":["boolean","null"]},"require_email":{"type":["boolean","null"]},"about":{"type":["string","null"],"description":"The artist's own release description."},"credits":{"type":["string","null"],"description":"The artist's own credits text."},"upc":{"type":["string","null"],"description":"The release barcode (UPC/EAN) as Bandcamp records it — the closest thing Bandcamp has to a catalogue number. WHY IT MATTERS: it is a cross-platform release identifier you can use to match the release against other catalogues. GOTCHA: it is a STRING (barcodes carry leading zeros and exceed safe-integer range — numericizing corrupts them) and is nullable (most Bandcamp self-releases have none) — null is an honest gap, not an error.","example":"888002345678"},"is_preorder":{"type":["boolean","null"]},"preorder_count":{"type":["integer","null"]},"publish_date":{"type":["string","null"]},"packages":{"description":"Raw physical-format / edition jsonb (verbatim)."}},"required":["minimum_price","set_price","is_set_price","default_price","currency","download_pref","free_download","require_email","about","credits","upc","is_preorder","preorder_count","publish_date"]},"BandcampRelease":{"type":"object","properties":{"bandcamp_item_id":{"type":"string","description":"A Bandcamp release id — the direct address for one release dossier (?item=<id> on /bandcamp/release returns the full tracklist). WHY IT MATTERS: it is the stable per-release key you carry from a release-list row back to the full dossier fetch. GOTCHA: it is a bigint serialized AS A STRING and is OPAQUE — never numericize it (JS numbers lose precision past 2^53 and the value is an identifier, not a quantity), never do arithmetic on it, and pass it back verbatim.","example":"1234567890"},"cluster_id":{"type":["string","null"],"description":"crate's CANONICAL artist identity — a pe-norm-v1 hex string. The SAME artist across Discogs, MusicBrainz and Bandcamp collapses to ONE cluster_id, so this is the key you store and the key you address every artist surface off of (/artist/{key} takes a 64-hex cluster_id directly). WHY IT MATTERS: it is crate's prime IP — the non-Discogs long-tail join key; discogs_artist_id / mbid are mere leaf coordinates onto it. GOTCHA: it is an OPAQUE string — pass it through verbatim, NEVER numericize, parse, or compare it as a number. null is an HONEST GAP (the name/link couldn't be resolved to a cluster), NOT an error — you still get HTTP 200. A 64-hex cluster_id always resolves at OBSERVED tier (resolved_via:'cluster'), never re-anchored onto a same-name Discogs row.","example":"a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6"},"artist":{"type":["string","null"]},"artist_subdomain":{"type":["string","null"]},"title":{"type":["string","null"]},"release_date":{"type":["string","null"],"description":"ISO timestamp (may be a reissue date or a future pre-order)."},"source_url":{"type":["string","null"],"description":"The Bandcamp album page URL."},"tags":{"type":"array","items":{"type":"string"},"description":"Per-release genre/mood/location tags (Bandcamp keywords)."},"label":{"allOf":[{"$ref":"#/components/schemas/BandcampLabel"},{"type":["object","null"],"description":"Release label (null when self-released/unknown)."}]},"artwork":{"type":"array","items":{"$ref":"#/components/schemas/ArtworkItem"},"description":"Link-only cover art (source: bandcamp, grain: release)."},"tracks":{"type":"array","items":{"type":"object","properties":{"track_num":{"type":"integer"},"title":{"type":["string","null"]},"duration_s":{"type":["number","null"],"description":"Track length in SECONDS, derived from Bandcamp's integer duration_ms. WHY IT MATTERS: a uniform per-track length you can sum for a release runtime. GOTCHA: the unit is seconds (the _s suffix), not milliseconds and not minutes — don't re-divide by 1000. It is nullable when Bandcamp exposed no duration (honest gap).","example":"284"},"license_type":{"type":["string","null"]},"track_url":{"type":["string","null"],"description":"The Bandcamp track PAGE url for one track. WHY IT MATTERS: it is the canonical human-visitable link to the track on Bandcamp. GOTCHA: this is NOT a playable/direct audio stream URL — Bandcamp's actual stream URLs are tokenized and expiring and are not stored (both a technical and a ToS constraint). Do not feed track_url to an audio element expecting bytes; it is a page link. It is also nullable (a track may have no stored page url) — an honest gap, not an error.","example":"https://artistname.bandcamp.com/track/the-track-title"}},"required":["track_num","title","duration_s","license_type","track_url"]}},"economics":{"allOf":[{"$ref":"#/components/schemas/BandcampReleaseEconomics"},{"type":["object","null"],"description":"Pricing / about / credits / barcode / packages / preorder (null if mirror has none)."}]}},"required":["bandcamp_item_id","cluster_id","artist","artist_subdomain","title","release_date","source_url","tags","label","artwork","tracks","economics"]},"BandcampReleaseSummary":{"type":"object","properties":{"bandcamp_item_id":{"type":"string","description":"A Bandcamp release id — the direct address for one release dossier (?item=<id> on /bandcamp/release returns the full tracklist). WHY IT MATTERS: it is the stable per-release key you carry from a release-list row back to the full dossier fetch. GOTCHA: it is a bigint serialized AS A STRING and is OPAQUE — never numericize it (JS numbers lose precision past 2^53 and the value is an identifier, not a quantity), never do arithmetic on it, and pass it back verbatim.","example":"1234567890"},"artist":{"type":["string","null"]},"title":{"type":["string","null"]},"release_date":{"type":["string","null"]},"source_url":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"label":{"allOf":[{"$ref":"#/components/schemas/BandcampLabel"},{"type":["object","null"]}]}},"required":["bandcamp_item_id","artist","title","release_date","source_url","tags","label"]},"DossierManifest":{"type":"object","properties":{"contract_version":{"type":"string"},"generated_at":{"type":"string"},"unavailable_grains":{"type":"array","items":{}},"grains":{"type":"array","items":{}}},"required":["contract_version","generated_at","unavailable_grains","grains"]},"MasterDossierContract":{"type":"object","properties":{"contract_version":{"type":"string"},"grain":{"type":"string","enum":["master"]},"id":{"type":"number"},"header":{"type":"object","properties":{"title":{"type":"string"},"artist":{"type":["string","null"]},"year":{"type":["number","null"]},"formats":{"type":"array","items":{"type":"string"}},"cube_quadrant":{"type":"object","properties":{"code":{"type":["string","null"]},"label":{"type":["string","null"]}},"required":["code","label"]}},"required":["title","artist","year","formats","cube_quadrant"]},"sections":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"state":{"type":"string"}},"required":["field","state"]}},"artwork":{"type":"array","items":{"$ref":"#/components/schemas/ArtworkItem"}},"freshness":{"type":"object","properties":{"mirror_lag_s":{"type":["number","null"]},"seen_lag_s":{"type":["number","null"]}},"required":["mirror_lag_s","seen_lag_s"]},"cache":{"type":"object","properties":{"etag":{"type":"string"},"maxAge":{"type":"number"}},"required":["etag","maxAge"]},"provenance":{"type":"array","items":{}},"generated_at":{"type":"string"}},"required":["contract_version","grain","id","header","sections","artwork","freshness","cache","provenance","generated_at"]},"ArtistDossierContract":{"type":"object","properties":{"contract_version":{"type":"string"},"grain":{"type":"string","enum":["artist"]},"slug":{"type":"string"},"display":{"type":"string"},"id":{"type":["number","null"]},"cluster_id":{"type":["string","null"],"description":"crate's CANONICAL artist identity — a pe-norm-v1 hex string. The SAME artist across Discogs, MusicBrainz and Bandcamp collapses to ONE cluster_id, so this is the key you store and the key you address every artist surface off of (/artist/{key} takes a 64-hex cluster_id directly). WHY IT MATTERS: it is crate's prime IP — the non-Discogs long-tail join key; discogs_artist_id / mbid are mere leaf coordinates onto it. GOTCHA: it is an OPAQUE string — pass it through verbatim, NEVER numericize, parse, or compare it as a number. null is an HONEST GAP (the name/link couldn't be resolved to a cluster), NOT an error — you still get HTTP 200. A 64-hex cluster_id always resolves at OBSERVED tier (resolved_via:'cluster'), never re-anchored onto a same-name Discogs row.","example":"a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6"},"resolved_via":{"type":["string","null"],"enum":["discogs","cluster"],"description":"The binding TIER — how trustworthy the identity match is. 'discogs' = canonical, Discogs-bound (verified). 'cluster' = OBSERVED/UNVERIFIED identity inferred from the seen booking graph with no Discogs bind. null = the lookup did not resolve at all (honest gap). WHY IT MATTERS: it is a trust signal you must respect — a 'cluster' result is crate showing you what it can SEE in the booking graph, not what it has verified. GOTCHA: surface a 'cluster' result as flagged/unverified, NEVER as canonical truth; do not silently merge a 'cluster' artist with a verified one. A bare 64-hex key always comes back 'cluster' by design (it skips the cc0_artists lookup so a hex address never re-anchors onto a same-name Discogs row).","example":"discogs"},"identity":{"type":["object","null"],"properties":{"discogsArtistId":{"type":["number","null"]},"clusterId":{"type":["string","null"]},"resolvedVia":{"type":"string","enum":["discogs","cluster"]},"name":{"type":"string"},"realname":{"type":["string","null"]},"profile":{"type":["string","null"]},"urls":{"type":"array","items":{"type":"string"}},"aliases":{"type":"array","items":{"type":"string"}}},"required":["discogsArtistId","clusterId","resolvedVia","name","realname","profile","urls","aliases"]},"behavioral":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"ownerReach":{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["count"]},"value":{"type":"number"}},"required":["kind","value"]},{"type":"object","properties":{"kind":{"type":"string","enum":["suppressed"]}},"required":["kind"]}]},"wantlistDemand":{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["count"]},"value":{"type":"number"}},"required":["kind","value"]},{"type":"object","properties":{"kind":{"type":"string","enum":["suppressed"]}},"required":["kind"]}]},"collectorCoownership":{"type":["number","null"]},"communityFootprint":{"type":["number","null"]},"travelsWith":{"type":"array","items":{}},"workCount":{"type":["number","null"]},"primaryGenre":{"type":["string","null"]},"primaryStyles":{"type":"array","items":{"type":"string"}},"masterCountWithGenre":{"type":["number","null"]}},"required":["ownerReach","wantlistDemand","collectorCoownership","communityFootprint","travelsWith","workCount","primaryGenre","primaryStyles","masterCountWithGenre"]}},"required":["state","signals"]},"editorial":{"type":"object","properties":{"state":{"type":"string"},"signals":{}},"required":["state"]},"across_the_web":{"type":"object","properties":{"count":{"type":"number"},"links":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"sourceSubtype":{"type":["string","null"]},"url":{"type":"string"},"title":{"type":["string","null"]},"description":{"type":["string","null"]},"confidence":{"type":["number","null"]}},"required":["source","sourceSubtype","url","title","description","confidence"]}}},"required":["count","links"]},"journalism":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"pressCount":{"type":"number"},"reviewCount":{"type":"number"},"interviewCount":{"type":"number"},"featureCount":{"type":"number"},"items":{"type":"array","items":{}}},"required":["pressCount","reviewCount","interviewCount","featureCount","items"]}},"required":["state","signals"]},"connections":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":"object","properties":{"count":{"type":"number"},"related":{"type":"array","items":{}}},"required":["count","related"]}},"required":["state","signals"]},"emergence":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"emergenceTier":{"type":"string"},"momentumTier":{"type":"string"},"emergenceScore":{"type":"number"},"momentumScore":{"type":"number"},"establishmentDelta":{"type":"number"},"eventsRecent":{"type":"number"},"isFreshBreakout":{"type":"boolean"},"isUnsigned":{"type":"boolean"}},"required":["emergenceTier","momentumTier","emergenceScore","momentumScore","establishmentDelta","eventsRecent","isFreshBreakout","isUnsigned"]}},"required":["state","signals"]},"breakout":{"type":"object","properties":{"state":{"type":"string"},"signal":{"type":["object","null"],"properties":{"emergenceTier":{"type":"string"},"corroboration":{"type":"string"},"pressCount":{"type":"number"}},"required":["emergenceTier","corroboration","pressCount"]}},"required":["state","signal"]},"compositions":{"type":"object","properties":{"state":{"type":"string"},"works":{"type":"array","items":{"type":"object","properties":{"workMbid":{"type":"string"},"workName":{"type":"string"},"ownerReach":{"type":"number"},"versionCollectors":{"type":"number"},"versionDepth":{"type":"number"},"nRecordingsGlobal":{"type":"number"}},"required":["workMbid","workName","ownerReach","versionCollectors","versionDepth","nRecordingsGlobal"]}}},"required":["state","works"]},"tier_presence":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"spansTiers":{"type":"boolean"},"playsClubs":{"type":"boolean"},"playsFestivals":{"type":"boolean"},"clubEvents":{"type":"number"},"festivalEditions":{"type":"number"},"firstClubAt":{"type":["string","null"]},"lastClubAt":{"type":["string","null"]},"firstFestivalAt":{"type":["string","null"]},"lastFestivalAt":{"type":["string","null"]}},"required":["spansTiers","playsClubs","playsFestivals","clubEvents","festivalEditions","firstClubAt","lastClubAt","firstFestivalAt","lastFestivalAt"]}},"required":["state","signals"]},"early_demand":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"ownerReach":{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["count"]},"value":{"type":"number"}},"required":["kind","value"]},{"type":"object","properties":{"kind":{"type":"string","enum":["suppressed"]}},"required":["kind"]}]},"wishlistDemand":{"anyOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["count"]},"value":{"type":"number"}},"required":["kind","value"]},{"type":"object","properties":{"kind":{"type":"string","enum":["suppressed"]}},"required":["kind"]}]},"earliestSupportedAt":{"type":["string","null"]},"earliestWishedAt":{"type":["string","null"]},"demandCoverage":{"type":["number","null"]},"wishedCoverage":{"type":["number","null"]},"alsoOwnDegree":{"type":["number","null"]}},"required":["ownerReach","wishlistDemand","earliestSupportedAt","earliestWishedAt","demandCoverage","wishedCoverage","alsoOwnDegree"]}},"required":["state","signals"]},"bandcamp_emergence":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"emergenceClass":{"type":"string"},"demandLead":{"type":"number"},"demandRatio":{"type":["number","null"]},"ownerReach":{"type":["number","null"]},"wishlistDemand":{"type":["number","null"]},"distinctReleases":{"type":["number","null"]},"earliestWishedAt":{"type":["string","null"]}},"required":["emergenceClass","demandLead","demandRatio","ownerReach","wishlistDemand","distinctReleases","earliestWishedAt"]}},"required":["state","signals"]},"artwork":{"type":"object","properties":{"state":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ArtworkItem"}}},"required":["state","items"]},"dated_appearance":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":"object","properties":{"count":{"type":"number"},"sources":{"type":"array","items":{"type":"object","properties":{"sourceKind":{"type":"string"},"firstAppearanceDate":{"type":["string","null"]},"latestAppearanceDate":{"type":["string","null"]},"appearanceCount":{"type":"number"}},"required":["sourceKind","firstAppearanceDate","latestAppearanceDate","appearanceCount"]}}},"required":["count","sources"]}},"required":["state","signals"]},"network_position":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"degree":{"type":"number"},"weightedDegree":{"type":"number"},"resolvedNeighbourRatio":{"type":["number","null"]},"recentDegree":{"type":"number"},"establishmentDelta":{"type":["number","null"]}},"required":["degree","weightedDegree","resolvedNeighbourRatio","recentDegree","establishmentDelta"]}},"required":["state","signals"]},"airplay_breadth":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"firstAirplayDate":{"type":["string","null"]},"latestAirplayDate":{"type":["string","null"]},"airplayCount":{"type":"number"},"distinctStations":{"type":"number"},"distinctVenues":{"type":"number"},"distinctDjs":{"type":"number"}},"required":["firstAirplayDate","latestAirplayDate","airplayCount","distinctStations","distinctVenues","distinctDjs"]}},"required":["state","signals"]},"brokerage":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"degree":{"type":"number"},"effectiveSize":{"type":["number","null"]},"brokerageScore":{"type":["number","null"]},"brokerageTier":{"type":"string"}},"required":["degree","effectiveSize","brokerageScore","brokerageTier"]}},"required":["state","signals"]},"lead_time":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"pressToAirplayDays":{"type":"number"},"undergroundToBookingDays":{"type":"number"}},"required":["pressToAirplayDays","undergroundToBookingDays"]}},"required":["state","signals"]},"bandcamp_tastemaker":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"supporterCohortSize":{"type":"number"},"aesq":{"type":"number"},"aesqMedian":{"type":["number","null"]},"aesqRw":{"type":["number","null"]},"meanFirstBuyerEarliness":{"type":["number","null"]}},"required":["supporterCohortSize","aesq","aesqMedian","aesqRw","meanFirstBuyerEarliness"]}},"required":["state","signals"]},"live_demand":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"demandTier":{"type":"string"},"demandMomentumScore":{"type":["number","null"]},"eventsTracked":{"type":"number"},"eventsSoldOut":{"type":"number"},"distinctProviders":{"type":"number"},"distinctPromoters":{"type":"number"},"avgFillFraction":{"type":["number","null"]},"maxSoldOutLeadDays":{"type":["number","null"]}},"required":["demandTier","demandMomentumScore","eventsTracked","eventsSoldOut","distinctProviders","distinctPromoters","avgFillFraction","maxSoldOutLeadDays"]}},"required":["state","signals"]},"editorial_attention":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"memberCount":{"type":"number"},"sourceRepoCount":{"type":"number"},"firstObservedAt":{"type":["string","null"]},"latestObservedAt":{"type":["string","null"]}},"required":["memberCount","sourceRepoCount","firstObservedAt","latestObservedAt"]}},"required":["state","signals"]},"web_presence":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"bandcampUrl":{"type":["string","null"]},"soundcloudUrl":{"type":["string","null"]},"instagramUrl":{"type":["string","null"]},"websiteUrl":{"type":["string","null"]},"discogsArtistIdClaimed":{"type":["number","null"]},"raStatus":{"type":["string","null"]},"followerCount":{"type":["number","null"]},"country":{"type":["string","null"]},"firstEventAt":{"type":["string","null"]}},"required":["bandcampUrl","soundcloudUrl","instagramUrl","websiteUrl","discogsArtistIdClaimed","raStatus","followerCount","country","firstEventAt"]}},"required":["state","signals"]},"editorial_canon":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"pickCount":{"type":"number"},"picks":{"type":"array","items":{"type":"object","properties":{"relation":{"type":"string"},"polarity":{"type":["string","null"]},"confidence":{"type":["number","null"]},"assertedBy":{"type":"string"},"isDirectQuote":{"type":"boolean"},"speakerName":{"type":["string","null"]},"objectName":{"type":"string"}},"required":["relation","polarity","confidence","assertedBy","isDirectQuote","speakerName","objectName"]}}},"required":["pickCount","picks"]}},"required":["state","signals"]},"discography":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":"object","properties":{"clusterId":{"type":["string","null"]},"total":{"type":"number"},"shown":{"type":"number"},"truncated":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"discogsMasterId":{"type":"number"},"representativeName":{"type":["string","null"]},"isPrimary":{"type":"boolean"},"billingPosition":{"type":"number"},"_links":{"type":"object","properties":{"master":{"type":"string"}},"required":["master"]}},"required":["discogsMasterId","representativeName","isPrimary","billingPosition","_links"]}},"overMerge":{"type":["object","null"],"properties":{"discogsArtistIdCount":{"type":"number"},"folded":{"type":"boolean"}},"required":["discogsArtistIdCount","folded"]},"observedAt":{"type":["string","null"]}},"required":["clusterId","total","shown","truncated","entries","overMerge","observedAt"]}},"required":["state","signals"]},"provenance":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"producer":{"type":"string"},"sourceTable":{"type":"string"},"refreshCadence":{"type":"string"},"tier":{"type":"string"},"honestGapState":{"type":"string"},"detailAnchor":{"type":"string"}},"required":["field","producer","sourceTable","refreshCadence","tier","honestGapState"]}},"generated_at":{"type":"string"}},"required":["contract_version","grain","slug","display","id","cluster_id","resolved_via","identity","behavioral","editorial","across_the_web","journalism","connections","emergence","breakout","compositions","tier_presence","early_demand","bandcamp_emergence","artwork","dated_appearance","network_position","airplay_breadth","brokerage","lead_time","bandcamp_tastemaker","live_demand","editorial_attention","web_presence","editorial_canon","discography","provenance","generated_at"]},"LabelDossierContract":{"type":"object","properties":{"contract_version":{"type":"string"},"grain":{"type":"string","enum":["label"]},"slug":{"type":"string"},"display":{"type":"string"},"id":{"type":["number","null"],"description":"Resolved Discogs label id (the LEAF coordinate); null = cluster-only / unresolved."},"cluster_id":{"type":["string","null"],"description":"crate's CANONICAL artist identity — a pe-norm-v1 hex string. The SAME artist across Discogs, MusicBrainz and Bandcamp collapses to ONE cluster_id, so this is the key you store and the key you address every artist surface off of (/artist/{key} takes a 64-hex cluster_id directly). WHY IT MATTERS: it is crate's prime IP — the non-Discogs long-tail join key; discogs_artist_id / mbid are mere leaf coordinates onto it. GOTCHA: it is an OPAQUE string — pass it through verbatim, NEVER numericize, parse, or compare it as a number. null is an HONEST GAP (the name/link couldn't be resolved to a cluster), NOT an error — you still get HTTP 200. A 64-hex cluster_id always resolves at OBSERVED tier (resolved_via:'cluster'), never re-anchored onto a same-name Discogs row.","example":"a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6"},"resolved_via":{"type":["string","null"],"enum":["discogs","cluster"],"description":"The binding TIER — how trustworthy the identity match is. 'discogs' = canonical, Discogs-bound (verified). 'cluster' = OBSERVED/UNVERIFIED identity inferred from the seen booking graph with no Discogs bind. null = the lookup did not resolve at all (honest gap). WHY IT MATTERS: it is a trust signal you must respect — a 'cluster' result is crate showing you what it can SEE in the booking graph, not what it has verified. GOTCHA: surface a 'cluster' result as flagged/unverified, NEVER as canonical truth; do not silently merge a 'cluster' artist with a verified one. A bare 64-hex key always comes back 'cluster' by design (it skips the cc0_artists lookup so a hex address never re-anchors onto a same-name Discogs row).","example":"discogs"},"identity":{"type":["object","null"],"properties":{"discogsLabelId":{"type":["number","null"]},"name":{"type":"string"},"parentLabel":{"type":["string","null"]},"profile":{"type":["string","null"]},"urls":{"type":"array","items":{"type":"string"}}},"required":["discogsLabelId","name","parentLabel","profile","urls"]},"binds":{"type":["object","null"],"properties":{"discogs_label_id":{"type":["number","null"]},"label_mbid":{"type":["string","null"]},"bandcamp_url":{"type":["string","null"],"description":"The label's self-declared Bandcamp page URL from the cross-source identity spine (a LINK, never fetched — same rehost:false posture as artwork). WHY IT MATTERS: it is the canonical 'listen/buy on Bandcamp' address for the label, and the bind that lets a Bandcamp-native (cluster-only) label still resolve. GOTCHA: it is a page link, not an API or audio endpoint; it is nullable (no Bandcamp bind for this cluster yet) — null is an honest gap, not an error.","example":"https://labelname.bandcamp.com"}},"required":["discogs_label_id","label_mbid","bandcamp_url"],"description":"cycle-L1: cross-source identity binds from label_identity_v1. bandcamp_url is a LINK only (rehost:false, never fetched)."},"presence":{"type":["object","null"],"properties":{"has_discogs":{"type":"boolean"},"has_bandcamp":{"type":"boolean"},"has_seen_momentum":{"type":"boolean"},"has_editorial":{"type":"boolean"}},"required":["has_discogs","has_bandcamp","has_seen_momentum","has_editorial"],"description":"cycle-L1: which source planes this cluster is observed in (presence flags, not measured counts)."},"over_merge":{"type":["object","null"],"properties":{"discogs_label_id_count":{"type":"number","description":"OVER-MERGE observability: how many distinct raw Discogs label ids folded into this one label_cluster_id. WHY IT MATTERS: a label cluster is name-derived, so distinct Discogs labels that share a normalized name can collapse together; this count is crate's honesty signal that the cluster may conflate more than one real label. GOTCHA: when it is >1 the dossier is forced to the OBSERVED tier (resolved_via:'cluster') and surfaces an `over_merge` block — do NOT present such a cluster's identity or signals as authoritative for a single label. 1 (or absent) means a clean single-label cluster.","example":"1"},"folded":{"type":"boolean","enum":[true]}},"required":["discogs_label_id_count","folded"],"description":"cycle-L1: present ONLY when >1 raw Discogs label id folded into this cluster (over-merge honesty) — the dossier is then forced to the OBSERVED tier."},"behavioral":{"type":"object","properties":{"state":{"type":"string"},"signals":{}},"required":["state"]},"travels":{"type":"object","properties":{"state":{"type":"string"},"related":{"type":"array","items":{"type":"object","properties":{"clusterId":{"type":"string"},"labelName":{"type":["string","null"]},"npmi":{"type":"number","description":"The rarity-weighted normalized pointwise mutual information of a Bandcamp co-ownership edge — how much more often two labels are co-collected than chance, downweighting ubiquitous labels. WHY IT MATTERS: it ranks the `travels` adjacency (higher = a stronger, more distinctive co-ownership tie) so you can take the top few as the label's nearest neighbors. GOTCHA: it is a relative affinity score, NOT a count or a probability — don't sum it, threshold it absolutely, or show it as a percentage; use it only to order edges. Pair it with `support` (the distinct-supporter count behind the edge, k-anon floor 5).","example":"0.42"},"support":{"type":"number"},"_links":{"type":"object","properties":{"label":{"type":"string"}},"required":["label"]}},"required":["clusterId","labelName","npmi","support","_links"]}}},"required":["state","related"],"description":"cycle-L1: Bandcamp co-ownership adjacency (mirror.bandcamp_label_travels_v1, cluster-keyed, k-anon k>=5). Each entry links onward to /api/v1/label/{cluster_id}."},"dj_momentum":{"type":"object","properties":{"state":{"type":"string"},"signals":{"type":["object","null"],"properties":{"playCount":{"type":"number"},"distinctDjs":{"type":"number"},"distinctArtists":{"type":"number"},"distinctTracklists":{"type":"number"},"firstPlayedAt":{"type":["string","null"]},"lastPlayedAt":{"type":["string","null"]},"playsLast365d":{"type":"number","description":"How many times the label's roster was played in DJ sets in the last 365 days — the RECENCY signal of label DJ-momentum (seen.label_djset_momentum). WHY IT MATTERS: it is the lead 'which labels are breaking right now' number; pair it with distinctArtists (roster breadth) to separate a one-hit label from a deep accelerating roster. GOTCHA: it is a raw count over public DJ tracklists (no per-fan data), not a rate — divide by playCount to get `velocity`. A label with no DJ plays has no row at all (dj_momentum.state='honest_gap'), not a zero.","example":"342"},"velocity":{"type":["number","null"],"description":"The share of a label's all-time DJ-set plays that happened in the last 365 days: playsLast365d / playCount ∈ [0,1]. WHY IT MATTERS: it normalizes momentum so a young breaking label (near 1.0) is distinguishable from a long-established catalogue label with the same recent count but a huge back-catalogue (near 0). GOTCHA: it is null when playCount is 0 (no all-time plays — avoid div-by-zero); it is a ratio, not a percentage or a count — don't sum it across labels.","example":"0.61"}},"required":["playCount","distinctDjs","distinctArtists","distinctTracklists","firstPlayedAt","lastPlayedAt","playsLast365d","velocity"]}},"required":["state","signals"],"description":"cycle-L2: per-label DJ-set momentum (seen.label_djset_momentum, cluster-keyed) — which labels are breaking in DJ sets. Lead with playsLast365d (recency) + distinctArtists (roster breadth); velocity = playsLast365d/playCount. honest_gap when the label has no DJ plays."},"provenance":{"type":"array","items":{}},"generated_at":{"type":"string"}},"required":["contract_version","grain","slug","display","id","cluster_id","resolved_via","identity","binds","presence","over_merge","behavioral","travels","dj_momentum","provenance","generated_at"]},"FestivalDossierContract":{"type":"object","properties":{"contract_version":{"type":"string"},"grain":{"type":"string","enum":["festival"]},"slug":{"type":"string"},"display":{"type":"string"},"id":{"type":["string","null"]},"identity":{},"lineup":{},"provenance":{"type":"array","items":{}},"generated_at":{"type":"string"}},"required":["contract_version","grain","slug","display","id","provenance","generated_at"]},"BandcampFeedContract":{"type":"object","properties":{"key":{"type":"object","properties":{"kind":{"type":"string"},"input":{"type":"string"},"resolved_cluster_id_hex":{"type":["string","null"]},"resolved_discogs_artist_id":{"type":["number","null"]},"resolved_mbid":{"type":["string","null"]}},"required":["kind","input","resolved_cluster_id_hex","resolved_discogs_artist_id","resolved_mbid"]},"sources":{"type":"object","additionalProperties":{}},"_meta":{"type":"object","properties":{"k_anon_floor":{"type":"number"},"note":{"type":"string"},"granted_sources":{"type":"array","items":{"type":"string"}},"generated_at":{"type":"string"}},"required":["k_anon_floor","note","granted_sources","generated_at"]}},"required":["key","sources","_meta"]},"BandcampBulkPage":{"type":"object","properties":{"source":{"type":"string"},"rows":{"type":"array","items":{"type":"object","additionalProperties":{}}},"next_cursor":{"type":["string","null"],"description":"An OPAQUE keyset-pagination cursor. To page the full corpus of one Bandcamp source, take the next_cursor from a page and pass it back as ?cursor=<value> on the next request; null means you have reached the last page. WHY IT MATTERS: keyset pagination over a cluster_id-PK source is stable under inserts (unlike offset paging) and is the only way to walk the whole bulk feed. GOTCHA: it is OPAQUE — pass it back verbatim, NEVER construct, decode, or mutate it; a hand-built or stale cursor yields invalid_source_or_cursor (400). It is always a string.","example":"a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6"},"_meta":{"type":"object","properties":{"k_anon_floor":{"type":"number"},"note":{"type":"string"},"generated_at":{"type":"string"}},"required":["k_anon_floor","note","generated_at"]}},"required":["source","rows","next_cursor","_meta"]},"BandcampReleaseResponse":{"anyOf":[{"type":"object","properties":{"object":{"type":"string","enum":["bandcamp.release"]},"present":{"type":"boolean","enum":[true]},"release":{"$ref":"#/components/schemas/BandcampRelease"}},"required":["object","present","release"]},{"type":"object","properties":{"object":{"type":"string","enum":["bandcamp.release"]},"present":{"type":"boolean","enum":[false]},"note":{"type":"string"}},"required":["object","present","note"]},{"type":"object","properties":{"object":{"type":"string","enum":["bandcamp.release_list"]},"cluster_id":{"type":"string","description":"crate's CANONICAL artist identity — a pe-norm-v1 hex string. The SAME artist across Discogs, MusicBrainz and Bandcamp collapses to ONE cluster_id, so this is the key you store and the key you address every artist surface off of (/artist/{key} takes a 64-hex cluster_id directly). WHY IT MATTERS: it is crate's prime IP — the non-Discogs long-tail join key; discogs_artist_id / mbid are mere leaf coordinates onto it. GOTCHA: it is an OPAQUE string — pass it through verbatim, NEVER numericize, parse, or compare it as a number. null is an HONEST GAP (the name/link couldn't be resolved to a cluster), NOT an error — you still get HTTP 200. A 64-hex cluster_id always resolves at OBSERVED tier (resolved_via:'cluster'), never re-anchored onto a same-name Discogs row.","example":"a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6"},"count":{"type":"integer"},"releases":{"type":"array","items":{"$ref":"#/components/schemas/BandcampReleaseSummary"}}},"required":["object","cluster_id","count","releases"]}]},"ApiRootIndex":{"type":"object","properties":{"object":{"type":"string","enum":["api_index"]},"version":{"type":"string","enum":["v1"]},"cold_start":{"type":"object","properties":{"problem":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"step":{"type":"number"},"call":{"type":"string"},"gives":{"type":"string"}},"required":["step","call","gives"]}}},"required":["problem","steps"]},"recipes":{"type":"array","items":{"type":"object","properties":{"task":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"call":{"type":"string"},"gives":{"type":"string"}},"required":["call","gives"]}}},"required":["task","steps"]}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string"},"auth":{"type":"string","enum":["public","sync"]},"eli5":{"type":"string"},"description":{"type":"string"},"example":{"type":"string"},"how_to_get_the_key":{"type":["string","null"]}},"required":["name","url","auth","eli5","description","example","how_to_get_the_key"]}},"errors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"http_status":{"type":"integer"},"when":{"type":"string"},"fix":{"type":"string"}},"required":["code","http_status","when","fix"]}},"links":{"type":"object","properties":{"openapi":{"type":"string"},"docs":{"type":"string"}},"required":["openapi","docs"]},"types":{"type":"object","properties":{"openapi":{"type":"string"},"generate":{"type":"string"}},"required":["openapi","generate"]}},"required":["object","version","cold_start","recipes","resources","errors","links","types"]},"SignupAccepted":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"message":{"type":"string"}},"required":["ok","message"]},"SignupRequest":{"type":"object","properties":{"email":{"type":"string","maxLength":255,"format":"email"}},"required":["email"]},"IdentityResolution":{"type":"object","properties":{"cluster_id":{"type":["string","null"],"description":"crate's CANONICAL artist identity — a pe-norm-v1 hex string. The SAME artist across Discogs, MusicBrainz and Bandcamp collapses to ONE cluster_id, so this is the key you store and the key you address every artist surface off of (/artist/{key} takes a 64-hex cluster_id directly). WHY IT MATTERS: it is crate's prime IP — the non-Discogs long-tail join key; discogs_artist_id / mbid are mere leaf coordinates onto it. GOTCHA: it is an OPAQUE string — pass it through verbatim, NEVER numericize, parse, or compare it as a number. null is an HONEST GAP (the name/link couldn't be resolved to a cluster), NOT an error — you still get HTTP 200. A 64-hex cluster_id always resolves at OBSERVED tier (resolved_via:'cluster'), never re-anchored onto a same-name Discogs row.","example":"a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6a3f9c1e84b2d70f6"},"slug":{"type":["string","null"]},"display":{"type":["string","null"]},"locators":{"type":"object","properties":{"discogs":{"type":["number","null"]},"mbid":{"type":["string","null"]},"bandcamp":{"type":"array","items":{"type":"string"}},"soundcloud":{"type":"array","items":{"type":"string"}},"instagram":{"type":"array","items":{"type":"string"}},"spotify":{"type":"array","items":{"type":"string"}},"youtube":{"type":"array","items":{"type":"string"}},"website":{"type":"array","items":{"type":"string"}}},"required":["discogs","mbid","bandcamp","soundcloud","instagram","spotify","youtube","website"]},"resolved_via":{"type":["string","null"],"enum":["discogs","cluster"],"description":"The binding TIER — how trustworthy the identity match is. 'discogs' = canonical, Discogs-bound (verified). 'cluster' = OBSERVED/UNVERIFIED identity inferred from the seen booking graph with no Discogs bind. null = the lookup did not resolve at all (honest gap). WHY IT MATTERS: it is a trust signal you must respect — a 'cluster' result is crate showing you what it can SEE in the booking graph, not what it has verified. GOTCHA: surface a 'cluster' result as flagged/unverified, NEVER as canonical truth; do not silently merge a 'cluster' artist with a verified one. A bare 64-hex key always comes back 'cluster' by design (it skips the cc0_artists lookup so a hex address never re-anchors onto a same-name Discogs row).","example":"discogs"},"resolved_from":{"type":"string","enum":["url","name","locator"],"description":"HOW you addressed the artist on this /resolve call — which input path matched. 'url' = you pasted an artist link (Discogs/MusicBrainz parsed to a clean id, or Bandcamp/SoundCloud/Instagram/etc reverse-matched against the indexed seen.artist_link_index). 'name' = exact case-insensitive ?q= name match. 'locator' = you passed a structured id (?cluster=, ?discogs=, ?mbid=). WHY IT MATTERS: it tells you which branch of the front door produced the answer, so you know how strong the match is (a name match is fuzzier than a locator). GOTCHA: distinct from resolved_via — resolved_from is the INPUT route ('how you asked'), resolved_via is the OUTPUT binding tier ('how good the answer is'). Pair it with `matched_on` (which surface matched) and `note` (why a recognized link didn't resolve).","example":"url"},"matched_on":{"type":"string"},"note":{"type":"string"}},"required":["cluster_id","slug","display","locators","resolved_via","resolved_from"]},"TastemakersResponse":{"type":"object","properties":{"leaderboard":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"number"},"name":{"type":"string"},"discogsArtistId":{"type":["number","null"]},"slug":{"type":["string","null"]},"ownTier":{"type":"string","enum":["breakout","rising","climbing","steady"]},"degree":{"type":"number"},"emergingNeighbours":{"type":"number"},"pctEmerging":{"type":"number"},"brokerageScore":{"type":["number","null"]},"egoDensity":{"type":["number","null"]},"undergroundToBookingDays":{"type":["number","null"]},"pressToAirplayDays":{"type":["number","null"]},"earliestSupportedAt":{"type":["string","null"]},"demandCoverage":{"type":["number","null"]},"ownerReach":{"type":["number","null"]},"alsoOwnDegree":{"type":["number","null"]}},"required":["rank","name","discogsArtistId","slug","ownTier","degree","emergingNeighbours","pctEmerging","brokerageScore","egoDensity","undergroundToBookingDays","pressToAirplayDays","earliestSupportedAt","demandCoverage","ownerReach","alsoOwnDegree"]}},"onesToWatch":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"number"},"name":{"type":"string"},"discogsArtistId":{"type":["number","null"]},"slug":{"type":["string","null"]},"emergenceTier":{"type":"string","enum":["breakout","rising","climbing","steady"]},"momentumTier":{"type":"string","enum":["breakout","rising","steady","cooling","dormant"]},"emergenceScore":{"type":"number"},"profile":{"type":"string","enum":["network-velocity","festival-pedigree"]},"cultureForwardFestivals":{"type":"number"},"fiveFactorScore":{"type":["number","null"]},"degree":{"type":"number"},"linkable":{"type":"boolean"},"coFestivalCount":{"type":["number","null"]},"coCountryCount":{"type":["number","null"]},"undergroundToBookingDays":{"type":["number","null"]},"pressToAirplayDays":{"type":["number","null"]},"earliestSupportedAt":{"type":["string","null"]},"demandCoverage":{"type":["number","null"]},"ownerReach":{"type":["number","null"]},"alsoOwnDegree":{"type":["number","null"]},"brokerageScore":{"type":["number","null"]},"corroborationCount":{"type":["number","null"]},"corroboratingAxes":{"type":"array","items":{"type":"string"}}},"required":["rank","name","discogsArtistId","slug","emergenceTier","momentumTier","emergenceScore","profile","cultureForwardFestivals","fiveFactorScore","degree","linkable","coFestivalCount","coCountryCount","undergroundToBookingDays","pressToAirplayDays","earliestSupportedAt","demandCoverage","ownerReach","alsoOwnDegree","brokerageScore","corroborationCount","corroboratingAxes"]}},"earlySpinners":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"number"},"djName":{"type":"string"},"totalMastersPlayed":{"type":"number"},"mastersPlayedFirst":{"type":"number"},"mastersPlayedWithin7d":{"type":"number"},"cuttingEdgeScore":{"type":"number"},"firstPlayAt":{"type":["string","null"]},"latestPlayAt":{"type":["string","null"]}},"required":["rank","djName","totalMastersPlayed","mastersPlayedFirst","mastersPlayedWithin7d","cuttingEdgeScore","firstPlayAt","latestPlayAt"]}},"snapshotDate":{"type":["string","null"]},"state":{"type":"string","enum":["present","empty","degraded"]},"stale":{"type":"boolean"}},"required":["leaderboard","onesToWatch","earlySpinners","snapshotDate","state","stale"]},"OnesToWatchResponse":{"type":"object","properties":{"onesToWatch":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"number"},"name":{"type":"string"},"discogsArtistId":{"type":["number","null"]},"slug":{"type":["string","null"]},"emergenceTier":{"type":"string","enum":["breakout","rising","climbing","steady"]},"momentumTier":{"type":"string","enum":["breakout","rising","steady","cooling","dormant"]},"emergenceScore":{"type":"number"},"profile":{"type":"string","enum":["network-velocity","festival-pedigree"]},"cultureForwardFestivals":{"type":"number"},"fiveFactorScore":{"type":["number","null"]},"degree":{"type":"number"},"linkable":{"type":"boolean"},"coFestivalCount":{"type":["number","null"]},"coCountryCount":{"type":["number","null"]},"undergroundToBookingDays":{"type":["number","null"]},"pressToAirplayDays":{"type":["number","null"]},"earliestSupportedAt":{"type":["string","null"]},"demandCoverage":{"type":["number","null"]},"ownerReach":{"type":["number","null"]},"alsoOwnDegree":{"type":["number","null"]},"brokerageScore":{"type":["number","null"]},"corroborationCount":{"type":["number","null"]},"corroboratingAxes":{"type":"array","items":{"type":"string"}}},"required":["rank","name","discogsArtistId","slug","emergenceTier","momentumTier","emergenceScore","profile","cultureForwardFestivals","fiveFactorScore","degree","linkable","coFestivalCount","coCountryCount","undergroundToBookingDays","pressToAirplayDays","earliestSupportedAt","demandCoverage","ownerReach","alsoOwnDegree","brokerageScore","corroborationCount","corroboratingAxes"]}},"snapshotDate":{"type":["string","null"]},"state":{"type":"string","enum":["present","empty","degraded"]},"stale":{"type":"boolean"}},"required":["onesToWatch","snapshotDate","state","stale"]},"BreakoutsResponse":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"discogsArtistId":{"type":"number"},"clusterId":{"type":["string","null"]},"resolvedVia":{"type":"string","enum":["discogs","cluster"]},"name":{"type":"string"},"slug":{"type":"string"},"emergenceTier":{"type":"string","enum":["breakout","rising"]},"emergenceScore":{"type":"number"},"pressCount":{"type":"number"},"corroboration":{"type":"string","enum":["corroborated","booking_ahead"]},"location":{"type":["object","null"],"properties":{"city":{"type":["string","null"]},"region":{"type":["string","null"]},"country":{"type":["string","null"]}},"required":["city","region","country"]}},"required":["discogsArtistId","clusterId","resolvedVia","name","slug","emergenceTier","emergenceScore","pressCount","corroboration","location"]}},"state":{"type":"string","enum":["present","empty","degraded"]}},"required":["items","state"]},"WayfindAnswerRequest":{"type":"object","properties":{"question":{"type":"string","minLength":1,"maxLength":512}},"required":["question"]},"WayfindInterpretRequest":{"type":"object","properties":{"q":{"type":"string","minLength":1,"maxLength":256}},"required":["q"]}},"parameters":{}},"paths":{"/api/v1/search":{"get":{"summary":"Faceted search across the catalogue","description":"Faceted search across the catalogue. Requires an X-API-Key (post-cycle-078 wall); crate's own /explore + /crate search call it server-side via the first-party proxy. New params (cube_quadrant, dj_count_min, exclude_artist, exclude_label) added in cycle-001 sprint-1.","tags":["Search"],"parameters":[{"schema":{"type":"string","maxLength":256},"required":false,"name":"q","in":"query"},{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"genre","in":"query"},{"schema":{"type":"string","enum":["and","or"]},"required":false,"name":"genre_mode","in":"query"},{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"style","in":"query"},{"schema":{"type":"string","enum":["and","or"]},"required":false,"name":"style_mode","in":"query"},{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"format","in":"query"},{"schema":{"type":"string","enum":["and","or"]},"required":false,"name":"format_mode","in":"query"},{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"country","in":"query"},{"schema":{"type":"string","enum":["and","or"]},"required":false,"name":"country_mode","in":"query"},{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"label","in":"query"},{"schema":{"type":"string","enum":["and","or"]},"required":false,"name":"label_mode","in":"query"},{"schema":{"type":"integer","minimum":1900,"maximum":2100},"required":false,"name":"year_from","in":"query"},{"schema":{"type":"integer","minimum":1900,"maximum":2100},"required":false,"name":"year_to","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"maximum":10000,"default":0},"required":false,"name":"offset","in":"query"},{"schema":{"anyOf":[{"type":"string","pattern":"^[01]{3}$"},{"type":"array","items":{"type":"string","pattern":"^[01]{3}$"},"minItems":1,"maxItems":8}]},"required":false,"name":"cube_quadrant","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"maximum":1000000},"required":false,"name":"dj_count_min","in":"query"},{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"exclude_artist","in":"query"},{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"exclude_label","in":"query"}],"responses":{"200":{"description":"Search results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"searchCatalogue"}},"/api/v1/masters/{id}":{"get":{"summary":"Single master enrichment","tags":["Masters"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"maximum":2000000000},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Master enrichment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MasterEnrichment"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Payment required (past_due customer or suspended key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getMaster"}},"/api/v1/masters/batch":{"post":{"summary":"Batch master enrichment (≤100 IDs)","description":"Counts as ONE request against rate limit (not N). Body size capped at 32 KB.","tags":["Masters"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"integer","exclusiveMinimum":0,"maximum":2000000000},"minItems":1,"maxItems":100}},"required":["ids"],"additionalProperties":false}}}},"responses":{"200":{"description":"Batch enrichment results (input-order preserved)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Payment required (past_due customer or suspended key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Payload too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"batchMasters"}},"/api/v1/dossier/manifest":{"get":{"summary":"Cross-grain dossier field manifest (data dictionary)","description":"cycle-023 — every field crate can expose for every entity grain (master/artist/label) with provenance (producer, source table, refresh cadence, tier, honest-gap state), plus unavailable grains (song — no fleet track key, carrefour#71). The machine-readable Swagger-like overview; read it to discover the full surface without hitting every entity endpoint. Keyed (X-API-Key), pure/static.","tags":["Dossier"],"responses":{"200":{"description":"Dossier field manifest","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DossierManifest"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getDossierManifest"}},"/api/v1/dossier/master/{id}":{"get":{"summary":"Full per-master dossier contract (aggregation gateway)","description":"cycle-020 — the canonical exhaustive dossier: every fleet signal for the master in every state (present/absent/blind/pending honest-gap) plus a provenance manifest (producer, source table, refresh cadence, tier). Keyed (X-API-Key) (same posture as the public /crate/master page). Distinct from /api/v1/masters/{id} (the older basic enrichment).","tags":["Dossier"],"parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"maximum":2000000000},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Master dossier contract","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MasterDossierContract"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getMasterDossier"}},"/api/v1/dossier/artist/{slug}":{"get":{"summary":"Full per-artist dossier contract (aggregation gateway)","description":"cycle-021 — the canonical exhaustive artist dossier: identity (cc0_artists), collector behavior (mirror.artist_signals_v1), editorial/event (seen.artist_dossier), and artist-level \"Across the Web\" (mirror.master_links_rollup aggregated), each with a classified state, plus a provenance manifest. Keyed (X-API-Key) (matches the public /crate/artist page). An unresolved slug returns identity:null (HTTP 200, honest-gap), not 404.","tags":["Dossier"],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"Artist dossier contract","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtistDossierContract"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getArtistDossier"}},"/api/v1/dossier/label/{slug}":{"get":{"summary":"Full per-label dossier contract (aggregation gateway)","description":"cycle-022 → cycle-L1 — the cluster-first label dossier: cc0_labels identity + label_identity_v1 cross-source binds (cluster_id prime key, discogs/mbid/bandcamp leaves, over-merge honesty) + label_signals_v1 collector behavior + bandcamp_label_travels_v1 co-ownership adjacency, each with a classified state, plus a provenance manifest. Keyed (X-API-Key). Unresolved slug → identity:null (HTTP 200, honest-gap), not 404. Alias of GET /api/v1/label/{key} (the canonical cluster-first address).","tags":["Dossier"],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"Label dossier contract","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabelDossierContract"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getLabelDossier"}},"/api/v1/label/{key}":{"get":{"summary":"Canonical label dossier by cluster_id hex or slug","description":"cycle-L1 (carrefour#105) — the cluster-first canonical label address. {key} is EITHER a 64-hex label_cluster_id (the fleet-wide prime key; resolves DIRECTLY from label_identity_v1, OBSERVED tier, never re-anchors on a same-name Discogs row — INV-020) OR a name-slug (cc0_labels exact-name → forward-crosswalk to the cluster; byte-identical to /dossier/label/{slug}). discogs:/mbid: locators → 400 invalid_label_key (labels are addressed by cluster_id-hex or name-slug; /api/v1/resolve is artist-only this cycle). Keyed (X-API-Key). Unresolved key → identity:null (HTTP 200, honest-gap), not 404.","tags":["Dossier","Identity"],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"key","in":"path"}],"responses":{"200":{"description":"Label dossier contract","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabelDossierContract"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getLabelByKey"}},"/api/v1/dossier/festival/{slug}":{"get":{"summary":"Full per-festival dossier contract (aggregation gateway)","description":"cycle-060 — the canonical festival dossier: identity (seen.festival_canonical, the de-fragmented grain: display name, country, inclusion tier, year span, source-mix badges) + the consolidated editions⋈lineup across member festivals (seen.festival_editions/festival_lineup_entries), with a classified state, plus a provenance manifest. The {slug} is the canonical_key. Keyed (X-API-Key). Unresolved key → identity:null (HTTP 200, honest-gap), not 404. Lineup is name-only this cycle.","tags":["Dossier"],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"Festival dossier contract","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FestivalDossierContract"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getFestivalDossier"}},"/api/v1/bandcamp/{artistKey}":{"get":{"summary":"Flat Bandcamp feed for one artist — every granted source, verbatim columns","description":"cycle-064 — the maximally-flexible Bandcamp feed: every crate_reader-granted Bandcamp surface (mirror signals/emergence/identity/signals_mbid/early_supporter_quality/scene/travels_with/self_declared_links/lead_time_authority + seen gravity/tastemaker) returned FLAT and source-tagged with ALL columns verbatim (the consumer reshapes). artistKey = <64-hex cluster_id> | discogs:<int> | mbid:<uuid>. Keyed (X-API-Key). Data is k-anon/public-safe; counts arrive already-floored — do NOT re-floor. Unresolved/sparse → 200 with the relevant sources {present:false}. Malformed key → 400.","tags":["Bandcamp"],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":80},"required":true,"name":"artistKey","in":"path"}],"responses":{"200":{"description":"Flat source-tagged Bandcamp feed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BandcampFeedContract"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getBandcampArtist"}},"/api/v1/bandcamp":{"get":{"summary":"Bandcamp feed index (no params) + keyset-paginated single-source bulk","description":"cycle-064/065 — called with NO query params, returns a self-describing MANIFEST (index of everything available: every source + grain/key/k-anon, the per-artist endpoint, and how to paginate) so a keyless consumer can discover the whole surface. With ?source=<name>&cursor=<64hex>&limit=<1..200> (default source=signals_mbid, limit=50) returns keyset-paginated rows of one cluster_id-PK source, all columns verbatim — page the full corpus via next_cursor. Keyed (X-API-Key). Unknown/non-paginable source or malformed cursor → 400.","tags":["Bandcamp"],"parameters":[{"schema":{"type":"string"},"required":false,"name":"source","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"One page of a single Bandcamp source","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BandcampBulkPage"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getBandcampFeedIndex"}},"/api/v1/bandcamp/release":{"get":{"summary":"Per-release Bandcamp dossier (title, date, cover, tracklist, tags, album URL)","description":"cycle-080 — the Bandcamp release layer, served verbatim from mirror's release-grain VIEWs (bandcamp_release_v1 + bandcamp_release_track_v1, mig 0189) + the per-release cover (bandcamp_artwork_v1). Query ONE of (precedence item → url → cluster): ?item=<bandcamp_item_id> (direct → ONE full dossier with tracks) | ?url=<bandcamp album url> (normalized exact source_url match → ONE dossier) | ?cluster_id=<64-hex> (the artist's releases as a summary LIST, no tracks — discovery; each row carries bandcamp_item_id, fetch ?item= for the full dossier). Honest gaps: per-track DIRECT stream URL is not stored (Bandcamp streams are tokenized/expiring + ToS) — track_url is the track PAGE; label/catalog are not in the crawl. Keyed (X-API-Key); ONE crate_reader checkout; never fetches any URL. Unresolved → 200 present:false / empty releases (honest-gap), NOT 404. Malformed/missing → 400.","tags":["Bandcamp"],"parameters":[{"schema":{"type":"string"},"required":false,"name":"item","in":"query"},{"schema":{"type":"string"},"required":false,"name":"url","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cluster_id","in":"query"}],"responses":{"200":{"description":"A single release dossier (item/url) or the artist release list (cluster_id); honest-gap when unresolved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BandcampReleaseResponse"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getBandcampRelease"}},"/api/v1":{"get":{"summary":"API root index — start here (cold-start recipe + resource map)","description":"cycle-072 — a friendly, self-describing root: the cold-start recipe (resolve a name/link → cluster_id → dossier) followed by every product surface with its auth tier + how to get the key. PURE (no DB), edge-cacheable. The human-facing sibling of /api/v1/openapi.json.","tags":["Meta"],"security":[],"responses":{"200":{"description":"API root index","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiRootIndex"}}}}},"operationId":"getApiIndex"}},"/api/v1/signup":{"post":{"summary":"Request a free-tier sign-in link (self-serve)","description":"cycle-C — free-tier self-serve, step 1. PUBLIC (no API key). POST { email } → a magic sign-in link is emailed (valid 30 min). NO account/key is created until the link is verified at /api/v1/signup/verify. Anti-enumeration: a well-formed email always returns 200 regardless of whether it already has an account or whether the email was actually sent. Abuse-bounded by per-IP + per-email + global SES limiters (not an API key).","tags":["Signup"],"security":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}}},"responses":{"200":{"description":"Accepted — if the email is eligible, a sign-in link was emailed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupAccepted"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}}},"operationId":"requestSignupLink"}},"/api/v1/signup/verify":{"get":{"summary":"Verify a free-tier sign-in link → dashboard","description":"cycle-C — free-tier self-serve, step 2. PUBLIC. GET ?token=<magic-link token> → on success sets the dashboard session cookie and 303-redirects to /api/dashboard (where you mint your key). An existing customer of any tier is logged in (read-only, no mutation); a new email creates a free customer (idempotent). An invalid/expired/disabled link 303-redirects to /signup with a friendly error.","tags":["Signup"],"security":[],"parameters":[{"schema":{"type":"string"},"required":true,"name":"token","in":"query"}],"responses":{"303":{"description":"Redirect to /api/dashboard (session set) or /signup (friendly error)"}},"operationId":"verifySignupLink"}},"/api/v1/resolve":{"get":{"summary":"Resolve any identifier (name · pasted link · id) → canonical cluster_id","description":"The cluster-first front door (cycle-069/070/071): converts ANY identifier a caller holds into the canonical cluster_id + a human slug + the full locator set, WITHOUT knowing crate internals. Query ONE of: ?url=<any artist link> (discogs/musicbrainz → clean id; bandcamp/soundcloud/instagram/website/spotify/youtube → reverse-match the indexed seen.artist_link_index; twitter/reddit recognized but not yet cross-referenced → honest note + name fallback) | ?q=<artist name> (exact case-insensitive) | ?cluster=<64-hex> | ?discogs=<int> | ?mbid=<uuid>. On a resolved cluster, locators returns the full public profile-link set per platform (cycle-079, link-only forward read). resolved_from tells you how it matched ('url'|'name'|'locator'), matched_on which surface, note explains an unresolved recognized link. resolved_via = best binding tier ('discogs' verified, else 'cluster' observed, else null). Keyed (X-API-Key); ONE crate_reader checkout; never fetches the pasted URL. Unresolved → 200 with nulls (honest-gap), not 404. Malformed/missing → 400.","tags":["Identity"],"parameters":[{"schema":{"type":"string"},"required":false,"name":"url","in":"query"},{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cluster","in":"query"},{"schema":{"type":"string"},"required":false,"name":"discogs","in":"query"},{"schema":{"type":"string"},"required":false,"name":"mbid","in":"query"}],"responses":{"200":{"description":"Resolved identity (or nulls when unresolved)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityResolution"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"resolveIdentity"}},"/api/v1/artist/{key}":{"get":{"summary":"Cluster-first canonical artist dossier (cluster_id hex OR slug)","description":"cycle-069 (API v2 P1) — the same exhaustive artist dossier as /dossier/artist/{slug}, addressed by the canonical cluster_id (64-hex, prime key) OR a human slug. A 64-hex key resolves identity DIRECTLY from the cluster_id (OBSERVED tier), skipping the cc0_artists lookup so a hex entry never re-anchors onto a same-name Discogs row → resolved_via is always 'cluster'. A slug resolves via the name path (resolved_via 'discogs' or 'cluster'). discogs:/mbid: locators are not a canonical address — convert them via /api/v1/resolve first (→ 400 here). Keyed (X-API-Key); unresolved → 200 identity:null (honest-gap), not 404. /dossier/artist/{slug} remains a live alias.","tags":["Dossier","Identity"],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"key","in":"path"}],"responses":{"200":{"description":"Artist dossier contract (same shape as /dossier/artist/{slug})","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtistDossierContract"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getArtistByKey"}},"/api/v1/tastemakers":{"get":{"summary":"Tastemaker leaderboard + ones-to-watch (machine-readable)","description":"cycle-048 E1 — crate's richest artist-grain analytics (rank, ownTier, brokerageScore, corroborating axes, lead-times, Bandcamp demand), the SAME derived data the public /crate/tastemakers page renders. Read from the offline-published S3 snapshot (NO DB checkout). Keyed (X-API-Key). Fail-soft: `state` is `present` (rows), `empty` (derived data present but no rows), or `degraded` (derived data absent/unreadable) — degraded returns HTTP 200 honest-gap, never a 500. `stale=true` flags a snapshot older than 7 days. `?limit=` bounds each array (1..200).","tags":["Tastemakers"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Max rows (1..200)."},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Tastemaker leaderboard + ones-to-watch (or honest-gap state)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TastemakersResponse"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getTastemakers"}},"/api/v1/tastemakers/ones-to-watch":{"get":{"summary":"Ones-to-watch rising artists (machine-readable slice)","description":"cycle-048 E1 — the ones-to-watch slice on its own surface (mirrors the /crate/tastemakers page section): rising/breakout artists with emergence + momentum tiers, five-factor score, corroborating axes, and Bandcamp demand. Same derived S3 read as /api/v1/tastemakers, NO DB checkout. Keyed (X-API-Key), fail-soft (degraded → HTTP 200). `?limit=` bounds the array (1..200).","tags":["Tastemakers"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Max rows (1..200)."},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Ones-to-watch rows (or honest-gap state)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnesToWatchResponse"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getOnesToWatch"}},"/api/v1/breakouts":{"get":{"summary":"Breakouts index — emerging artists (machine-readable)","description":"cycle-048 E2 — the breakouts index (booking-momentum \"ones to watch\" from seen.artist_emergence, cross-validated against ridden press): the SAME single-checkout read the public /crate/breakouts explorer renders. Keyed (X-API-Key). Fail-soft: `state` is `present`/`empty`/`degraded` — a read failure returns HTTP 200 honest-gap, never a 500. `?limit=` is clamped to 200 (never an unbounded scan). `?tier=breakout|rising` and `?corroboration=corroborated|booking_ahead` filter the returned list (unknown values are ignored).","tags":["Discovery"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Max rows (clamped to 200)."},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","enum":["breakout","rising"]},"required":false,"name":"tier","in":"query"},{"schema":{"type":"string","enum":["corroborated","booking_ahead"]},"required":false,"name":"corroboration","in":"query"}],"responses":{"200":{"description":"Breakouts index items (or honest-gap state)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakoutsResponse"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getBreakouts"}},"/api/v1/facets":{"get":{"summary":"Precomputed facet snapshot","description":"Returns the same shape as `SearchResponse.facets`, narrowed by optional genre/style/country/year_from/year_to. format/label are NOT supported (see SDD §888).","tags":["Facets"],"security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"genre","in":"query"},{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"style","in":"query"},{"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"array","items":{"type":"string","minLength":1,"maxLength":256},"minItems":1,"maxItems":20}]},"required":false,"name":"country","in":"query"},{"schema":{"type":"integer","minimum":1900,"maximum":2100},"required":false,"name":"year_from","in":"query"},{"schema":{"type":"integer","minimum":1900,"maximum":2100},"required":false,"name":"year_to","in":"query"}],"responses":{"200":{"description":"Facet counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacetCounts"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Payment required (past_due customer or suspended key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Request deadline (15s) or query timeout exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getFacets"}},"/api/v1/usage":{"get":{"summary":"Per-customer monthly usage snapshot","tags":["Usage"],"security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Usage snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Payment required (past_due customer or suspended key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"getUsage"}},"/api/v1/wayfind/answer":{"post":{"summary":"Natural-language answer over the catalogue","description":"cycle-003 — synthesizes a natural-language answer plus supporting catalogue results for a free-text question. Requires an X-API-Key (post-cycle-078 wall); crate's own /crate NL box reaches it server-side via the first-party proxy. A payment-blocked customer (past_due/suspended) receives 402. On synthesis/Redis trouble it returns a degraded 200 fallback rather than failing; a hard cost-breaker outage returns 503. (WAF rate rules + a cost-breaker still bound traffic at the edge.)","tags":["Wayfind"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WayfindAnswerRequest"}}}},"responses":{"200":{"description":"Synthesized answer (or degraded fallback) + supporting results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WayfindAnswerResponse"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Payment required (past_due customer or suspended key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"wayfindAnswer"}},"/api/v1/wayfind/interpret":{"post":{"summary":"Interpret a natural-language query into structured search params","description":"cycle-003 — parses a free-text query into structured facets/intent. Authenticated AND Sync-tier-only (mirrors the /search?nl=true tier gate); a non-Sync key receives an explicit 401 `tier_not_eligible`.","tags":["Wayfind"],"security":[{"ApiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WayfindInterpretRequest"}}}},"responses":{"200":{"description":"Structured interpretation (or degraded:null block)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WayfindInterpretResponse"}}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure (invalid query, malformed body, bad facet name)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Payment required (past_due customer or suspended key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded — see Retry-After + X-RateLimit-* headers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"503":{"description":"Database pool exhausted — retry after 5s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"operationId":"wayfindInterpret"}},"/api/v1/search-events/observed":{"post":{"summary":"Beacon: a search result was observed (cache-hit telemetry)","description":"cycle-002 — client beacon reporting that a search result was observed from cache. Authenticated with the per-search beacon JWT (Authorization: Bearer), NOT the X-API-Key. Body ≤512 bytes; idempotent (duplicate beacons are no-ops). The beacon token MUST match the body search_event_id.","tags":["Beacon"],"security":[{"BeaconBearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObservedBeaconRequest"}}}},"responses":{"204":{"description":"Beacon accepted (idempotent no-op on duplicate)","headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure — `invalid_json`, `invalid_payload` (with `details` = Zod flattened error object), or `timestamp_skew` (with `skew_ms`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BeaconError"}}}},"401":{"description":"Beacon-token failure — `missing_token`, `invalid_token`, or `token_search_event_mismatch`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BeaconError"}}}},"413":{"description":"Payload too large (>512 bytes)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BeaconError"}}}}},"operationId":"recordObservedBeacon"}},"/api/v1/search-events/refined":{"post":{"summary":"Beacon: a search was refined (facet-change telemetry)","description":"cycle-002 — client beacon reporting facet changes that refined a prior search (≤10 changed facets). Authenticated with the per-search beacon JWT (Authorization: Bearer), NOT the X-API-Key. Body ≤512 bytes; idempotent. The beacon token MUST match the body search_event_id.","tags":["Beacon"],"security":[{"BeaconBearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefinedBeaconRequest"}}}},"responses":{"204":{"description":"Beacon accepted (idempotent no-op on duplicate)","headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}}}},"400":{"description":"Validation failure — `invalid_json`, `invalid_payload` (with `details` = Zod flattened error object), or `timestamp_skew` (with `skew_ms`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BeaconError"}}}},"401":{"description":"Beacon-token failure — `missing_token`, `invalid_token`, or `token_search_event_mismatch`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BeaconError"}}}},"413":{"description":"Payload too large (>512 bytes)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BeaconError"}}}}},"operationId":"recordRefinedBeacon"}},"/api/v1/openapi.json":{"get":{"summary":"OpenAPI 3.1 specification (this document)","description":"Public endpoint, no auth required.","tags":["Spec"],"security":[],"responses":{"200":{"description":"OpenAPI 3.1 spec","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}}},"operationId":"getOpenApiSpec"}}},"webhooks":{},"x-concepts":[{"term":"cluster_id","eli5":"crate's one true name for an artist — a pe-norm-v1 hex string that collapses the same artist's Discogs, MusicBrainz, and Bandcamp identities into a single canonical key. Key all artist data off it. It's an opaque string: pass it through verbatim, never numericize it. cluster_id:null is an honest gap (couldn't resolve), not an error.","see":"IdentityResolution (getResolve, GET /api/v1/resolve); ArtistDossierContract.cluster_id (GET /api/v1/artist/{key})"},{"term":"dossier","eli5":"The full picture crate composes for one entity by joining every fleet signal. Each facet carries a classified state plus a provenance manifest (producer, source table, refresh cadence, tier). Read /dossier/manifest to discover the whole field surface without calling every endpoint.","see":"DossierManifest (GET /api/v1/dossier/manifest); ArtistDossierContract / MasterDossierContract / LabelDossierContract / FestivalDossierContract"},{"term":"grain","eli5":"Which kind of entity a dossier is about: artist, master (a Discogs release-group, i.e. a 'release' in the consumer sense), label, or festival. The dossier manifest also lists deliberately-unavailable grains (e.g. song — no fleet track key).","see":"GET /api/v1/dossier/artist/{slug}, /dossier/master/{id}, /dossier/label/{slug}, /dossier/festival/{slug}; DossierManifest.unavailable_grains"},{"term":"honest gap","eli5":"An unresolved or empty lookup returns HTTP 200 with present:false, a null field, or state:\"honest_gap\" — this is NORMAL, not an error. crate shows what it can see and is explicit about what it can't, rather than 404-ing or faking data. Only 4xx/5xx are errors; branch on the body, not the status.","see":"Error schema (the inverse: only 4xx/5xx); ArtistDossierContract.identity:null; BandcampReleaseResponse present:false variant"},{"term":"resolved_via","eli5":"The binding TIER of a resolved identity: 'discogs' = canonical Discogs-bound (verified); 'cluster' = OBSERVED/unverified from the booking graph (surface flagged-unverified, never as canonical); null = unresolved. A 64-hex cluster_id address is always 'cluster' by design.","see":"IdentityResolution.resolved_via; ArtistDossierContract.resolved_via"},{"term":"resolved_from","eli5":"How you addressed the artist on /resolve: 'url' (pasted link), 'name', or 'locator' (a foreign id). Paired with matched_on (which surface matched) and note (why a recognized link didn't resolve).","see":"IdentityResolution.resolved_from (GET /api/v1/resolve)"},{"term":"cube / cube_quadrant","eli5":"crate's behavioral-signal model: a 3-bit string code (e.g. \"101\") placing a master on three axes — who OWNS it (collector), who PLAYS it (DJ), who WRITES about it (critic). Codes run \"000\" (no signal) to \"111\" (full intersection). null = not yet classified (honest gap). owner_count/dj_count/critic_count give the magnitude.","see":"ResultRow.cube_quadrant + owner_count/dj_count/critic_count; CubeQuadrant schema; MasterDossierContract.header.cube_quadrant"},{"term":"tastemakers","eli5":"A discovery surface of influential curators plus crate's richest artist-grain analytics (rank, own-tier, brokerage score, lead-times, Bandcamp demand). Served from an offline snapshot, fail-soft: state is present/empty/degraded (degraded = 200 honest-gap), stale=true if older than 7 days.","see":"TastemakersResponse (GET /api/v1/tastemakers); OnesToWatchResponse (GET /api/v1/tastemakers/ones-to-watch)"},{"term":"breakouts","eli5":"A discovery surface of emerging artists on the rise ('ones to watch') — booking momentum cross-validated against press. Filter by ?tier=breakout|rising and ?corroboration=corroborated|booking_ahead; ?limit= clamped to 200. Fail-soft state present/empty/degraded.","see":"BreakoutsResponse (GET /api/v1/breakouts)"},{"term":"beacons","eli5":"Client-side search-event telemetry (observed = cache hit, refined = facet change). Authenticated NOT with X-API-Key but with a short-lived per-search JWT bound to one search_event_id, sent as Authorization: Bearer. Bodies <=512 bytes, idempotent (duplicate = 204 no-op).","see":"ObservedBeaconRequest / RefinedBeaconRequest (POST /api/v1/search-events/observed, /refined); BeaconBearerAuth security scheme; BeaconError"},{"term":"bandcamp_item_id","eli5":"A Bandcamp release id — a bigint rendered AS A STRING. Opaque: pass it through verbatim, never numericize it. It's the per-release key you fetch the full dossier with (?item=).","see":"BandcampRelease.bandcamp_item_id; BandcampReleaseSummary; GET /api/v1/bandcamp/release"},{"term":"track_url","eli5":"A Bandcamp track PAGE url — NOT a playable/stream URL. Bandcamp streams are tokenized and expiring (and out of ToS), so crate never stores them. The same link-only posture covers artwork (rehost:false hotlinks crate never re-hosts).","see":"BandcampTrack.track_url; ArtworkItem (rehost:false)"},{"term":"next_cursor","eli5":"An opaque keyset-pagination cursor (string) for the bulk Bandcamp feed. Pass it back verbatim on the next request; never construct or decode it. null = last page. Call /bandcamp with no params for the self-describing source manifest.","see":"BandcampBulkPage.next_cursor (GET /api/v1/bandcamp?source=...)"},{"term":"opaque ids","eli5":"cluster_id, bandcamp_item_id, and pagination cursors are always strings and always opaque — round-trip them verbatim, never parse, increment, or numericize. This keeps clients correct across id-scheme changes.","see":"IdentityResolution.cluster_id; BandcampRelease.bandcamp_item_id; BandcampBulkPage.next_cursor"},{"term":"versioning","eli5":"The API major version is the URL path (/api/v1). The spec's info.version (1.4.0) bumps on every spec change and is drift-guarded because the document is generated from code. Operations carry stable operationIds; keyed 2xx responses declare X-RateLimit-* headers.","see":"info.version; GET /api/v1/openapi.json; RATE_LIMIT_RESPONSE_HEADERS on keyed operations"}]}