{"openapi":"3.1.0","info":{"title":"Credits API","version":"1.2.2","description":"Music Credits + Identifier Search. **Interactive docs (Scalar):** `/docs`. **OpenAPI JSON:** `/_openapi.json`.\n\n## Servers and paths\n\n- **API:** `/v1/*` — **snake_case** JSON. Scalar lists these operations under **Canonical API (v1)**.\n- **This specification** (as served at `/_openapi.json` and `/docs`) documents the **`/v1/*`** surface plus discovery, redirects, and shared infrastructure — not alternate version prefixes.\n- **Production API host** (`https://api.credits.fm`): use paths as in this document (e.g. `GET /v1/search`). Do **not** call `https://api.credits.fm/api/search` for Try-it — unversioned `/api/*` here **308** to `/v1/*` (see **Redirects** tag).\n- **credits.fm (Next.js)** exposes camelCase JSON under `https://credits.fm/api/*`. Conceptually map `GET /v1/search` ↔ `GET https://credits.fm/api/search` by swapping the prefix; casing follows each host (see discovery `GET /`).\n\n## Contribute by default\n\nContributing is **opt-out**, not opt-in. Lookup responses are designed to highlight data gaps so clients can submit improvements.\n\n- **GET** `/v1/isrc/{isrc}`, `/v1/iswc/{iswc}`, `/v1/ipi/{ipi}`, `/v1/isni/{isni}`: unless you pass **`?contribute=false`**, successful responses may include **`missing_fields`** (array) and **`contribute_url`** when gaps exist.\n- **POST** `/v1/batch`: include **`\"contribute\": false`** in the JSON body to omit **`contribute_url`** in the response. Default is to include it.\n- **POST** `/v1/resolve/track`: include **`\"contribute\": false`** in the body to omit **`missing_fields`** / **`contribute_url`** on success. Default is on.\n- **MCP** (`/v1/mcp` and unversioned `/mcp`): lookup tools accept **`contribute: false`** in tool arguments to opt out of contribution nudges; the server instructions describe the default behavior.\n\nActual writes use **POST /v1/contribute** with a valid **`x-api-key`** (see the Contributions tag). A dedicated public UI for this API may ship later; for now, **Scalar** documents behavior here.\n\n## Music Identity graph detail — pagination & expansion\n\nSingle-resource **graph** responses are served from **identifier paths** — **`/v1/iswc/{iswc}`**, **`/v1/isrc/{isrc}`**, **`/v1/upc/{upc}`**, **`/v1/isni/{isni}`**, **`/v1/ipi/{ipi}`** — with the same query parameters. Older bookmarked human-readable URL segments may still **308** to these paths in production; this specification lists the canonical routes only. **Identifier** routes accept:\n\n- **`include`**, **`relationships`**, **`relationship`** — merged; comma-separated relationship keys; **`include=all`** expands every edge the handler supports.\n- **`depth`** — 1–5 (default 1); controls how far nested objects expand (e.g. plain ISRC codes vs `{ isrc, title, … }`). On **UPC graph** (**`/v1/upc/{upc}`**) with **`include=songs`**, **`depth=2`** adds each work’s recordings, writers, and publishers under that song.\n- **`limit`** / **`offset`** — paginate **relationship arrays** only (default **50** / **0**); **`limit=-1`** returns full lists. Each expanded list includes a matching **`total_*`** field (e.g. **`total_recordings`**) for client-side paging.\n\nSee **`docs/MUSIC_IDENTITY_API.md`** and **`docs/PAGINATION.md`** for aliases and edge cases.\n\n## API keys (do not confuse with “wrong Supabase”)\n\n- **Read-heavy routes** (`GET /v1/isrc/…`, `POST /v1/batch`, `GET /v1/search`, …) are **public**. They do **not** validate `x-api-key`; a **200** from batch or a lookup **does not** prove your key is registered.\n- **Writes / key management** (`POST /v1/contribute`, `POST /v1/keys`) **require** a valid `cfm_…` key stored in **`api_keys`** on the **same** Supabase project as the index (`isrc_index`, …). If contribute returns **401** while batch returns **200**, treat that as **key resolution** (missing header, wrong prefix, revoked key, typo) — **not** as evidence of two different databases: **contributions** and **api_keys** use the same `Supabase()` client as batch lookups.\n- **Anonymous tier** cannot contribute (`contribute` rate limit `0`/min); you need a **free** (or internal) key for `POST /v1/contribute`."},"servers":[{"url":"https://api.credits.fm","description":"Production (standalone Nitro). Documented paths are under **`/v1/*`** (e.g. `/v1/search`, `/v1/suggest`). Do not use `/api/...` on this host for Try-it — unversioned `/api/*` **308**s to **`/v1/*`."}],"paths":{"":{"get":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}}},"/api/{path}":{"get":{"tags":[],"parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"summary":"308 redirects — /api/* → versioned routes at host root"}},"/healthz":{"get":{"tags":["Health"],"parameters":[],"responses":{"200":{"description":"Liveness OK (body may indicate degraded/critical)"}},"summary":"Liveness check (Render)","description":"Always returns 200 so load balancers succeed. Body contains status, database, and table health."}},"/":{"get":{"tags":["Discovery"],"parameters":[],"responses":{"200":{"description":"JSON with `service`, `version`, `transition`, `docs` (canonical **`v1`** templates), `links` (including `mcp_docs` → `/mcp/docs` → `/docs`)."}},"summary":"API discovery","description":"Returns service metadata and **`/v1/*`** URL templates for health, search, lookups, batch, MCP, and Music Identity. Interactive reference: **GET /docs**. Contribution hints on lookups are opt-out (see top-level API description)."}},"/mcp":{"delete":{"tags":["MCP"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"DELETE /mcp — session teardown (alias of /v1/mcp)"},"get":{"tags":["MCP"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"GET /mcp — unversioned alias of /v1/mcp","description":"Re-exports **`GET /v1/mcp`**. Prefer **`/v1/mcp`** in documentation and new clients."},"options":{"tags":["MCP"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"OPTIONS /mcp — CORS preflight (alias of /v1/mcp)"},"post":{"tags":["MCP"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"POST /mcp — unversioned alias of /v1/mcp","description":"Re-exports **`POST /v1/mcp`**. Prefer **`/v1/mcp`** for new integrations."}},"/mcp/docs":{"get":{"tags":["Discovery"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"GET /mcp/docs — redirect to Scalar /docs","description":"**302** to **`/docs`** (Scalar UI)."}},"/v1/artist/{id}":{"get":{"tags":["Music Identity"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^[a-zA-Z0-9]{22}$","example":"1dfeR4HaWDbWqFHLkxsg1d"},"description":"Spotify artist ID (22 chars)"}],"responses":{"200":{"description":"Artist discography (snake_case)"},"400":{"description":"Invalid Spotify artist ID"},"404":{"description":"Artist not found"},"500":{"description":"Internal error"},"503":{"description":"Spotify not configured"}},"summary":"Get artist discography by Spotify ID","description":"Spotify catalog + ISRC backfill from Supabase (`isrc_index` / MusicBrainz). Requires SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET."}},"/v1/audit/catalog":{"post":{"tags":["Audit"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Publisher-catalog audit (two-file classification)","description":"Classify a publisher catalog into recording-match issues and composition-registration (duplicate registration) issues. Each row carries the metadata that drove its classification: recording title + writers + IPIs from the ISRC side, plus the full related-works group context from the assigned MLC Song Code. Pass `?format=csv` for a combined-CSV response with two clearly-marked sections."}},"/v1/audit/classify":{"post":{"tags":["Audit"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Classify one ISRC (audit)","description":"Single-row catalog-audit classification. Same scoring as /v1/audit/catalog but for one ISRC. Returns the row's recording-match score, composition-registration flag, and the full related-works group context."}},"/v1/audit/shares":{"options":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}},"post":{"tags":["Audit"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Share audit (batch)"}},"/v1/audit/unmatched":{"options":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}},"post":{"tags":["Audit"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Unmatched audit (batch)"}},"/v1/batch":{"options":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}},"post":{"tags":["Batch"],"parameters":[],"responses":{"400":{"description":"Invalid body or no identifier arrays"},"503":{"description":"Database unavailable"}},"summary":"Batch lookup","security":[],"description":"Look up multiple identifiers in one request. Send JSON with one or more of: isrcs, iswcs, ipis, isnis, upcs (each array max 100, total across arrays ≤ 100). Returns maps keyed by code. Top-level `contribute_url` is included unless `contribute: false` in the body.\n\n**Auth:** this route does **not** read or validate `x-api-key`. Sending a key has **no effect** on authorization. A **200** here does **not** prove your API key is valid; use `POST /v1/contribute` (or `POST /v1/keys`) to verify credentials.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"isrcs":{"type":"array","items":{"type":"string"},"maxItems":100},"iswcs":{"type":"array","items":{"type":"string"},"maxItems":100},"ipis":{"type":"array","items":{"type":"string"},"maxItems":100},"isnis":{"type":"array","items":{"type":"string"},"maxItems":100},"upcs":{"type":"array","items":{"type":"string"},"maxItems":100},"contribute":{"type":"boolean","description":"Include contribute_url in response (default true). Set false to opt out."},"include_mlc_works":{"type":"boolean","description":"Augment ISRC results with live MLC Public Work Search data. Cache-respecting (14-day); only stale rows hit MLC. Capped at 25 ISRCs/request."}}}}}}}},"/v1/billing/checkout":{"post":{"tags":["Billing"],"parameters":[],"responses":{"200":{"description":"{ url } — redirect the user here"},"400":{"description":"Invalid plan"},"401":{"description":"Missing or invalid API key"},"503":{"description":"Billing not configured"}},"summary":"Start Credits Graph checkout","description":"Create a Stripe Checkout session for a paid Credits Graph plan (`pro` or `partner`). Authenticate with your full `x-api-key`. Returns the hosted Checkout URL."}},"/v1/billing/portal":{"post":{"tags":["Billing"],"parameters":[],"responses":{"200":{"description":"{ url }"},"401":{"description":"Missing or invalid API key"},"404":{"description":"No billing customer for that key"},"503":{"description":"Billing not configured"}},"summary":"Open billing portal","description":"Create a Stripe Customer Portal session for the key presented via `x-api-key`. Returns the hosted portal URL."}},"/v1/billing/webhook":{"post":{"tags":["Billing"],"parameters":[],"responses":{"200":{"description":"Acknowledged"},"400":{"description":"Signature verification failed"}},"summary":"Stripe webhook","description":"Internal Stripe webhook receiver. Not for public use."}},"/v1/catalog/analyze":{"post":{"tags":["Catalog"],"parameters":[],"responses":{"200":{"description":"Catalog analysis result (publisher → writer-IPI → ISWC graph)."},"400":{"description":"Invalid body or no inputs provided."},"503":{"description":"Database unavailable."}},"summary":"Analyze an ISRC catalog (publisher → writer-IPI → ISWC graph)","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"isrcs":{"type":"array","items":{"type":"string"},"description":"Flat ISRC list. Each becomes a 1-row 'song'. Use this for ad-hoc 'give me the publisher graph for these ISRCs' lookups."},"songs":{"type":"array","description":"Pre-grouped Song No. structure. Use this to reproduce a publisher's exact catalog grouping (writers, multiple ISRC variants per song). Matches the shape `python3 scripts/catalog-extract.py` emits in the credits repo.","items":{"type":"object","required":["songNo","rows"],"properties":{"songNo":{"type":"string"},"title":{"type":"string","nullable":true},"writersRaw":{"type":"string","nullable":true},"writers":{"type":"array","items":{"type":"string"}},"rows":{"type":"array","items":{"type":"object","required":["isrc"],"properties":{"isrc":{"type":"string"},"artist":{"type":"string","nullable":true},"recordingTitle":{"type":"string","nullable":true}}}}}}},"publisher_name_re":{"type":"string","description":"Case-insensitive regex matching the target publisher's name in MLC. Optional; when omitted, roster sections return empty arrays."},"enrich":{"type":"boolean","description":"Default true. When true, unmatched ISRCs are queued for crosslink enrichment as a side effect."}}}}}}}},"/v1/catalog/enrich":{"post":{"tags":["Catalog"],"parameters":[],"responses":{"200":{"description":"Enrichment dispatched. Most work is fire-and-forget; response returns within seconds."},"400":{"description":"Invalid body or no ISRCs provided."},"503":{"description":"Database unavailable."}},"summary":"Enrich a catalog of ISRCs (queue crosslink + fetch MLC songwriters)","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["isrcs"],"properties":{"isrcs":{"type":"array","items":{"type":"string"}},"fetch_mlc":{"type":"boolean","description":"Default true. Set false to skip the synchronous Notes Credits fetch — only enqueue crosslink verification."}}}}}}}},"/v1/compute/result":{"post":{"tags":["Contributions"],"parameters":[],"responses":{"200":{"description":"Processed (status: verified|staged|recorded|rejected|invalid)"},"400":{"description":"Invalid body"},"429":{"description":"Rate limit exceeded"},"503":{"description":"Database unavailable"}},"summary":"Submit a Credits Compute work-unit result","description":"Submit the ranked winner and your computed similarity score for a unit from `GET /v1/compute/unit`. No API key. Results are verified server-side before they count; verified contributions are staged additively (never overwriting canonical data) and accrue access-credits toward higher tiers.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["session_id","unit_token","winner_id"],"properties":{"session_id":{"type":"string","format":"uuid"},"unit_token":{"type":"string"},"winner_id":{"type":"string"},"score":{"type":"number","description":"Your locally-computed similarity score. Required for composition_similarity units; optional for identity units and control units (the server recomputes and verifies regardless)."}}}}}}}},"/v1/compute/unit":{"get":{"tags":["Contributions"],"parameters":[{"in":"query","name":"session_id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"task_type","required":false,"schema":{"type":"string","enum":["composition_similarity","identity","crosslink_verify"]}},{"in":"query","name":"anchor","required":false,"schema":{"type":"string"},"description":"crosslink_verify only: target a specific ISWC anchor (the Credits Graph \"verify this connection\" path). Claims its queued unit, seeds one on demand when the anchor has single-source recording links, or falls back to a random unit."}],"responses":{"200":{"description":"A work unit, or { unit: null }"},"400":{"description":"Missing or invalid session_id"},"429":{"description":"Rate limit exceeded"},"503":{"description":"Database unavailable"}},"summary":"Get a Credits Compute work unit","description":"Issue one deterministic, CPU-bound Credits Compute work unit (rank candidate compositions by similarity, submit the best). No API key. Computing the ranking is the cost; submit the result to `POST /v1/compute/result`. Returns `{ unit: null }` when Credits Compute is unconfigured or no unit is available."}},"/v1/contribute":{"options":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}},"post":{"tags":["Contributions"],"parameters":[],"responses":{"201":{"description":"Accepted. Response shape: `{ accepted, verified, no_match, pending, total, results }`. Per-item `status` is one of: `auto_accepted` (applied), `pending` (queued for review), `verified` (positive verification — cross-source agreement, sources merged, confidence bumped), `verified_already_recorded` (replay, no trust bump), `no_match` (negative verification — sources disagree or contributor explicitly asserted non-relationship; entry persisted to `*_index.conflicts[]`), `no_match_already_recorded` (replay), `rejected` (validation failure or non-internal-tier no_match). Sum `accepted + verified` for total positive verifications. New contribution `type: \"no_match\"` lets internal-tier contributors explicitly assert a value or relationship is wrong (works on any contributable field, including arrays). For unknown identifiers, fill_null/append/no_match create a sparse row so the contribution can land; correction on a missing row rejects."},"400":{"description":"Invalid body"},"401":{"description":"API key required or invalid"},"403":{"description":"IP blocked"},"429":{"description":"Rate limit exceeded"},"503":{"description":"Database unavailable"}},"summary":"Submit contributions","description":"Submit data contributions (target, id, field, value, type, citation, notes). Requires **x-api-key** (`cfm_` prefix) that exists in **`api_keys`** on this service’s Supabase project. Max 25 items per request. Responses are snake_case (`target_id`, `status`, etc.). Rate limits apply per key tier; **anonymous** clients cannot contribute (0/min).\n\n**Troubleshooting:** `POST /v1/batch` and other lookups do **not** validate API keys. If batch returns **200** with the same header but contribute returns **401**, the key is missing, not `cfm_`-prefixed, not found, or revoked — not a separate “write database.”","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["contributions"],"properties":{"contributions":{"type":"array","maxItems":25,"items":{"type":"object","properties":{"target":{"type":"string","enum":["isrc","iswc","ipi","isni","upc"]},"id":{"type":"string"},"field":{"type":"string"},"value":{},"type":{"type":"string","enum":["fill_null","append","correction","no_match"]},"citation":{"type":"string"},"notes":{"type":"string"}}}}}}}}}}},"/v1/contributors":{"get":{"tags":["Lookups"],"parameters":[],"responses":{"200":{"description":"Leaderboard data","content":{"application/json":{"schema":{"type":"object","properties":{"total_contributors":{"type":"integer","description":"Unique contributors in last 30 days"},"contributions_30d":{"type":"integer","description":"Accepted contributions in last 30 days"},"top":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Display name (api_keys.contributor_name → name → 'Anonymous')"},"accepted_30d":{"type":"integer"},"top_target":{"type":["string","null"],"description":"Most-contributed-to table (e.g. 'isrc_index')"}}}},"updated_at":{"type":"string","format":"date-time"}}}}}}},"summary":"Public contributors leaderboard","description":"Top 25 contributors by accepted-contribution count in the last 30 days, plus aggregate counts. Powers https://credits.fm/about/contributors and gives agents social proof for the flywheel."}},"/v1/cover-art":{"options":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}},"post":{"tags":["Cover art"],"parameters":[],"responses":{"200":{"description":"coverArt map (ISRC -> URL)"}},"summary":"Cover art by ISRCs","description":"Fetch cover art URLs for up to 10 ISRCs. DB cache first; partial map when some missing.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"isrcs":{"type":"array","items":{"type":"string"},"maxItems":10}}}}}}}},"/v1/coverage":{"get":{"tags":["Health"],"parameters":[],"responses":{"200":{"description":"Coverage totals and stats"},"503":{"description":"Database unavailable"}},"summary":"Index coverage","description":"Returns exact row counts per index (isrc, iswc, ipi, isni, upc) from credits_index_summary, the full 20-cell cross-link grid + growth + day-over-day deltas + ISRC quality metrics + source mix from the latest index_coverage_stats snapshot. Cached 1 hour."}},"/v1/crowd/task":{"get":{"tags":["Contributions"],"parameters":[{"name":"target","in":"query","required":true,"schema":{"type":"string","enum":["isrc","iswc","ipi","isni","upc"]}},{"name":"id","in":"query","required":true,"schema":{"type":"string"}},{"name":"session_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A task, or { task: null }"},"400":{"description":"Invalid target or missing id"},"503":{"description":"Database unavailable"}},"summary":"Get a crowd verification task","description":"Returns one anonymous, one-tap verification microtask for a record: a real source disagreement (`kind: conflict`) or a known-answer control item (`kind: credit`). No API key required. Pass `session_id` (a client-generated UUID held in a first-party cookie) so tasks the session already voted on are not re-served. Returns `{ task: null }` when the record has nothing to verify."}},"/v1/crowd/vote":{"post":{"tags":["Contributions"],"parameters":[],"responses":{"200":{"description":"Recorded (status: recorded|verified|resolved)"},"400":{"description":"Invalid body, unknown task, or choice not offered"},"429":{"description":"Rate limit exceeded"},"503":{"description":"Database unavailable"}},"summary":"Vote on a crowd verification task","description":"Record an anonymous vote on a microtask returned by `GET /v1/crowd/task`. No API key. `value` must be one of the task's offered option values, or `{ \"not_sure\": true }`. Credit tasks score the session's trust weight; conflict tasks evaluate weighted consensus. A field flips to verified only when enough trusted, independent sessions agree on the live value — anonymous votes never overwrite canonical data.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["task_id","session_id","value"],"properties":{"task_id":{"type":"string"},"session_id":{"type":"string","format":"uuid"},"value":{}}}}}}}},"/v1/enrich":{"post":{"tags":["Enrich"],"parameters":[],"responses":{"200":{"description":"iswcTitles and isrcMeta maps"},"500":{"description":"Database not configured"}},"summary":"Batch enrich ISWC/ISRC metadata","description":"Batch-fetch ISWC titles and ISRC metadata (title, artists) for lazy loading. No auth. Capped at 5000 per array.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"iswcs":{"type":"array","items":{"type":"string"}},"isrcs":{"type":"array","items":{"type":"string"}}}}}}}}},"/v1/graph/{type}/{id}":{"get":{"tags":["Music Identity"],"parameters":[{"in":"path","name":"type","required":true,"schema":{"type":"string","enum":["isrc","iswc","ipi","isni","upc","musician"]},"description":"Root type. `musician` roots take a MusicBrainz MBID as the LOOKUP key and are seeded from the artist's recordings + IPIs/ISNIs (effective depth 1 from the seeds). Responses speak Credits IDs: musician node `id` is the deterministic Credits ID, with `mbid` carried as source provenance."},{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"Root identifier code (or MBID for musician)"},{"in":"query","name":"depth","required":false,"schema":{"type":"integer","minimum":1,"maximum":2},"description":"Hops from the root (default 2; fixed for musician roots)"},{"in":"query","name":"people","required":false,"schema":{"type":"string","enum":["0","1"]},"description":"Include musician (people) nodes via the MusicBrainz credit overlay (default 1)"},{"in":"query","name":"per_hop","required":false,"schema":{"type":"integer","minimum":1,"maximum":25},"description":"Logical-edge cap per (node, neighbor-type) (default 12)"},{"in":"query","name":"max_nodes","required":false,"schema":{"type":"integer","minimum":10,"maximum":300},"description":"Total node budget (default 200)"},{"in":"query","name":"nocache","required":false,"schema":{"type":"string"},"description":"Bypass CDN cache (aliases: no_cache, fresh)"}],"responses":{"200":{"description":"Graph neighborhood: nodes[], edges[], truncated, stats"},"400":{"description":"Invalid type or identifier"},"404":{"description":"Unknown identifier (no index row, no edges, no checks)"},"503":{"description":"Database unavailable"}},"summary":"Credits Graph neighborhood","description":"The Credits Graph: identifier nodes (`isrc`, `iswc`, `ipi`, `isni`, `upc`) and crosslink edges around one root code, with per-edge source provenance and verification status (`verified` = ≥2 independent sources, `single` = one). Node `no_match` lists directions checked with no counterpart (verified absence). Typed ISRC↔ISRC relationship edges (cover, sample, remix, …) are overlaid as `kind: relation`. `depth` 1–2 (default 2); explore third-order by re-querying any node at depth 1 and merging. Caps: `per_hop` ≤25 per (node, neighbor-type), `max_nodes` ≤300; `truncated` and per-node `has_more` report capping."}},"/v1/graph/activity":{"get":{"tags":["Music Identity"],"parameters":[{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":150},"description":"Max events (default 60)"},{"in":"query","name":"since","required":false,"schema":{"type":"string","format":"date-time"},"description":"Only events after this ISO timestamp (clamped to now−24h)"},{"in":"query","name":"nocache","required":false,"schema":{"type":"string"},"description":"Bypass CDN cache (aliases: no_cache, fresh)"}],"responses":{"200":{"description":"events[], stats, as_of"},"400":{"description":"Invalid since timestamp"},"503":{"description":"Database unavailable"}},"summary":"Credits Graph activity feed","description":"Status updates across the Credits Graph, newest first: `link_found` (crosslink edge observed), `link_verified` (edge crossed ≥2 independent sources), `relation_found` (typed ISRC↔ISRC relationship: cover, sample, remix, …), `no_match_recorded` (direction checked against every source, no counterpart — verified absence). Poll with `since` set to the previous response's `as_of` for deltas. `since` is clamped to the last 24 hours. Stats include the planner-estimated crosslink total and the latest daily completeness snapshot."}},"/v1/graph/search":{"get":{"tags":["Music Identity"],"parameters":[{"in":"query","name":"q","required":true,"schema":{"type":"string"},"description":"Search query"},{"in":"query","name":"type","required":false,"schema":{"type":"string","enum":["all","artist","song","recording","release","musician","publisher"]},"description":"Entity bucket (default all)"},{"in":"query","name":"limit","required":false,"schema":{"type":"integer"},"description":"Per bucket (default 20, max 500, -1 = cap 500)"},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0},"description":"Skip within bucket"},{"in":"query","name":"exclude_lyrics","required":false,"schema":{"type":"string","enum":["true","false","1","0"]},"description":"When true, ISRC/recording bucket skips lyrics-based index matches (title/song columns only)"},{"in":"query","name":"nocache","required":false,"schema":{"type":"string"},"description":"Bypass CDN cache (aliases: no_cache, fresh)"}],"responses":{"200":{"description":"Bucketed hits (artists, songs, recordings, releases, musicians, publishers)"},"400":{"description":"Invalid query"},"503":{"description":"Database unavailable"}},"summary":"Graph search (bucketed)","description":"Music Identity search: bucketed results by entity type (`all`, `artist`, `song`, `recording`, `release`, `musician`, `publisher`). Default `limit` 20 (max 500; `-1` requests up to 500 per bucket). Use `offset` for paging within each bucket. Optional `exclude_lyrics=true` omits lyrics-indexed ISRC matches (same as GET /v1/search). Optional `nocache` / `no_cache` / `fresh` for uncached reads."}},"/v1/health":{"get":{"tags":["Health"],"parameters":[],"responses":{"200":{"description":"All critical tables above threshold"},"503":{"description":"DB unavailable or one or more critical tables below threshold"}},"summary":"Health check (full)","description":"Returns 200 when DB is connected and critical tables meet thresholds; 503 when DB unavailable or any table below threshold. Response includes status, database, timestamp, and per-table status."}},"/v1/identity":{"get":{"tags":["Music Identity"],"parameters":[],"responses":{"200":{"description":"JSON discovery payload with path templates and links"}},"summary":"Identifier graph discovery","description":"Returns links and descriptions for Music Identity resources under `/v1/` (recordings, songs, releases, artists, musicians, publishers, graph search). See also **GET /v1/graph/search**."}},"/v1":{"get":{"tags":["Music Identity"],"parameters":[],"responses":{"200":{"description":"JSON discovery payload with path templates and links"}},"summary":"Discovery root (legacy)","description":"Same JSON as **GET /v1/identity** — path templates and links for Music Identity under `/v1/`. Prefer `/v1/identity` for new clients."}},"/v1/ipi-group/summary":{"get":{"tags":["Lookups"],"parameters":[{"in":"query","name":"ipis","required":true,"schema":{"type":"string","example":"00177337244,00177337342"}}],"responses":{"200":{"description":"Aggregated summary"},"400":{"description":"Invalid or oversized group"},"404":{"description":"No catalog data found for any IPI in the group"},"503":{"description":"Database unavailable"}},"summary":"Catalog verify summary for a group of IPIs","description":"Aggregated Catalog Verify summary across a group of up to 10 IPIs. Use for publisher entities that hold separate IPIs across PROs (e.g. Sony Music Publishing). Returns the same shape as /v1/ipi/{ipi}/summary with the head listing every IPI in the group."},"post":{"tags":["Lookups"],"parameters":[],"responses":{"200":{"description":"Aggregated summary"},"400":{"description":"Invalid or oversized group"},"404":{"description":"No catalog data found for any IPI in the group"},"503":{"description":"Database unavailable"}},"summary":"Catalog verify summary for an IPI group (body variant)","description":"POST variant of /v1/ipi-group/summary. Use when callers would prefer a body to a query string (internal tools, partner onboarding). Same shape and IPI cap as the GET.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ipis"],"properties":{"ipis":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^\\d{9,11}$"},"example":["00177337244","00177337342"]}}}}}}}},"/v1/ipi/{ipi}":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"ipi","required":true,"schema":{"type":"string","pattern":"^\\d{9,11}$","example":"00508530861"},"description":"9–11 digit IPI (**digits only** in the path; hyphens stripped if present). **`represent`** implies graph mode."}],"responses":{"400":{"description":"Invalid IPI"},"404":{"description":"IPI not found"},"503":{"description":"Database unavailable"}},"summary":"Get IPI","description":"**Canonical IPI URL.** Default: flat **`IpiDetailResponse`**. Graph mode: **`represent=musician`** (default when inferred) or **`represent=publisher`**; legacy **`/v1/musicians/…`** / **`/v1/publishers/…`** **308** here with the appropriate `represent` hint."}},"/v1/ipi/{ipi}/publisher-links":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"ipi","required":true,"schema":{"type":"string","pattern":"^\\d{9,11}$","example":"00508530861"}},{"in":"query","name":"role","required":false,"schema":{"type":"string","enum":["RightsAdministrator","OriginalPublisher","SubPublisher","SubstitutedPublisher"]}},{"in":"query","name":"include","required":false,"schema":{"type":"string","enum":["gaps"]}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Ranked publisher-links list"},"400":{"description":"Invalid parameters"},"404":{"description":"No songwriter credits for this IPI"},"503":{"description":"Database unavailable"}},"summary":"Publisher links for a songwriter IPI","description":"Returns every publisher linked to the IPI across any ISRC, ranked by song count. Optionally filter by role (MLC long-form) and request per-song gap flags (where the top RightsAdministrator is missing). Data source: ipi_isrc_mappings. Distinct from the registration-eligibility endpoint, which gates on the curated verified_publishers table."}},"/v1/ipi/{ipi}/publishers":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"ipi","required":true,"schema":{"type":"string","pattern":"^\\d{9,11}$","example":"00508530861"},"description":"9-11 digit IPI number"},{"in":"query","name":"include","required":false,"schema":{"type":"string","enum":["isrcs"]},"description":"Pass include=isrcs to return the list of shared ISRCs per publisher"}],"responses":{"200":{"description":"Publisher list for songwriter"},"400":{"description":"Invalid or missing IPI"},"404":{"description":"No songwriter credits found for this IPI"},"503":{"description":"Database unavailable"}},"summary":"Get publishers for songwriter IPI","description":"Returns all publishers that co-occur with the given songwriter IPI across any ISRC, from ipi_isrc_mappings. Pass ?include=isrcs to see shared ISRCs per publisher."}},"/v1/ipi/{ipi}/registration-eligibility":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"ipi","required":true,"schema":{"type":"string","pattern":"^\\d{9,11}$","example":"00508530861"}},{"in":"query","name":"only","required":false,"schema":{"type":"string","enum":["eligible","ineligible"]}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Eligibility verdicts per ISRC"},"400":{"description":"Invalid parameters"},"404":{"description":"No credits for this IPI"},"503":{"description":"Database unavailable"}},"summary":"Registration eligibility for a songwriter IPI","description":"Per-song verdict (eligible | ineligible) gated on the verified_publishers table. A song is ineligible when any credited publisher's IPI is in verified_publishers. Blockers list the verified publisher(s) that caused the ineligible verdict."}},"/v1/ipi/{ipi}/summary":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"ipi","required":true,"schema":{"type":"string","pattern":"^\\d{9,11}$","example":"00177337244"}}],"responses":{"200":{"description":"Summary payload"},"400":{"description":"Invalid IPI"},"404":{"description":"No catalog data found for this IPI"},"503":{"description":"Database unavailable"}},"summary":"Catalog verify summary for an IPI","description":"Single-call summary used by the Catalog Verify hero: counts + top-5 samples for writers (publisher mode), publishers (songwriter mode), works (ISWCs), recordings (ISRCs), releases (UPCs), and linked ISNIs. Branches internally on entity_type."}},"/v1/ipi/{ipi}/works":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"ipi","required":true,"schema":{"type":"string","pattern":"^\\d{9,11}$","example":"00177337244"}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Ranked works list"},"400":{"description":"Invalid IPI format"},"404":{"description":"No works found for this IPI"},"503":{"description":"Database unavailable"}},"summary":"Ranked works (ISWCs) for an IPI","description":"Returns the works attributable to this IPI ranked by recording count. For publisher IPIs the works are aggregated from the publisher catalog (ISRCs → ISWCs); for songwriter/artist IPIs they come from ipi_index.iswcs joined to iswc_index. Triggers a fire-and-forget crosslink verification queue entry on each call."}},"/v1/isni/{isni}":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"isni","required":true,"schema":{"type":"string","pattern":"^\\d{15}[\\dX]$","example":"0000000000012571"},"description":"16-character ISNI (spaces/dashes stripped; last digit may be **X**)."}],"responses":{"400":{"description":"Invalid or missing ISNI"},"404":{"description":"ISNI not found"},"503":{"description":"Database unavailable"}},"summary":"Get ISNI","description":"**Canonical ISNI URL.** Default: flat **`IsniDetailResponse`** — name, entity type, roles, linked IPIs, sample works/recordings, and **`recordings_url`** → **`GET /v1/isni/{isni}/recordings`** (full paginated list). Falls back to MusicBrainz when not in index. With **`graph=1`** or any of **`include`**, **`relationships`**, **`relationship`**, **`depth`**: Music Identity graph (legacy **`GET /v1/artists/{isni}`** **308**s here); e.g. **`?include=recordings`** selects only that edge without enabling flat pagination mode. **`limit` / `offset` alone** (no graph keys) paginate flat **`isrcs`** / **`iswcs`** slices only. Optional contribution hints unless **`?contribute=false`**."}},"/v1/isni/{isni}/recordings":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"isni","required":true,"schema":{"type":"string","example":"0000000000012571"},"description":"16-character ISNI (last char may be X)."},{"in":"query","name":"limit","required":false,"schema":{"type":"integer"}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0,"default":0},"description":"Number of recordings to skip (paginate the deduped, sorted list)."}],"responses":{"200":{"description":"Recordings list (may be empty if ISNI has none linked)."},"400":{"description":"Invalid ISNI."},"404":{"description":"ISNI not found."},"503":{"description":"Database unavailable."}},"summary":"Get recordings for an ISNI","description":"Returns the full ISRC list for an ISNI's recordings, paginated and enriched with recording title, artist names, ISWC, and total streams. Walks `mb_artist_isnis → mb_artist_credits → mb_recordings → mb_isrcs`, dedupes by ISRC, and joins `isrc_index` for metadata. Use `?limit=5000` for catalog dumps."}},"/v1/isrc/{isrc}":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"isrc","required":true,"schema":{"type":"string","pattern":"^[A-Z]{2}[A-Z0-9]{3}\\d{7}$","example":"GBAYE0601498"},"description":"12-character ISRC (dashes/spaces stripped before validation). Response fields are **snake_case** (`recording_title`, …)."}],"responses":{"400":{"description":"Invalid or missing ISRC"},"404":{"description":"ISRC not found"},"503":{"description":"Database unavailable"}},"summary":"Get ISRC","description":"**Canonical ISRC URL.** Default: flat **`IsrcDetailResponse`** plus optional contribution hints. With graph query keys (**`graph=1`**, **`include`**, **`limit`**, …): Music Identity graph (legacy **`GET /v1/recordings/{isrc}`** **308**s here) **plus the same flat scalar fields** (songwriters, performers, MLC fields, sources, …) so graph mode is a superset of the flat document."}},"/v1/isrc/{isrc}/mlc-live":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"isrc","required":true,"schema":{"type":"string","pattern":"^[A-Z]{2}[A-Z0-9]{3}\\d{7}$","example":"USRC12003364"},"description":"12-character ISRC"},{"in":"query","name":"refresh","required":false,"schema":{"type":"boolean","default":false},"description":"Bypass the 14-day cache and force a fresh MLC API call"}],"responses":{"200":{"description":"MLC works for this ISRC (source: cache | live | stale | unavailable)"},"400":{"description":"Invalid ISRC"},"503":{"description":"Database unavailable"}},"summary":"Live MLC Public Work Search by ISRC","description":"Authoritative live MLC Public Work Search lookup for this ISRC. Returns work(s) with writer chains, ISWCs, publishers, and alternative titles. Cached 14 days — pass `?refresh=true` to force a fresh MLC fetch. `source` is `cache` | `live` | `stale` | `unavailable`. An empty `works` array means no MLC registration (or MLC is temporarily unavailable — `source: \"unavailable\"`). Always 200 unless the database is unavailable."}},"/v1/isrc/{isrc}/relationships":{"get":{"tags":["Relationships"],"parameters":[{"in":"path","name":"isrc","required":true,"schema":{"type":"string","example":"USUM71710485"},"description":"12-character ISRC"},{"in":"query","name":"type","required":false,"schema":{"type":"string"},"description":"Filter to one relation_type (cover, sample, interpolation, ...). Omit for all."},{"in":"query","name":"direction","required":false,"schema":{"type":"string","enum":["from","to","both"],"default":"both"},"description":"Edge direction relative to the ISRC"},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Aggregated edges with source attribution"},"400":{"description":"Invalid ISRC or query param"},"503":{"description":"Database unavailable"}},"summary":"Get typed relationships for an ISRC","description":"Returns directed typed edges (cover / sample / interpolation / translation / medley / remix / live / etc.) involving this ISRC. Each edge is sourced from one or more upstream catalogs (MusicBrainz, SecondHandSongs, Discogs, etc.) — aggregated rows include the union of sources backing the edge."}},"/v1/iswc/{iswc}":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"iswc","required":true,"schema":{"type":"string","example":"T-010434240-6"},"description":"ISWC in **`T-#########-#`** form or compact **`T##########`** (normalized server-side). Response fields are **snake_case** (`song_title`, `isrc_count`, …)."}],"responses":{"400":{"description":"Invalid or missing ISWC"},"404":{"description":"ISWC not found"},"503":{"description":"Database unavailable"}},"summary":"Get ISWC","description":"**Canonical ISWC URL.** Without graph query keys: flat **`IswcDetailResponse`** (snake_case) plus optional contribution hints. With **`graph=1`** or **`include`** / **`relationships`** / **`limit`** / **`offset`** / **`depth`**: Music Identity **graph** JSON (same behavior as legacy **`GET /v1/songs/{iswc}`**, which **308**s to this path), **including flat work fields** (`songwriters`, `songwriter_count`, `isrc_count`, `updated_at`, …) alongside relationship arrays."}},"/v1/iswc/{iswc}/related-works":{"get":{"tags":["Compositions"],"parameters":[{"in":"path","name":"iswc","required":true,"schema":{"type":"string","example":"T-928-760-686-4"},"description":"ISWC in T-#########-# or compact T########## form. Normalized server-side."}],"responses":{"200":{"description":"Related-works group including this ISWC."},"400":{"description":"Invalid ISWC."},"404":{"description":"ISWC not found in any group."},"503":{"description":"Database unavailable."}},"summary":"Get related-works group for ISWC","description":"Returns the related-works group containing this ISWC. A group groups ISWCs and MLC Song Codes that represent the same musical work. 404 when the ISWC is in our index but not yet grouped (no duplicate registrations found)."}},"/v1/iswc/{iswc}/relationships":{"get":{"tags":["Relationships"],"parameters":[{"in":"path","name":"iswc","required":true,"schema":{"type":"string","example":"T-928-760-686-4"},"description":"ISWC in T-#########-# or T########## form"},{"in":"query","name":"type","required":false,"schema":{"type":"string"}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Aggregated edges with source attribution"},"400":{"description":"Invalid ISWC or query param"},"503":{"description":"Database unavailable"}},"summary":"Get typed relationships for an ISWC","description":"Returns directed typed edges involving any ISRC tied to this ISWC. Edges are sourced from one or more upstream catalogs (MusicBrainz, SecondHandSongs, Discogs)."}},"/v1/keys":{"options":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}},"post":{"tags":["API keys"],"parameters":[],"responses":{"201":{"description":"Key created (key, prefix, name, created_at, limits)"},"400":{"description":"Invalid body"},"409":{"description":"Max keys per email exceeded"},"429":{"description":"Too many key creation requests from this IP"},"503":{"description":"Database unavailable"}},"summary":"Create API key","description":"Create a new API key (name, email required). Returns the raw `cfm_` key once — store it securely. Max 3 active keys per email.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email"],"properties":{"name":{"type":"string","minLength":2,"maxLength":100},"email":{"type":"string","format":"email"},"contributor_name":{"type":"string"},"contributor_url":{"type":"string","format":"uri"}}}}}}}},"/v1/lookup":{"get":{"tags":["Lookup"],"parameters":[{"in":"query","name":"url","required":true,"schema":{"type":"string","format":"uri"},"description":"Spotify or Apple Music URL"}],"responses":{"200":{"description":"Resolved track, album, or artist hint"},"400":{"description":"Missing or unsupported URL"},"404":{"description":"Resource not found on platform"},"500":{"description":"Internal error"},"503":{"description":"Spotify not configured or unavailable"}},"summary":"Resolve URL (Spotify / Apple Music)","description":"Resolve Spotify or Apple Music URLs to track/album metadata and identifiers. Spotify requires client credentials; ISRC backfill uses Supabase when set (`NITRO_PUBLIC_BASE_URL` for credits links)."}},"/v1/mcp":{"delete":{"tags":["MCP"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"MCP session teardown (DELETE)","description":"JSON-RPC session cleanup when supported."},"get":{"tags":["MCP"],"parameters":[{"in":"header","name":"Accept","required":false,"schema":{"type":"string"},"description":"Use `text/html` to redirect to Scalar `/docs`"}],"responses":{"200":{"description":"JSON MCP metadata"},"302":{"description":"Redirect to /docs when Accept is text/html"}},"summary":"MCP discovery (GET)","description":"Without `Accept: text/html`, returns JSON transport metadata (`protocolVersion`, `apiBase: /v1`). With `Accept: text/html`, redirects to **/docs** (Scalar)."},"options":{"tags":["MCP"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"MCP CORS preflight (OPTIONS)"},"post":{"tags":["MCP"],"parameters":[],"responses":{"200":{"description":"JSON-RPC result object, array of results, or JSON-RPC error"},"204":{"description":"No content (e.g. notification with no reply)"}},"summary":"MCP JSON-RPC over HTTP","description":"Model Context Protocol (stateless). POST JSON-RPC 2.0: `initialize`, `tools/list`, `tools/call`. Tools call this host's REST API under **/v1/**. Optional **x-api-key** header for higher rate limits. Lookup tools include contribution nudges by default; pass **`contribute: false`** in tool arguments to opt out.","requestBody":{"required":true,"content":{"application/json":{"schema":{"description":"Single JSON-RPC object or an array of requests (batch)","oneOf":[{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{},"method":{"type":"string"},"params":{"type":"object","additionalProperties":true}}},{"type":"array","items":{"type":"object","additionalProperties":true}}]}}}}}},"/v1/me":{"get":{"tags":["Contributions"],"parameters":[{"in":"query","name":"session_id","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Your earned tier and contribution totals"},"503":{"description":"Database unavailable"}},"summary":"Your Credits Compute status","description":"Returns your earned Credits Compute tier and contribution totals. Identify with an API key (`x-api-key`) and/or an anonymous compute session (`?session_id=`). Contributing verified work raises your `tier`."}},"/v1/mlc/{code}/related-works":{"get":{"tags":["Compositions"],"parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string","example":"NVCBR5"},"description":"MLC Song Code, e.g. NVCBR5 (case-insensitive)."}],"responses":{"200":{"description":"Related-works group including this MLC Song Code."},"400":{"description":"Invalid MLC Song Code."},"404":{"description":"MLC Song Code not found in any group."},"503":{"description":"Database unavailable."}},"summary":"Get related-works group for MLC Song Code","description":"Returns the related-works group containing this MLC Song Code. A group groups MLC Song Codes and ISWCs that represent the same musical work. 404 when the code is not part of any group (i.e. no detected duplicate registrations)."}},"/v1/musician/{mbid}":{"get":{"tags":["Music Identity"],"parameters":[{"in":"path","name":"mbid","required":true,"schema":{"type":"string","format":"uuid","example":"000002a0-8f8a-4320-ac61-7f60e8b44f32"},"description":"MusicBrainz artist ID"}],"responses":{"200":{"description":"Musician profile"},"400":{"description":"Invalid MBID format"},"404":{"description":"Musician not found"},"503":{"description":"Service unavailable"}},"summary":"Get musician by MBID","description":"Full musician profile by MBID (credits.fm parity): nested artist, unified credits, releases, Spotify supplement when configured."}},"/v1/musician/{mbid}/publisher-links":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"mbid","required":true,"schema":{"type":"string","format":"uuid","example":"000002a0-8f8a-4320-ac61-7f60e8b44f32"}},{"in":"query","name":"role","required":false,"schema":{"type":"string","enum":["RightsAdministrator","OriginalPublisher","SubPublisher","SubstitutedPublisher"]}},{"in":"query","name":"include","required":false,"schema":{"type":"string","enum":["gaps"]}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Ranked publisher-links list"},"400":{"description":"Invalid parameters"},"404":{"description":"Musician not found"},"503":{"description":"Database unavailable"}},"summary":"Publisher links for a musician (MBID)","description":"Aggregates publisher-links data across every IPI linked to the musician via mb_artist_ipis. Returns a unioned, ranked publisher list plus per-contributing-IPI breakdown. Optional gap flags identify ISRCs where the top RightsAdministrator is missing across the musician's full catalog."}},"/v1/musician/{mbid}/registration-eligibility":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"mbid","required":true,"schema":{"type":"string","format":"uuid","example":"000002a0-8f8a-4320-ac61-7f60e8b44f32"}},{"in":"query","name":"only","required":false,"schema":{"type":"string","enum":["eligible","ineligible"]}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Eligibility verdicts per ISRC"},"400":{"description":"Invalid parameters"},"404":{"description":"Musician not found"},"503":{"description":"Database unavailable"}},"summary":"Registration eligibility for a musician (MBID)","description":"Per-song verdict (eligible | ineligible) aggregated across every IPI linked to the MBID via mb_artist_ipis. Gated on the verified_publishers table."}},"/v1/ownership/analyze-data":{"post":{"tags":["Ownership"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Songwriter analysis on caller-supplied matched_songs (no fetch)"}},"/v1/ownership/analyze":{"post":{"tags":["Ownership"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Songwriter analysis across ISRCs"}},"/v1/ownership/ip-chains-data":{"post":{"tags":["Ownership"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Broken IP-chain detection on caller-supplied matched_songs (no fetch)"}},"/v1/ownership/ip-chains":{"post":{"tags":["Ownership"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Broken IP-chain detection for a writer across ISRCs"}},"/v1/ownership/representation-data":{"post":{"tags":["Ownership"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Representation verdict on caller-supplied matched_songs (no fetch)"}},"/v1/ownership/representation":{"post":{"tags":["Ownership"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Songwriter representation check across ISRCs"}},"/v1/ownership/search-data":{"post":{"tags":["Ownership"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Songwriter name search on caller-supplied matched_songs (no fetch)"}},"/v1/ownership/search":{"post":{"tags":["Ownership"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Songwriter name search with representation analysis"}},"/v1/publishers/{ipi}/catalog":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"ipi","required":true,"schema":{"type":"string","pattern":"^\\d{9,11}$","example":"01138884620"}},{"in":"query","name":"role","required":false,"schema":{"type":"string","enum":["RightsAdministrator","OriginalPublisher","SubPublisher","SubstitutedPublisher"]}},{"in":"query","name":"include","required":false,"schema":{"type":"string","enum":["issues"]}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Publisher catalog"},"400":{"description":"Invalid parameters"},"404":{"description":"No catalog found for this IPI / role"},"503":{"description":"Database unavailable"}},"summary":"Get publisher catalog with optional audit flags","description":"Returns every ISRC where the given publisher IPI is credited (optionally filtered by role). Pass include=issues to get per-song data-quality flags (missing_iswc, missing_mlc_code, missing_sw_ipi) and summary counts."}},"/v1/publishers/{ipi}/musicians":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"ipi","required":true,"schema":{"type":"string","pattern":"^\\d{9,11}$","example":"01138884620"}},{"in":"query","name":"role","required":false,"schema":{"type":"string","enum":["RightsAdministrator","OriginalPublisher","SubPublisher","SubstitutedPublisher","all"]}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":1000}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Musicians for the publisher"},"400":{"description":"Invalid parameters"},"404":{"description":"Publisher has no matching songs"},"503":{"description":"Database unavailable"}},"summary":"Musicians administered by a publisher","description":"Reverse lookup: given a publisher IPI, returns the ranked list of songwriters/musicians they work with, with per-musician song counts. Default role filter is RightsAdministrator (the most BI-relevant slice). Use role=all for no filter. MBIDs resolved via mb_artist_ipis when available."}},"/v1/publishers/search":{"get":{"tags":["Lookups"],"parameters":[{"in":"query","name":"q","required":true,"schema":{"type":"string","minLength":2}},{"in":"query","name":"role","required":false,"schema":{"type":"string","enum":["RightsAdministrator","OriginalPublisher","SubPublisher","SubstitutedPublisher"]}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Publisher search results"},"400":{"description":"Invalid query parameters"},"503":{"description":"Database unavailable"}},"summary":"Search publishers by name","description":"Returns publisher entities matching a name fragment, aggregated from ipi_isrc_mappings. Each item carries role counts (RightsAdministrator, OriginalPublisher, SubPublisher, SubstitutedPublisher) and distinct song count across any role. Optionally filter by a single publisher role."}},"/v1/related-works/{id}":{"get":{"tags":["Compositions"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","example":"ISWC:T9287606864"},"description":"Deterministic group ID. Either `ISWC:<min iswc>` or `MLC:<min mlc_song_code>`."}],"responses":{"200":{"description":"Related-works group with full member list."},"400":{"description":"Invalid group ID format."},"404":{"description":"Related-works group not found."},"503":{"description":"Database unavailable."}},"summary":"Get related-works group by ID","description":"Returns the full related-works group — head + every member ISWC and MLC Song Code. Groups group identifiers that represent the same underlying musical work. Built nightly from MLC Song Code collisions, CISAC redirects, MusicBrainz works, and title/writer similarity."}},"/v1/resolve/batch":{"post":{"tags":["Resolve"],"parameters":[],"responses":{"200":{"description":"Batch results"},"400":{"description":"Invalid body"},"503":{"description":"Database unavailable"}},"summary":"Batch resolve tracks to ISRCs","description":"Up to 50 { name, artist } pairs. Returns total, resolved count, and per-row credits_url when ISRC found.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tracks":{"type":"array","maxItems":50,"items":{"type":"object","properties":{"name":{"type":"string"},"artist":{"type":"string"}}}}}}}}}}},"/v1/resolve/songwriter":{"post":{"tags":["Resolve"],"parameters":[],"responses":{"200":{"description":"Resolved songwriter (single match with publishers, or disambiguation list)"},"400":{"description":"Invalid body"},"503":{"description":"Database unavailable"}},"summary":"Resolve songwriter name to IPI","description":"Given a songwriter name and ISRCs/ISWCs for context, searches isrc_index.songwriters to find matching IPIs. If exactly one IPI matches, returns publisher data inline. If multiple match, returns a disambiguation list.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Songwriter name (fuzzy matched)"},"isrcs":{"type":"array","items":{"type":"string"},"description":"ISRCs of songs they're credited on (max 500)"},"iswcs":{"type":"array","items":{"type":"string"},"description":"ISWCs of works they're credited on (max 100)"}},"required":["name"]}}}}}},"/v1/resolve/track":{"post":{"tags":["Resolve"],"parameters":[],"responses":{"400":{"description":"Missing required body fields"},"500":{"description":"Unexpected server error"},"503":{"description":"Database unavailable"},"504":{"description":"Resolve exceeded time budget (try again)"}},"summary":"Resolve single track to ISRC and credits","description":"Resolve track by name+artist or spotify_url or apple_music_url. Returns ISRC, songwriters, performers, credits_url when found; otherwise 200 with `isrc: null` and `search_url`. On success, optional `missing_fields` / `contribute_url` unless `contribute: false` in the body.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"artist":{"type":"string"},"spotify_url":{"type":"string","format":"uri"},"apple_music_url":{"type":"string","format":"uri"},"contribute":{"type":"boolean","description":"Include missing_fields + contribute_url when gaps exist (default true). Set false to opt out."}}}}}}}},"/v1/samples":{"get":{"tags":["Discovery"],"parameters":[],"responses":{"200":{"description":"Arrays of sample identifiers per type"},"503":{"description":"Database unavailable"}},"summary":"Sample identifiers","description":"Returns up to ten IDs per type: curated working examples first, then richer index rows (isrcs, iswcs, ipis, isnis, upcs, mbids) for building detail URLs."}},"/v1/search":{"get":{"tags":["Search"],"parameters":[{"in":"query","name":"q","required":true,"schema":{"type":"string","minLength":1},"description":"Search text or identifier (canonicalized server-side)"},{"in":"query","name":"type","required":false,"schema":{"type":"string","enum":["auto","isrc","iswc","ipi","isni","upc","musician","all"],"default":"auto"},"description":"Which buckets to search (default auto)"},{"in":"query","name":"match","required":false,"schema":{"type":"string","enum":["default","recording_title"],"default":"default"},"description":"`recording_title`: title/song columns only for ISRC bucket (no artist-name catalog path); with type auto/all, other buckets are skipped unless the query looks like a specific code."},{"in":"query","name":"exclude_lyrics","required":false,"schema":{"type":"string","enum":["true","false","1","0"]},"description":"When true, ISRC hits skip lyrics-indexed / broad text paths (title-focused ranking)."},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":0}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0,"default":0}},{"in":"query","name":"engine","required":false,"schema":{"type":"string","enum":["pg","typesense"]},"description":"Search engine for fuzzy text queries. Default follows `NITRO_USE_TYPESENSE`. `typesense` forces Typesense for this request when server permits override; `pg` forces PostgreSQL. Engine applies to every text-search `type` (`auto`, `all`, `isrc`, `iswc`, `ipi`, `isni`, `upc`); the `musician` bucket and exact identifier (ISRC/ISWC/IPI/ISNI/UPC) lookups always use PG. When Typesense answers a bucket, the per-bucket PG branch is skipped to avoid the 12s tsquery ceiling."},{"in":"query","name":"entity_type","required":false,"schema":{"type":"string"},"description":"Comma-separated list. Filters IPI / ISNI buckets by `entity_type` (e.g. `musician,publisher`). Typesense path only."},{"in":"query","name":"roles","required":false,"schema":{"type":"string"},"description":"Comma-separated list. Filters IPI / ISNI buckets by `roles` (any-of). Typesense path only."},{"in":"query","name":"release_year_min","required":false,"schema":{"type":"integer","minimum":0},"description":"Lower bound on `release_year` for ISRC + UPC buckets. Typesense path only."},{"in":"query","name":"release_year_max","required":false,"schema":{"type":"integer","minimum":0},"description":"Upper bound on `release_year` for ISRC + UPC buckets. Typesense path only."},{"in":"query","name":"min_isrc_count","required":false,"schema":{"type":"integer","minimum":0},"description":"Minimum `isrc_count` for ISWC / IPI / ISNI / UPC buckets. Typesense path only."},{"in":"query","name":"min_total_streams","required":false,"schema":{"type":"integer","minimum":0},"description":"Minimum cached `total_streams` for ISRC bucket. Typesense path only."},{"in":"query","name":"source","required":false,"schema":{"type":"string"},"description":"Comma-separated list. Filters ISRC bucket by primary `source` (e.g. `mlc,musicbrainz`). Typesense path only."},{"in":"query","name":"type_label","required":false,"schema":{"type":"string"},"description":"Comma-separated list. Filters musicians bucket by `type_label` (`group` | `person`). Title-cased server-side to match stored values; Typesense path only."},{"in":"query","name":"sort_by","required":false,"schema":{"type":"string","enum":["relevance","streams","isrc_count","recent","title"],"default":"relevance"},"description":"Per-bucket sort applied on the Typesense path. `streams` only meaningful for ISRC; `recent` only for ISRC/UPC. Other buckets fall back to relevance."},{"in":"query","name":"facets","required":false,"schema":{"type":"string","enum":["true","false","1","0"]},"description":"When truthy on the Typesense path, response carries a `meta` block with per-bucket facet counts + `search_time_ms` + `found` totals."},{"in":"query","name":"nocache","required":false,"schema":{"type":"string"},"description":"Skip public CDN cache (aliases: no_cache, fresh)"}],"responses":{"200":{"description":"Bucketed hits; optional linkResult, warnings"},"400":{"description":"Invalid query (bad type, limit, offset window, …)"},"503":{"description":"Database unavailable"}},"summary":"Unified identifier search"}},"/v1/suggest":{"get":{"tags":["Search"],"parameters":[{"in":"query","name":"q","required":false,"schema":{"type":"string","minLength":2},"description":"Search string (min 2 chars)"},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":0,"maximum":25},"description":"Max suggestions (default 10); 0 = bounded window fill"},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0},"description":"Skip N suggestions; offset + limit ≤ 100 (default 0)"},{"in":"query","name":"nocache","required":false,"schema":{"type":"string"},"description":"Skip CDN cache (1/true/yes); aliases no_cache, fresh"}],"responses":{"200":{"description":"Suggestions array (empty when q too short or DB unavailable)"}},"summary":"Autocomplete suggestions","description":"Fast autocomplete across ISRC, ISWC, IPI, ISNI, UPC and musicians. Min 2 characters. limit default 10; limit 0 fills bounded window."}},"/v1/typeahead":{"get":{"tags":["Search"],"parameters":[{"in":"query","name":"q","required":false,"schema":{"type":"string","minLength":2},"description":"Search string (min 2 chars after trim)"},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":25,"default":10},"description":"Max suggestions returned after interleaving (default 10)"},{"in":"query","name":"nocache","required":false,"schema":{"type":"string"},"description":"Skip CDN cache (1/true/yes); aliases no_cache, fresh"}],"responses":{"200":{"description":"Suggestions array (empty when q too short or DB unavailable)"}},"summary":"Cross-collection typeahead (Typesense-backed)","description":"Prefix + typo-tolerant autocomplete across recordings, songs, releases, publishers/writers, and artists. Response shape matches /v1/suggest so the frontend dropdown can route both endpoints through the same parser. Falls back to PG `/v1/suggest` when Typesense isn't configured."}},"/v1/upc/{upc}":{"get":{"tags":["Lookups"],"parameters":[{"in":"path","name":"upc","required":true,"schema":{"type":"string","pattern":"^\\d{12,14}$","example":"602445790098"},"description":"12–14 digit UPC / EAN (**digits only** in the path). No contribution hints on flat release rows."}],"responses":{"200":{"description":"Flat **`upc_index`** row (snake_case) or Music Identity release graph when graph query keys are used.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Invalid or missing UPC"},"404":{"description":"UPC not found"},"503":{"description":"Database unavailable"}},"summary":"Get UPC","description":"**Canonical UPC URL.** Default: release summary from `upc_index`. With **`graph=1`** or graph query keys: Music Identity graph (same as legacy **`GET /v1/releases/{upc}`**)."}},"/v1/validate/batch":{"options":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}},"post":{"tags":["Validate"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Batch ISRC validation","description":"Validate ISRCs against index + Notes Credits API. Optional include_dsp_check cross-references Spotify and Apple Music when credentials are configured. ?format=csv returns CSV."}},"/v1/verify/{type}/{code}":{"get":{"tags":["Verify"],"parameters":[{"in":"path","name":"type","required":true,"schema":{"type":"string","enum":["isrc","iswc","ipi","isni","upc"],"example":"isrc"}},{"in":"path","name":"code","required":true,"schema":{"type":"string","example":"GBAYE0601498"}},{"in":"query","name":"raw","required":false,"schema":{"type":"string","enum":["1"]},"description":"Include raw per-adapter results"}],"responses":{"200":{"description":"Verify result"},"400":{"description":"Invalid type or code format"}},"summary":"Multi-source identifier verification","description":"Fans out to every source that supports the given type (ISRC → Notes Credits + MusicBrainz + Spotify + Apple Music; ISWC → CISAC + MusicBrainz; IPI → MusicBrainz; ISNI → ISNI.org + MusicBrainz; UPC → MusicBrainz + Spotify + Apple Music). Returns per-field `{value, verified, agreeing_sources, disagreeing_sources}` verdicts, an `overall_level` (unverified / single_source / verified / conflicted), and `linked_identifiers` with per-edge source attribution."}},"/v1/verify/batch":{"post":{"tags":["Verify"],"parameters":[],"responses":{"200":{"description":"OK"}},"summary":"Batch multi-source verification","description":"Verify up to 100 `{ type, code }` items in one request. Invalid entries are returned inline as `{ error, input }` preserving order."}},"/docs":{"get":{"tags":["App Routes"],"parameters":[],"responses":{"200":{"description":"OK"}}}}},"tags":[{"name":"Discovery","description":"Service JSON (`GET /`), samples, MCP doc redirect."},{"name":"Health","description":"Liveness (`/healthz`, `/v1/health`) and public coverage metrics."},{"name":"Search","description":"Unified search and autocomplete (`/v1/search`, `/v1/suggest`)."},{"name":"Lookup","description":"Platform URL resolution (`/v1/lookup`)."},{"name":"Lookups","description":"Identifier detail routes (ISRC, ISWC, IPI, ISNI, UPC) and related sub-resources."},{"name":"Music Identity","description":"Graph discovery (`GET /v1/identity`) and bucketed search (`GET /v1/graph/search`)."},{"name":"Batch","description":"Batch identifier fetch (`POST /v1/batch`)."},{"name":"Resolve","description":"Track/songwriter resolution helpers."},{"name":"Validate","description":"ISRC validation against registry data."},{"name":"Verify","description":"Multi-source identifier verification."},{"name":"Audit","description":"Share and unmatched audits (JSON/CSV)."},{"name":"Contributions","description":"Authenticated data contributions (`POST /v1/contribute`)."},{"name":"API keys","description":"Issue API keys (`POST /v1/keys`)."},{"name":"MCP","description":"Model Context Protocol over Streamable HTTP (`/v1/mcp` — unversioned `/mcp` aliases here)."},{"name":"Cover art","description":"Cover art by ISRC list."},{"name":"Enrich","description":"Metadata enrichment (operator-style)."},{"name":"App Routes","description":"Nitro default bucket for untagged routes — should be empty; report stray operations if you see this group."}],"x-tagGroups":[{"name":"Canonical API (v1)","tags":["Discovery","Health","Search","Lookup","Lookups","Music Identity","Batch","Resolve","Validate","Verify","Audit","Contributions","API keys","MCP","Cover art","Enrich"]},{"name":"Infrastructure","tags":["App Routes"]}]}