Split a song into stems

November 14, 2026

Table of contents

  1. Request Headers
  2. Request Body
  3. Responses
  4. Model
  5. Examples
  6. Try It

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 200 with 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 answers 202 with the job as it stands: fetch the result with GET /jobs/jobid. Always check status: only completed carries the links.
  • Async: pass async: true. The POST answers 201 at once with the job in status: "processing". Poll GET /jobs/jobid or wait for the webhook.
  • replyUrl receives one POST of the final job record, as JSON, when the job completes or fails, for sync and async requests alike. Without replyUrl, the default replyUrl of your useapi.net account is used, see POST account, when it passes the same checks as a request’s: a public https URL.
  • 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 maxJobs requests at a time (default 4, which is ACE Music’s own limit, range 1 to 4, 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
Request Body
{
  "songId": "user:[email protected]:so_3f9c2a7d81e64b05a9d2c4e7",
  "mode": "all",
  "async": true,
  "replyUrl": "https://your-domain.com/webhook",
  "replyRef": "stems-1"
}
  • songId is required, a finished song’s songId (user:<id>-<email>-song:so_…). The split runs on the account that holds it.
  • mode is optional.
    Supported values: vocals (default), all.
  • async is optional, true to answer 201 at once and run the split in the background. Default: false.
  • replyUrl is optional, a public https URL that receives one POST of the job record when the split completes or fails.
    Callback body has the same JSON shape as GET /jobs/jobid response.
    Maximum length: 2048 characters.
  • replyRef is optional, your own reference echoed back in the job record.
    Maximum length: 1024 characters.
Responses
  • 200 OK

    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"
    }
    
  • 201 Created

    async: true, the split is running. A sync request that is still running after 100 seconds answers 202 with 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"
    }
    
  • 400 Bad Request

    {
      "error": "Parameter mode (drums) valid values: vocals,all",
      "code": 400
    }
    
  • 401 Unauthorized

    Invalid API token.

    {
      "error": "useapi.net ⁝ Unauthorized",
      "code": 401
    }
    
  • 403 Forbidden

    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
    }
    
  • 412 Precondition Failed

    The account has too few credits for the split. cost is what it needs.

  • 422 Unprocessable Entity

    Sync mode, ACE Music could not split the song. The body is the failed job record, with ACE Music’s reason in error.

  • 429 Too Many Requests

    The song’s account is running its maxJobs. Retry after the Retry-After header (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.