Split a song into stems
November 14, 2026
Table of contents
Split a finished song into stems with ACE Music’s stem splitter, on the account that holds the song. Stems need a paid ACE Music plan (Creator or Artist).
mode | Tracks | Credits |
|---|---|---|
vocals (default) | vocal and instrumental | 5 |
all | every stem ACE Music finds, such as vocals, backing_vocals, drums, bass, guitar, keyboard, strings, synth, woodwinds and fx | 60 |
Each track is an MP3 behind a signed link valid for 24 hours (expires). Read the job again with GET /jobs/jobid, or the song with GET /music/songId, for fresh links. A song that was already split in that mode answers at once with its tracks.
Sync, async and webhooks
- Every request runs as a job. Sync (the default): the POST waits and answers
200with the finished job record, or with the job’s error code and the record. In our tests an ACE-Step request took 20 to 95 seconds, ACE Music v1.1 Preview about 35 to 75 seconds, ACE Music v1 about 80 to 105 seconds and a stem split about 90 seconds. If the job is still running after 100 seconds, the POST answers202with the job as it stands: fetch the result with GET /jobs/jobid. Always checkstatus: onlycompletedcarries the links. - Async: pass
async: true. The POST answers201at once with the job instatus: "processing". Poll GET /jobs/jobidor wait for the webhook. replyUrlreceives onePOSTof the final job record, as JSON, when the job completes or fails, for sync and async requests alike. WithoutreplyUrl, the defaultreplyUrlof your useapi.net account is used, see POST account, when it passes the same checks as a request’s: a publichttpsURL.- A job that has not finished 30 minutes after it started fails with
504. ACE Music may still finish it and has likely charged for it, so look for its songs with GET /music before you run it again. - Each account runs at most
maxJobsrequests at a time (default4, which is ACE Music’s own limit, range1to4, set with POST /accounts). GET /jobs shows what is running. A job can be read for 7 days.
https://api.useapi.net/v1/acemusic/music/stems
Request Headers
Authorization: Bearer {API token}
Content-Type: application/json
# Alternatively you can use multipart/form-data
# Content-Type: multipart/form-data
API tokenis required, see Setup useapi.net for details.
Request Body
{
"songId": "user:[email protected]:so_3f9c2a7d81e64b05a9d2c4e7",
"mode": "all",
"async": true,
"replyUrl": "https://your-domain.com/webhook",
"replyRef": "stems-1"
}
songIdis required, a finished song’ssongId(user:<id>-<email>-song:so_…). The split runs on the account that holds it.modeis optional.
Supported values:vocals(default),all.asyncis optional,trueto answer201at once and run the split in the background. Default:false.replyUrlis optional, a publichttpsURL that receives onePOSTof the job record when the split completes or fails.
Callback body has the same JSON shape as GET /jobs/jobidresponse.
Maximum length:2048characters.replyRefis optional, your own reference echoed back in the job record.
Maximum length:1024characters.
Responses
-
The split is done.
{ "jobid": "j1114203012775104298s-u1234-email:[email protected]:so_3f9c2a7d81e64b05a9d2c4e7.0-bot:acemusic", "type": "stems", "status": "completed", "email": "[email protected]", "created": "2026-11-14T20:30:12.775Z", "stems": { "mode": "vocals", "songId": "user:[email protected]:so_3f9c2a7d81e64b05a9d2c4e7", "tracks": [ { "type": "vocal", "url": "https://….r2.cloudflarestorage.com/acemusic-data-prod/users/…/stem/202611/…?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=86400&…", "duration": 135.02 }, { "type": "instrumental", "url": "https://….r2.cloudflarestorage.com/acemusic-data-prod/users/…/stem/202611/…?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=86400&…", "duration": 135.02 } ], "expires": "2026-11-15T20:31:40.508Z" }, "completed": "2026-11-14T20:31:39.143Z" } -
async: true, the split is running. A sync request that is still running after 100 seconds answers202with the same body.{ "jobid": "j1114203348120936514s-u1234-email:[email protected]:so_3f9c2a7d81e64b05a9d2c4e7.2-bot:acemusic", "type": "stems", "status": "processing", "email": "[email protected]", "created": "2026-11-14T20:33:48.120Z", "stems": { "mode": "all", "songId": "user:[email protected]:so_3f9c2a7d81e64b05a9d2c4e7", "tracks": [] }, "replyUrl": "https://your-domain.com/webhook", "replyRef": "stems-1" } -
{ "error": "Parameter mode (drums) valid values: vocals,all", "code": 400 } -
Invalid API token.
{ "error": "useapi.net ⁝ Unauthorized", "code": 401 } -
The account is on the Free plan, or the song belongs to another useapi.net user.
{ "error": "Splitting the song into stems needs a paid ACE Music plan (the account is on free): Downloads come with a paid plan", "code": 403 } -
The account has too few credits for the split.
costis what it needs. -
Sync mode, ACE Music could not split the song. The body is the failed job record, with ACE Music’s reason in
error. -
The song’s account is running its
maxJobs. Retry after theRetry-Afterheader (30 seconds).{ "error": "Account [email protected] is running 4 of 4 jobs (maxJobs). Wait for one to finish, or raise maxJobs with POST /accounts", "code": 429 } -
596 Account Error
ACE Music refused the stored email and password of the song’s account. Add it again with the current password.
Model
The job record. GET /jobs/jobid and the replyUrl webhook return the same shape.
{ // TypeScript, all fields are optional
jobid: string // j<time><random><m|s>-u<id>-email:<email>-task:<song>.<song or 0|2>-bot:acemusic
type: 'music' | 'stems'
status: 'processing' | 'completed' | 'failed'
email: string // the ACE Music account the job runs on
created: string // ISO 8601
completed?: string // ISO 8601, when the job completed or failed
model?: string // music: the model you asked for
modelUsed?: string // music: the model that made the songs, see Model switching on POST /music
songs?: Song[] // music: the request's two songs, see Song below. A song deleted since, with
// DELETE /music/{songId} or on acemusic.ai, is only { songId, taskId, status: 'deleted' }
stems?: { // stems
mode: 'vocals' | 'all'
songId: string // the song that was split
songStatus?: 'deleted' // the song was deleted, its stems went with it (the job failed with code 404)
tracks: {
type: string // vocals: vocal, instrumental. all: vocals, backing_vocals, drums, bass, guitar,
// keyboard, strings, synth, woodwinds, fx (the stems ACE Music found)
url: string | null // signed mp3 link, valid 24 hours
duration?: number // seconds
}[]
expires?: string // ISO 8601, when the track links stop working (read the job again for fresh ones)
}
error?: string // status 'failed': why
code?: number // status 'failed': 400 | 422 | 504 | 404 | 596, see GET /jobs/{jobid}
replyUrl?: string
replyRef?: string
}
Song:
{ // TypeScript, all fields are optional
songId: string // user:<id>-<email>-song:so_… use it as a reference, to split, download or delete
taskId: string // ACE Music's own id of the song (so_…)
status: 'planning' | 'scoring' | 'queued' | 'running' | 'succeeded' | 'failed' | 'interrupted'
title: string | null
model: string | null // acemusic-v1 | acemusic-v1.1 | ace-step-1.5-xl, null for an uploaded file
style: string | null // the style the song was made with (ACE Music writes it for a prompt request)
lyrics: string | null // [verse] / [chorus] tagged
instrumental: boolean | null
duration: number | null // seconds
audioUrl: string | null // public mp3 link, https://acemusic.ai/data/<taskId>.mp3, once the song succeeded.
// It never changes: store this one. null for an uploaded file: get its audio with
// GET /music/download
playUrl?: string | null // the same mp3 as a direct link for players, valid until playUrlExpires (about an hour):
// some browsers (Brave) block the redirect behind audioUrl. Store audioUrl, play playUrl.
// For about a minute after the song is made it is audioUrl itself (ACE Music serves the
// file directly then). In job records and GET /music/{songId} only, not in GET /music or
// POST /files. null while the song is not finished, or when the link could not be read
playUrlExpires?: string | null // ISO 8601, when playUrl stops working: read the job or the song again for a fresh one
// an upload (kind 'upload'): no audioUrl; playUrl is its own audio, signed for 24 hours
coverUrl: string | null // cover art (jpg)
pageUrl: string | null // the song's page on acemusic.ai, when ACE Music made one
created: string | null // ISO 8601
finished: string | null // ISO 8601
kind: 'original' | 'cover' | 'mashup' | 'edit' | 'upload'
score: boolean // true when ACE Music already has the song's score (GET /music/score)
error?: { // a failed song
code?: string // ACE Music's code, e.g. ref_no_melody, song_not_made
message?: string
reason?: string
}
refunded?: boolean // ACE Music gave the credits back for a failed song
stems?: { // splits made with POST /music/stems
vocals?: { status?: string, tracks: { type?: string, url: string | null, duration?: number }[] }
all?: { status?: string, tracks: { type?: string, url: string | null, duration?: number }[] }
} // track links are signed (24 hours) on GET /music/{songId}, null elsewhere
upload?: { // an uploaded audio file (POST /files)
seconds?: number // its length, counted against the account's upload minutes
bpm?: number // the tempo ACE Music read
transcription?: string // done once ACE Music has read its lyrics, style and melody
}
}
Examples
-
curl -X POST "https://api.useapi.net/v1/acemusic/music/stems" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer …" \ -d '{ "songId": "user:[email protected]:so_3f9c2a7d81e64b05a9d2c4e7", "mode": "vocals" }' -
const token = "API token"; const response = await fetch("https://api.useapi.net/v1/acemusic/music/stems", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, body: JSON.stringify({ songId: "user:[email protected]:so_3f9c2a7d81e64b05a9d2c4e7", mode: "vocals" }) }); const job = await response.json(); console.log(response.status, job.status); job.stems?.tracks.forEach(track => console.log(track.type, track.url)); -
import requests token = "API token" response = requests.post( "https://api.useapi.net/v1/acemusic/music/stems", headers={"Authorization": f"Bearer {token}"}, json={"songId": "user:[email protected]:so_3f9c2a7d81e64b05a9d2c4e7", "mode": "vocals"} ) job = response.json() print(response.status_code, job.get("status")) for track in (job.get("stems") or {}).get("tracks", []): print(track["type"], track["url"])
Try It
The tracks play above the JSON once the split is completed.