Postal Developers
REST API

Tracks

List, get, count, upload and update tracks. Find duplicates and trace submissions.

A track is one audio file with an audio profile (title, artist, BPM, key, tags…), optional versions, and a share link.

List tracks

GET /api/v1/tracks

Scope read:tracks. Cursor-paginated.

QueryTypeDefaultNotes
limit1–20050
cursorstringpaging.next_cursor from the previous page
sortcreated_at, -created_at, name, -name-created_at
qstringName search
tagsCSVTag names, e.g. tags=Rock,Blues
tagModeany | allanyHow multiple tags combine
createdAfterYYYY-MM-DDInclusive
createdBeforeYYYY-MM-DDInclusive
fieldsCSVallSparse field set, e.g. id,name,createdAt,audio_profile,link

Field names you can request with fields: id, name, createdAt, audio_profile, shareholders, link, share_link, cid. When link is included (the default) each item carries a link to its public player page or null if no share link exists.

curl "https://api.postal.music/api/v1/tracks?limit=50&tags=Cinematic&tagMode=all&createdAfter=2026-08-01" \
  -H "X-API-Key: $POSTAL_API_KEY"
data
{
  "data": [
    {
      "id": "cm1x…",
      "name": "Night Drive v3.wav",
      "createdAt": "2026-09-01T10:12:44.000Z",
      "cid": "bafk…",
      "audio_profile": {
        "title": "Night Drive",
        "artist": "…",
        "bpm": 92,
        "key": "Am",
        "durationSec": 214,
        "genres": [{ "genre": { "name": "Downtempo" } }],
        "moods": [{ "mood": { "name": "Warm" } }]
      },
      "link": "https://postal.music/song/8f2k…"
    }
  ],
  "paging": { "limit": 50, "next_cursor": "cm1w…" }
}

Count tracks

GET /api/v1/tracks/count

Scope read:tracks. Returns { "count": 1284 }.

Get a track

GET /api/v1/tracks/single?id={trackId}

Scope read:tracks. Returns the full track with every tag relation expanded and its shareholders:

data
{
  "track": {
    "id": "cm1x…",
    "name": "Night Drive v3.wav",
    "audio_profile": {
      "title": "Night Drive",
      "bpm": 92,
      "key": "Am",
      "genres": [{ "genre": { "id": "…", "name": "Downtempo" } }],
      "moods": [  ],
      "types": [  ],
      "instruments": [  ],
      "vocals": [  ],
      "lyricThemes": [  ],
      "eras": [  ],
      "characters": [  ],
      "movements": [  ],
      "tags": [  ]
    },
    "shareholderTracks": [
      { "share": 50, "role": "…", "shareholder": { "id": "…", "fullName": "…", "pro": "BUMA" } }
    ]
  },
  "id": "cm1x…",
  "userId": "…"
}

track is null when the id does not exist or belongs to another account.

Versions

The response also carries activeVersionId and a versions array (id, label, name, active, visible, createdAt) summarising the track's audio history. To replace the audio, switch versions or get download links, see Track versions.

Upload files

POST /api/v1/tracks/upload
Content-Type: multipart/form-data

Scope create:tracks. Up to 25 audio files per request, 1 GB each.

FieldTypeNotes
filesfile[]Required. audio/* or application/octet-stream
visibletrue | falseDefault false. Whether the track shows in the library list
audioProfilesJSON stringOptional per-file metadata, matched by filename: [{ "name": "song.wav", "audioProfile": { "bpm": 120, "key": "C#m" } }]
curl -X POST https://api.postal.music/api/v1/tracks/upload \
  -H "X-API-Key: $POSTAL_API_KEY" \
  -F "files=@night-drive.wav" \
  -F "files=@daylight.mp3" \
  -F "visible=true" \
  -F 'audioProfiles=[{"name":"night-drive.wav","audioProfile":{"bpm":92,"key":"Am"}}]'
data
{
  "uploaded": [
    { "id": "cm1x…", "name": "night-drive.wav", "visible": true },
    { "id": "cm1y…", "name": "daylight.mp3", "visible": true }
  ]
}

Audio analysis (tempo, key, tags, content hash) runs asynchronously after upload. Poll suggested tags or subscribe to the track.audio_profile_completed webhook.

Upload by URL

POST /api/v1/tracks/upload/url

Scope create:tracks. Postal fetches each file server-side. URLs must be publicly reachable, use http or https, and serve audio/* or application/octet-stream. Private network addresses are rejected.

Request
{
  "items": [
    {
      "fileUrl": "https://cdn.example.com/audio/alpha.wav",
      "visible": true,
      "audioProfile": { "bpm": 120, "key": "Am" },
      "details": { "title": "Alpha", "artist": "The Keys", "isrc": "NLA1G2600001" }
    },
    { "fileUrl": "https://cdn.example.com/audio/beta.mp3" }
  ]
}

items holds 1–25 entries. details accepts the same fields as Update details and is applied after the upload.

data
{
  "uploaded": [{ "id": "cm1x…", "name": "alpha.wav", "visible": true }],
  "failed": [{ "url": "https://cdn.example.com/audio/beta.mp3", "error": "HTTP 404 when fetching …" }]
}

Per-item failures never fail the whole request.

Update details

PATCH /api/v1/tracks/details?id={trackId}

Scope update:tracks. All fields optional; only the fields you send change.

FieldType
title, artist, album, album_artist, composer, album_composerstring
bpmnumber
keystring, e.g. Am, F#
year, album_yearstring
isrcstring
trackGenrestring (free text, distinct from taxonomy genres)
track_number, disc_number, total_tracks_album, total_discs_albumstring
namestring (file display name)
lyrics, notesstring

Emits a track.updated webhook. Returns the updated track.

Find duplicates

GET /api/v1/tracks/duplicates?limit=20&offset=0

Scope read:tracks. Groups tracks by content hash so the same bytes uploaded twice are caught, however they were renamed. Re-encoded files have a different hash and are not matched.

data
{
  "groups": [
    {
      "cid": "bafk…",
      "count": 2,
      "tracks": [
        { "id": "cm1x…", "name": "Night Drive v3.wav", "createdAt": "…" },
        { "id": "cm1z…", "name": "Night Drive FINAL.wav", "createdAt": "…" }
      ]
    }
  ],
  "totalGroups": 7,
  "duplicateTrackCount": 15,
  "missingCid": 12,
  "scannedTracks": 1284,
  "paging": { "limit": 20, "offset": 0 }
}

missingCid counts tracks that have not been analysed yet and could not be checked. Triggering auto-tag populates the hash.

Track submissions

GET /api/v1/tracks/submissions?id={trackId}

Scope read:tracks. Every brief the track was submitted to, newest first, including briefs owned by other accounts.

data
{
  "trackId": "cm1x…",
  "trackName": "Night Drive",
  "submissions": [
    {
      "id": "…",
      "briefId": "…",
      "briefName": "Indie ad campaign Q4",
      "briefStatus": "open",
      "briefDeadline": "2026-10-01T00:00:00.000Z",
      "message": "Fits the warm brief, instrumental mix attached",
      "createdAt": "2026-09-02T09:30:00.000Z",
      "viaRequestEmail": "supervisor@agency.example"
    }
  ]
}

Returns { "ok": false, "error": "TRACK_NOT_FOUND" } for an id you do not own.

Cover art

POST /api/v1/tracks/cover-art

Scope update:tracks. Provide trackId and one of url (public image URL) or base64 (raw base64 or a data:image/…;base64, URI). Optional filename, default cover.jpg.

data
{ "url": "https://…/cover-art/…/cover.jpg", "key": "…", "trackId": "cm1x…" }

Errors: 400 missing_image, 400 invalid_image_url, 400 invalid_image_type, 400 image_download_failed, 404 track_not_found.

On this page