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.

Discovery vs Catalogue Search

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.

  1. The first call creates the session. Send POST /v1/discovery with a query and no session_id. There is no separate session-create endpoint.
  2. 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.
  3. MusiSearch remembers the current intent. A follow-up with the same session_id is read as a refinement of what the session is already searching for, so you do not need to restate the whole request.
  4. 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.
  5. A new search is a new session. To start unrelated work, omit session_id and call POST /v1/discovery again. 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_atlas is true or targets includes atlas (same Atlas gate as Catalogue Search).
  • catalog.read: required when Workspace catalogues are included (include_workspace true or targets includes workspace).

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 (requires atlas.read).
  • targets: alternative array of workspace and/or atlas.
  • 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.

HTTPFieldMeaningBillable
200status: successThe search ran and returned results.Yes
200status: no_resultsThe search ran and no track matched. results is empty.Yes
200status: needs_clarificationThe 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
202status: queued or processingA 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
422error_code: discovery_cannot_executeThe request was understood but not executed, because some criteria cannot be searched or resolved. See below.No
402error_code: insufficient_creditsThe Discovery credit balance cannot cover this billable turn.No
503error_code: upstream_unavailableA temporary dependency failure. Retry later.No
500error_code: internal_errorAn 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 of criterion_not_searchable, criterion_not_understood, criterion_combination_not_supported, conflicting_criteria, no_searchable_criteria, request_not_supported. Treat unknown values as criterion_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_score relative to the most relevant result of the same search, on [0, 1]. The top result is 1 even when its relevance_score is lower.
  • reasons: one ordered, user-facing explanation per requested criterion the track satisfies or is scored on, musical criteria first, then mandatory constraints. For sad 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 of reasons that are mandatory constraints (for example the BPM range). They decide eligibility only and never change relevance_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 example Matched 4 of 4 criteria, when the request has at least two criteria; otherwise null. 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: workspace or atlas (plus catalog_id when 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 supplied expected_version is stale, or an idempotency_key was 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: missing discovery.write, discovery.read, atlas.read, or catalog.read as 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.