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/tracksScope read:tracks. Cursor-paginated.
| Query | Type | Default | Notes |
|---|---|---|---|
limit | 1–200 | 50 | |
cursor | string | paging.next_cursor from the previous page | |
sort | created_at, -created_at, name, -name | -created_at | |
q | string | Name search | |
tags | CSV | Tag names, e.g. tags=Rock,Blues | |
tagMode | any | all | any | How multiple tags combine |
createdAfter | YYYY-MM-DD | Inclusive | |
createdBefore | YYYY-MM-DD | Inclusive | |
fields | CSV | all | Sparse 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": [
{
"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/countScope 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:
{
"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-dataScope create:tracks. Up to 25 audio files per request, 1 GB each.
| Field | Type | Notes |
|---|---|---|
files | file[] | Required. audio/* or application/octet-stream |
visible | true | false | Default false. Whether the track shows in the library list |
audioProfiles | JSON string | Optional 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"}}]'{
"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/urlScope 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.
{
"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.
{
"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.
| Field | Type |
|---|---|
title, artist, album, album_artist, composer, album_composer | string |
bpm | number |
key | string, e.g. Am, F# |
year, album_year | string |
isrc | string |
trackGenre | string (free text, distinct from taxonomy genres) |
track_number, disc_number, total_tracks_album, total_discs_album | string |
name | string (file display name) |
lyrics, notes | string |
Emits a track.updated webhook. Returns the updated track.
Find duplicates
GET /api/v1/tracks/duplicates?limit=20&offset=0Scope 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.
{
"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.
{
"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-artScope 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.
{ "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.