Track-title search — who has a track called X
const url = 'https://crate.hosaka.fm/api/v2/tracks?title=example&fuzzy=true&source=mb';const options = {method: 'GET', headers: {'X-API-Key': '<X-API-Key>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url 'https://crate.hosaka.fm/api/v2/tracks?title=example&fuzzy=true&source=mb' \ --header 'X-API-Key: <X-API-Key>'Title-first track discovery: a track title → the artist clusters that have a recording of it, ranked by how many releases carry that artist’s rendition (a ubiquity proxy), each with onward /artist links. The title-first complement to GET /api/v2/resolve?artist=&track= (which needs the artist and returns one identity). CORPUS (?source=): default mb = MusicBrainz-linked Discogs tracklists (catalogued/physical-release music); bandcamp = the Bandcamp track corpus (reaches the underground/self-released tail — a Bandcamp-only cut returns 0 under mb); all = both merged per cluster (each result carries a sources array). MATCH MODES: default = EXACT (case/punctuation-insensitive whole title); ?fuzzy=true = case-insensitive SUBSTRING (≥3 chars, so “angel ech” finds “Angel Echoes”). The response echoes which ran in mode. HONEST LIMITS: (1) fuzzy is substring, not typo/phonetic; (2) common titles fan out (e.g. “Intro”) — the ranking is then from a bounded candidate set with truncated:true, so narrow via ?artist=&track= on /resolve (fuzzy fans out sooner); (3) mb coverage is MB-linked catalogue (uneven — thinner for electronic/underground; use bandcamp/all there). Keyed (X-API-Key); no match → 200 with results:[] (honest gap), never 404.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Query Parameters
Section titled “Query Parameters ”The track title (required). Default (exact) match is case/punctuation-insensitive whole-title equality. With ?fuzzy=true it is a case-insensitive SUBSTRING match.
Fuzzy=true → case-insensitive SUBSTRING match on the raw title (e.g. title=angel echoes&fuzzy=true matches “Angel Echoes (Jon Hopkins Remix)”), gin_trgm-indexed; requires ≥3 characters (shorter is refused as an honest empty). Omitted/false = exact whole-title match. Fuzzy fans out more, so truncated fires sooner — narrow via /resolve?artist=&track=.
Which track corpus to search. mb (default) = MusicBrainz-linked Discogs tracklists (catalogued/physical-release music). bandcamp = the Bandcamp track corpus (mirror.bandcamp_release_track_v1) — reaches underground/self-released titles that are NOT on a Discogs tracklist (e.g. a Bandcamp-only cut returns 0 under mb). all = search both and merge per artist cluster (each result carries a sources array; releases/recordings are summed across corpora). For all, state:degraded means at least one corpus read failed → results may be partial.
Max artist-cluster rows (1–50, default 20).
Responses
Section titled “ Responses ”Ranked artist clusters with a track of that title (or an empty honest-gap list)
object
‘ok’ = the read ran (an empty results is a genuine no-match); ‘degraded’ = a substrate read failed (timeout/transient) so results is empty-by-failure, NOT a no-match — retry. For source=all, ‘degraded’ means ≥1 corpus read failed so results may be partial.
Which corpus was searched (echo of ?source=; default mb). all = both corpora merged per cluster.
Which match ran — ‘exact’ (default) or ‘fuzzy’ (?fuzzy=true substring).
The title as received (verbatim echo).
Exact: the alphanumeric-normalized form matched; fuzzy: the lowercased substring matched.
True when the exact-title match exceeded the candidate cap — the ranking is from a bounded sample; narrow via /resolve?artist=&track=.
Number of result rows (artist clusters).
object
Representative as-credited artist string — descriptive, never a join key.
The performing artist’s 64-hex cluster_id — the canonical onward key.
Distinct releases carrying this artist’s track of that title (the ranking signal). For source=all, the sum across corpora (Discogs + Bandcamp).
Distinct recordings of that title by this artist (original + remixes/live collapse here).
Which corpus/corpora this cluster’s hit came from — the provenance for source=all (e.g. [“mb”], [“bandcamp”], or both).
Onward v2 addresses.
object
Example
{ "state": "ok", "source": "mb", "mode": "exact", "results": [ { "sources": [ "mb" ] } ]}Headers
Section titled “Headers ”Requests allowed in the current window.
Requests remaining in the current window.
Unix epoch (seconds) when the current window resets.
Validation failure (invalid query, malformed body, bad facet name)
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".
| code | HTTP | when thrown | fix |
|---|---|---|---|
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/v2/resolve?q=<name>, then call /artist/{cluster_id} |
use_resolve_for_locator | 400 | /artist/{key} given a discogs:/mbid: locator (not a canonical address) | GET /api/v2/resolve?discogs=<id> (or ?mbid=), then use the returned cluster_id (the response next field is the ready-to-call URL) |
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/v2/label/warp) |
invalid_fields | 400 | /artist/{key}?fields= lists a facet that is not a valid top-level dossier field | use only the fields in the response valid set; omit ?fields= entirely for the full dossier (see the example field) |
missing_locator | 400 | /resolve called with none of q/cluster/discogs/mbid | pass exactly one locator |
invalid_locator | 400 | a /resolve locator is malformed for its type | fix the format, or fall back to ?q=<name> |
invalid_query | 400 | /search ?q= missing/empty, or any Zod validation failure (details[] attached) | pass ?q=<text>; fix each details entry |
invalid_facet | 400 | an unknown facet filter name was supplied | GET /api/v2/facets for valid names + values |
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 (or until X-RateLimit-Reset), then retry |
object
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.
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).
Actionable remediation in human terms — what to DO next, often naming the exact endpoint (a template with next.
Deep link to this code’s docs: https://crate.hosaka.fm/docs/api#error-. Auto-populated for catalogued codes.
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.
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.
Structured validation breakdown — on Zod 400s (invalid_query), an array of { path, message }, one per failed field. Present only when validation specifics are attached.
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.
Echoed on master_not_found (404) — the master id that did not resolve.
Example
{ "error": "invalid_query"}Rate limit exceeded — see Retry-After + X-RateLimit-* headers
object
Example
{ "error": "rate_limited"}Internal server error
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".
| code | HTTP | when thrown | fix |
|---|---|---|---|
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/v2/resolve?q=<name>, then call /artist/{cluster_id} |
use_resolve_for_locator | 400 | /artist/{key} given a discogs:/mbid: locator (not a canonical address) | GET /api/v2/resolve?discogs=<id> (or ?mbid=), then use the returned cluster_id (the response next field is the ready-to-call URL) |
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/v2/label/warp) |
invalid_fields | 400 | /artist/{key}?fields= lists a facet that is not a valid top-level dossier field | use only the fields in the response valid set; omit ?fields= entirely for the full dossier (see the example field) |
missing_locator | 400 | /resolve called with none of q/cluster/discogs/mbid | pass exactly one locator |
invalid_locator | 400 | a /resolve locator is malformed for its type | fix the format, or fall back to ?q=<name> |
invalid_query | 400 | /search ?q= missing/empty, or any Zod validation failure (details[] attached) | pass ?q=<text>; fix each details entry |
invalid_facet | 400 | an unknown facet filter name was supplied | GET /api/v2/facets for valid names + values |
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 (or until X-RateLimit-Reset), then retry |
object
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.
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).
Actionable remediation in human terms — what to DO next, often naming the exact endpoint (a template with next.
Deep link to this code’s docs: https://crate.hosaka.fm/docs/api#error-. Auto-populated for catalogued codes.
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.
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.
Structured validation breakdown — on Zod 400s (invalid_query), an array of { path, message }, one per failed field. Present only when validation specifics are attached.
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.
Echoed on master_not_found (404) — the master id that did not resolve.
Example
{ "error": "invalid_query"}Database pool exhausted — retry after 5s
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".
| code | HTTP | when thrown | fix |
|---|---|---|---|
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/v2/resolve?q=<name>, then call /artist/{cluster_id} |
use_resolve_for_locator | 400 | /artist/{key} given a discogs:/mbid: locator (not a canonical address) | GET /api/v2/resolve?discogs=<id> (or ?mbid=), then use the returned cluster_id (the response next field is the ready-to-call URL) |
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/v2/label/warp) |
invalid_fields | 400 | /artist/{key}?fields= lists a facet that is not a valid top-level dossier field | use only the fields in the response valid set; omit ?fields= entirely for the full dossier (see the example field) |
missing_locator | 400 | /resolve called with none of q/cluster/discogs/mbid | pass exactly one locator |
invalid_locator | 400 | a /resolve locator is malformed for its type | fix the format, or fall back to ?q=<name> |
invalid_query | 400 | /search ?q= missing/empty, or any Zod validation failure (details[] attached) | pass ?q=<text>; fix each details entry |
invalid_facet | 400 | an unknown facet filter name was supplied | GET /api/v2/facets for valid names + values |
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 (or until X-RateLimit-Reset), then retry |
object
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.
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).
Actionable remediation in human terms — what to DO next, often naming the exact endpoint (a template with next.
Deep link to this code’s docs: https://crate.hosaka.fm/docs/api#error-. Auto-populated for catalogued codes.
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.
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.
Structured validation breakdown — on Zod 400s (invalid_query), an array of { path, message }, one per failed field. Present only when validation specifics are attached.
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.
Echoed on master_not_found (404) — the master id that did not resolve.
Example
{ "error": "invalid_query"}Request deadline (15s) or query timeout exceeded
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".
| code | HTTP | when thrown | fix |
|---|---|---|---|
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/v2/resolve?q=<name>, then call /artist/{cluster_id} |
use_resolve_for_locator | 400 | /artist/{key} given a discogs:/mbid: locator (not a canonical address) | GET /api/v2/resolve?discogs=<id> (or ?mbid=), then use the returned cluster_id (the response next field is the ready-to-call URL) |
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/v2/label/warp) |
invalid_fields | 400 | /artist/{key}?fields= lists a facet that is not a valid top-level dossier field | use only the fields in the response valid set; omit ?fields= entirely for the full dossier (see the example field) |
missing_locator | 400 | /resolve called with none of q/cluster/discogs/mbid | pass exactly one locator |
invalid_locator | 400 | a /resolve locator is malformed for its type | fix the format, or fall back to ?q=<name> |
invalid_query | 400 | /search ?q= missing/empty, or any Zod validation failure (details[] attached) | pass ?q=<text>; fix each details entry |
invalid_facet | 400 | an unknown facet filter name was supplied | GET /api/v2/facets for valid names + values |
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 (or until X-RateLimit-Reset), then retry |
object
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.
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).
Actionable remediation in human terms — what to DO next, often naming the exact endpoint (a template with next.
Deep link to this code’s docs: https://crate.hosaka.fm/docs/api#error-. Auto-populated for catalogued codes.
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.
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.
Structured validation breakdown — on Zod 400s (invalid_query), an array of { path, message }, one per failed field. Present only when validation specifics are attached.
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.
Echoed on master_not_found (404) — the master id that did not resolve.
Example
{ "error": "invalid_query"}