Discovery
Natural-language Discovery (MusiSearch) with POST /v1/discovery.
Discovery is MusiMap's public API capability for MusiSearch:
natural-language music discovery over accessible Workspace catalogues and, when entitled,
MusiMap Atlas. Describe moods, styles, context, or intent in prose. The primary endpoint is
POST /v1/discovery.
Use Discovery (POST /v1/discovery) when the caller
describes music in natural language: moods, styles, context, intent, or prose.
Use Catalogue Search
(POST /v1/search/tracks)
when criteria are explicit and structured: lexical title and artist terms, taxonomy
tag IDs, identifiers, or audio-feature ranges. Catalogue Search does not interpret
natural language. Discovery does not replace structured filters.
Overview
Discovery turns a plain-language description into ranked tracks. Each call is one turn inside a session. Read the session mental model before you call the session endpoints.
- Discovery: “sad music with a little bit of rock and female vocals”, “the happiest music for a night in a bar”, multi-turn refinement.
- Catalogue Search: exact title/artist tokens, known ISRCs, required/optional/excluded tag IDs, BPM ranges, inventory follow-up by ID.
For the product behind this API, see the MusiSearch product page.
Endpoints
POST /v1/discovery: run a Discovery turn (creates a session on first call, or continues an existing session). Returns a bounded, non-paginated result set for that turn.GET /v1/discovery/sessions: list organisation Discovery sessions (paginated).GET /v1/discovery/sessions/{session_id}: session metadata (no embedded turn history).GET /v1/discovery/sessions/{session_id}/turns: paginated turn history with results.
After the product introduction above, prefer the capability name Discovery in API documentation. Use the four routes listed above; do not invent alternate product path names.
Session mental model
Discovery is conversational. Keep these five rules in mind before you use any session endpoint.
- The first call creates the session. Send
POST /v1/discoverywith aqueryand nosession_id. There is no separate session-create endpoint. - Your client remembers
session_id. The response returns it once. Store it next to your own conversation or screen state, and send it on every follow-up call. - MusiSearch remembers the current intent. A follow-up with the same
session_idis read as a refinement of what the session is already searching for, so you do not need to restate the whole request. - The catalogue target is immutable. The catalogue selection from the first
call is fixed for the life of the session. A conflicting target on a later call returns
catalogue_target_conflict. - A new search is a new session. To start unrelated work, omit
session_idand callPOST /v1/discoveryagain. To change the catalogue, start a new session as well.
The session endpoints below (GET /v1/discovery/sessions, session detail, and
turns) are read-only views of what you already created with
POST /v1/discovery.
Authentication
Discovery uses OAuth client credentials. Request the scopes below when you exchange credentials. See Authentication for the token flow.
- discovery.write: create or continue a Discovery turn
(
POST /v1/discovery). Consumes Discovery credits when the turn is billable. - discovery.read: list sessions, read session detail, and list turns.
- atlas.read: required when
include_atlasis true ortargetsincludesatlas(same Atlas gate as Catalogue Search). - catalog.read: required when Workspace catalogues are included
(
include_workspacetrue ortargetsincludesworkspace).
Organisation product access uses the discovery capability. Missing scope
returns HTTP 403 with insufficient_scope. Missing product
entitlement returns entitlement_required (or related entitlement errors). See
Scopes.
Do not reuse search.read for Discovery. Catalogue Search keeps
search.read; Discovery uses discovery.read /
discovery.write.
Discovery credits
Billable Discovery turns consume organisation Discovery credits. Billing is owned by the OAuth client's organisation. Empty sessions (no billable turns) cost nothing.
The first successful billable Discovery turn costs 1 Discovery credit and covers up to 10 billable turns in that session. Each additional billable turn after the tenth costs 1 additional Discovery credit.
- Exact 1-10 billable turns in a session = 1 Discovery credit total.
- Billable turn 11 = 2 Discovery credits total.
- Billable turn 12 = 3 Discovery credits total.
Successful searches and no_results outcomes are billable.
Upstream/provider failures and non-executable outcomes are not billed.
Inspect balances with GET /v1/me/credits
(public credit key discovery). Insufficient balance returns HTTP 402
with insufficient_credits.
Request
Catalogue selection
Catalogue selection is identical to
POST /v1/search/tracks:
include_workspace: include organisation Workspace catalogues.include_atlas: include MusiMap Atlas (requiresatlas.read).targets: alternative array ofworkspaceand/oratlas.catalog_id/catalog_ids: optional Workspace catalogue overrides.
Defaults match Catalogue Search: Workspace on, Atlas off, unless you override.
The catalogue target is fixed for the life of a session. On continuation, omit the
catalogue fields to reuse the session value. Supplying an equivalent target is allowed;
a conflicting target returns HTTP 400 with
catalogue_target_conflict.
First call (creates a session)
POST /v1/discovery without session_id creates a session, runs the
first turn, and returns results plus the new session_id.
Request
curl -X POST "https://api.musimap.com/v1/discovery" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "sad music with a little bit of rock and female vocals",
"include_workspace": false,
"include_atlas": true
}'
Continue a session
Pass the stored session_id to continue the same conversation. Omit the catalogue
fields so the session target stays unchanged.
Request
curl -X POST "https://api.musimap.com/v1/discovery" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"session_id": "<session_id>",
"query": "the happiest music for a night in a bar"
}'
The second query is also a valid first-turn query. Omit session_id to run it as
the start of a new session instead.
Response
A successful turn returns the standard envelope with the session and the turn results.
Result-set semantics
Discovery search results are not paginated. Each
POST /v1/discovery turn returns the bounded result set produced for that
turn. Do not send page, page_size, cursors, or other
pagination parameters on the search call; they are not part of this contract.
The same natural-language query is not guaranteed to return an identical result set on a later call. Results may legitimately change as catalogue content, MusiMap intelligence, ranking, or other search inputs evolve. Re-running the same query is a new execution, not a way to fetch another “page” of a previous result set.
A completed turn is a historical snapshot. To retrieve what a previous
turn returned, use the session-history endpoints, in particular
GET /v1/discovery/sessions/{session_id}/turns
(and the related session list/detail endpoints). Identify a turn by the
session_id and turn_seq returned on every successful
POST /v1/discovery. Do not reissue the query expecting the original hits
to come back unchanged.
Pagination applies only to browsing organisation session and turn history (list sessions, list turns). That is intentional: history lists are pageable; live Discovery search results are not.
Response
{
"status": 200,
"message": "OK",
"data": {
"session_id": "<session_id>",
"turn_seq": 1,
"status": "success",
"catalogue_target": {
"include_workspace": false,
"include_atlas": true
},
"created_at": "<iso8601>",
"updated_at": "<iso8601>",
"result_count": "<n>",
"results": [
{
"track_id": "<track_id>",
"title": "<title>",
"artists": [{ "name": "<artist>" }],
"relevance_score": "<0..1>",
"relative_score": "<0..1>",
"reasons": ["<public reason>"],
"satisfied_constraints": ["<satisfied constraint>"],
"coverage_summary": "<Matched n of m criteria | null>",
"source": { "layer": "atlas" }
}
],
"assistant_message": null
}
}
On a continuation, the response reuses the same session_id, increments
turn_seq, and returns the new turn's results.
Turn outcomes
Branch on the HTTP status first, then on data.status (HTTP 2xx) or
data.error_code (HTTP 4xx/5xx). Every outcome below keeps
session_id and turn_seq, so you can continue the same session.
| HTTP | Field | Meaning | Billable |
|---|---|---|---|
| 200 | status: success | The search ran and returned results. | Yes |
| 200 | status: no_results | The search ran and no track matched. results is empty. | Yes |
| 200 | status: needs_clarification | The request was not searched because it can be read in more than one way. The question is in assistant_message; no results are returned. | No |
| 202 | status: queued or processing | A temporary provider failure occurred and the turn was scheduled for an automatic retry. It may still complete; read it later with turn history instead of resubmitting. | Only if it later completes as success or no_results |
| 422 | error_code: discovery_cannot_execute | The request was understood but not executed, because some criteria cannot be searched or resolved. See below. | No |
| 402 | error_code: insufficient_credits | The Discovery credit balance cannot cover this billable turn. | No |
| 503 | error_code: upstream_unavailable | A temporary dependency failure. Retry later. | No |
| 500 | error_code: internal_error | An unexpected failure. | No |
Clarification
{
"status": 200,
"message": "OK",
"data": {
"session_id": "<session_id>",
"turn_seq": 1,
"status": "needs_clarification",
"catalogue_target": { "include_workspace": false, "include_atlas": true },
"created_at": "<iso8601>",
"updated_at": "<iso8601>",
"assistant_message": "<clarification question>"
}
}
Answer in the same session by sending a new POST /v1/discovery with the
session_id. Restate the complete request with the missing detail: a
clarification turn does not change the session's current search.
Requests that cannot be executed
When some criteria cannot be searched in the selected catalogue, or cannot be resolved,
Discovery does not run the search and returns HTTP 422.
data.details lists every blocking criterion.
{
"status": 422,
"message": "The Discovery request cannot be executed because some criteria are not searchable in the selected catalogue.",
"data": {
"error_code": "discovery_cannot_execute",
"details": [
{
"criterion": "piano",
"reason": "criterion_not_searchable",
"message": "I understood 'piano', but that criterion cannot currently be searched in this catalogue."
},
{
"criterion": "ballad",
"reason": "criterion_not_searchable",
"message": "I understood 'ballad', but that criterion cannot currently be searched in this catalogue."
}
],
"session_id": "<session_id>",
"turn_seq": 1
}
}
Each entry in details has:
criterion(optional): the criterion as Discovery understood it. It may be a normalised form of your wording, and is omitted when no user-facing text is available.reason: one ofcriterion_not_searchable,criterion_not_understood,criterion_combination_not_supported,conflicting_criteria,no_searchable_criteria,request_not_supported. Treat unknown values ascriterion_not_understood.message: a human-readable explanation. Do not parse it.
The session stays open. Continue with the same session_id and restate the
request without the blocking criteria (for example “slow sad music”). The
rejected turn is not remembered as the current search, so a follow-up such as
“remove the piano” on its own is not enough.
relevance_score, relative_score, explanations, and source
relevance_score: intrinsic relevance of the track to the query on a closed interval [0, 1] — how well it satisfies the requested musical criteria, independent of the other results. Hard constraints such as a release-year range decide which tracks are eligible; they do not change this score.relative_score:relevance_scorerelative to the most relevant result of the same search, on [0, 1]. The top result is1even when itsrelevance_scoreis lower.reasons: one ordered, user-facing explanation per requested criterion the track satisfies or is scored on, musical criteria first, then mandatory constraints. Forsad music with a little bit of rock, female vocals and a bpm between 100 and 135:Sad 94 %,Rock 43 % (requested: 10–45 %),High Pitched Vocal 99 %,BPM 112 (requested: 100–135). Display the count as the length of this list.satisfied_constraints: the entries ofreasonsthat are mandatory constraints (for example the BPM range). They decide eligibility only and never changerelevance_score. Do not render them a second time.coverage_summary: how many of the criteria the user requested the track satisfies, musical and numeric alike, for exampleMatched 4 of 4 criteria, when the request has at least two criteria; otherwisenull. A requested condition that could not be recognised stays in the count and is named. It is a summary, not a reason, and it is not the relevance score.source.layer:workspaceoratlas(pluscatalog_idwhen the hit is from Workspace).
Session endpoints
These read-only endpoints return sessions your organisation already created. They require
discovery.read.
List sessions
Request
curl "https://api.musimap.com/v1/discovery/sessions?page=1&page_size=20" \ -H "Authorization: Bearer $ACCESS_TOKEN"
Returns the standard pagination envelope
(count, page, page_size, total_pages,
has_next, has_previous, results).
Each result includes session_id, catalogue_target,
created_at, updated_at, and turn_count.
Sessions are organisation-scoped.
Session detail
Request
curl "https://api.musimap.com/v1/discovery/sessions/$SESSION_ID" \ -H "Authorization: Bearer $ACCESS_TOKEN"
Returns session metadata and catalogue configuration only. Turn history is not embedded;
use the turns list endpoint. Unknown or foreign sessions return HTTP 404
with not_found (no cross-organisation existence leak).
Paginated turn history
Use this endpoint to page through the turns already stored for a session, including the result snapshot captured when each turn completed. Pagination here pages history, not a live Discovery result set.
Request
curl "https://api.musimap.com/v1/discovery/sessions/$SESSION_ID/turns?page=1&page_size=20" \ -H "Authorization: Bearer $ACCESS_TOKEN"
Each turn includes sequence, submitted query, public status, timestamps, and the same
public result shape as the live POST /v1/discovery response
(relevance_score, relative_score, reasons,
satisfied_constraints, coverage_summary, source).
Turns stored before these fields existed return them derived from their stored
explanations. Prefer this
endpoint when you need the exact hits a prior turn returned.
Errors
- 422
discovery_cannot_execute: the request was not executed because some criteria cannot be searched or resolved (non-billable; see Requests that cannot be executed). - 402
insufficient_credits: organisation Discovery credit balance cannot cover the next billable turn. - 500
internal_error: an unexpected failure (non-billable). - 409
version_conflict/idempotency_conflict: the suppliedexpected_versionis stale, or anidempotency_keywas reused with a different query. - 503
upstream_unavailable: a dependency failure prevented Discovery from completing (non-billable; no provider identity in the public body). - 404
not_found: session unknown to this organisation. - 400
catalogue_target_conflict: continuation supplied a catalogue target that conflicts with the session. - 403
insufficient_scope: missingdiscovery.write,discovery.read,atlas.read, orcatalog.readas required by the call.
Envelope shape and shared codes: Errors. Pagination envelope: Pagination.
Examples
Two queries you can run as a first call to see Discovery behave differently:
- “sad music with a little bit of rock and female vocals”: mood, genre, and vocal character in one sentence.
- “the happiest music for a night in a bar”: mood and listening context.
Run each one without session_id to get two independent sessions, or send the
second one with the first session's session_id to see a continuation.
Prefer structured filters instead?
When you already know title tokens, tag IDs, or identifiers, use Catalogue Search.