Natural-language search
Rank your whole library against a description of the music you need.
POST /api/v1/tracks/searchScope read:tracks. Costs one credit per call. Failed calls are not
charged.
Describe the music in prose. Postal parses the description into musical targets (genre, mood, energy, tempo, vocals, reference artist, use) and ranks every analysed track in your library against them. The same engine powers the Co-Work panel in the app, so an integration and a person searching the same library get the same ordering.
Request
{
"query": "dark cinematic trap for a chase scene, no vocals, around 140",
"limit": 20,
"constraints": {
"bpmRange": [135, 145],
"durationRange": [60, 180],
"hasVocals": false,
"key": "Fm",
"language": "en"
},
"seedTrackIds": ["cm1x…"],
"excludeTrackIds": ["cm1a…", "cm1b…"]
}| Field | Type | Notes |
|---|---|---|
query | string, 1–2000 | Required |
limit | 1–50 | Default 20 |
constraints.bpmRange | [min, max] | Hard filter |
constraints.durationRange | [minSec, maxSec] | Hard filter |
constraints.hasVocals | boolean | Hard filter |
constraints.key | string | Hard filter |
constraints.language | string | Hard filter, lyric language |
seedTrackIds | string[] ≤ 50 | Steer toward these tracks |
excludeTrackIds | string[] ≤ 200 | Keep out, e.g. an existing shortlist |
Put hard requirements in constraints
The ranker is good at "feels like" and poor at negation. "No vocals" in the
prose is a hint; hasVocals: false is a guarantee. Anything that must be
strictly true belongs in constraints.
Response
{
"query": "dark cinematic trap for a chase scene, no vocals, around 140",
"count": 20,
"results": [
{
"id": "cm1x…",
"title": "Pursuit",
"artists": ["…"],
"durationSec": 142,
"bpm": 140,
"key": "Fm",
"genres": ["Trap", "Cinematic"],
"moods": ["Dark", "Tense"],
"score": 0.91,
"reason": ["Dark, tense trap at 140 BPM", "Instrumental"]
}
],
"interpretation": {
"briefParsed": true,
"summary": "Dark, tense cinematic trap around 140 BPM, instrumental",
"referenceArtist": null,
"genres": ["Trap", "Cinematic"],
"moods": ["Dark", "Tense"],
"bpmRange": [135, 145],
"keyMode": "minor",
"vocalPreference": "instrumental",
"energy": 0.85,
"valence": 0.2,
"lyricThemes": [],
"appliedConstraints": { "bpmRange": [135, 145], "hasVocals": false },
"appliedTuning": { "energy": 0.85, "valence": 0.2 }
},
"suggestedName": "Chase Scene Shortlist",
"searchId": "…"
}| Field | Meaning |
|---|---|
results[].score | 0–1 relevance, higher is better |
results[].reason | Short human explanations of the match |
interpretation | How the query was read. Use it to refine: if genres is wrong, name the genre explicitly; if bpmRange is missing, add a constraint |
interpretation.briefParsed | false means the parser was unavailable and the prose went to the ranker unstructured. Results are real but less precisely constrained |
suggestedName | A playlist title for this set, ready for Create playlist |
searchId | The Co-Work history entry, visible to the library owner in the app |
Refine loop
- Search with prose only. Read
interpretation. - Move anything that must hold into
constraints. - Put the tracks you have already shortlisted into
excludeTrackIdsand the ones you liked intoseedTrackIds, then search again.
When not to use it
Use GET /tracks when you know the name, the tag
or the date. It is free and returns immediately. Search is for "find me
something that feels like…".