Postal Developers
REST API

Natural-language search

Rank your whole library against a description of the music you need.

POST /api/v1/tracks/search

Scope 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…"]
}
FieldTypeNotes
querystring, 1–2000Required
limit1–50Default 20
constraints.bpmRange[min, max]Hard filter
constraints.durationRange[minSec, maxSec]Hard filter
constraints.hasVocalsbooleanHard filter
constraints.keystringHard filter
constraints.languagestringHard filter, lyric language
seedTrackIdsstring[] ≤ 50Steer toward these tracks
excludeTrackIdsstring[] ≤ 200Keep 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

data
{
  "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": "…"
}
FieldMeaning
results[].score0–1 relevance, higher is better
results[].reasonShort human explanations of the match
interpretationHow the query was read. Use it to refine: if genres is wrong, name the genre explicitly; if bpmRange is missing, add a constraint
interpretation.briefParsedfalse means the parser was unavailable and the prose went to the ranker unstructured. Results are real but less precisely constrained
suggestedNameA playlist title for this set, ready for Create playlist
searchIdThe Co-Work history entry, visible to the library owner in the app

Refine loop

  1. Search with prose only. Read interpretation.
  2. Move anything that must hold into constraints.
  3. Put the tracks you have already shortlisted into excludeTrackIds and the ones you liked into seedTrackIds, 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…".

On this page