API reference

Base URL https://api.stemx.app. All requests use HTTPS. Uploads are multipart/form-data; responses are JSON. New here? Start with the quickstart.

Authentication

Create a key on the keys page and send it on every request. The key is shown once; store it securely.

Header
Authorization: Bearer sk_live_...

A missing or unknown key returns 401 with {"error": "invalid_key"}.

Billing and quota

You are billed per second of input audio, from the same balance as the web app: your plan's monthly minutes first, then any pack minutes. Jobs that are queued or running count against the balance until they finish; failed and cancelled jobs are not billed. Plans and prices are on the pricing page.

POST /v1/jobs

Queue a separation. Returns 202 at once with a job ID. Fields: audio (the file; required, except for pitch and vocal_split jobs); stems, both (vocals and instrumental, the default) or six (vocals, drums, bass, guitar, piano, other); midi, true to also get a MIDI file per stem; format, mp3 (default), wav or flac, where WAV and FLAC need a paid plan or a minutes pack (otherwise 402 plan_required); and optionally webhook_url (see Webhooks). Audio up to 10 minutes and 100 MB.

Request
curl -X POST https://api.stemx.app/v1/jobs \
  -H "Authorization: Bearer sk_live_..." \
  -F "audio=@song.mp3" \
  -F "stems=six" \
  -F "midi=true"
Response (202)
{
  "job_id": "3n8fK2pQ7xW1mZ4vTbYh9Q",
  "status": "queued",
  "queue_position": 1,
  "created_at": "2026-10-02T04:12:00+00:00"
}

queue_position is 1-based and only present while the job is queued.

Analysis only

To get just the key and BPM, send -F "kind=analysis" with the audio: the job returns key and BPM without separating anything. It is not billed in minutes. It is capped at 100 a day per account, shared with the web key and BPM finder; over the cap you get 429 analysis_limit. Analysis jobs cannot take a webhook_url: they finish in seconds, so poll the job.

Pitch and lead/backing vocals

Two more kinds work on a finished full separation instead of new audio. Send kind=pitch with source_job_id and semitones (non-zero, within ±12) to transpose its stems, or kind=vocal_split with source_job_id to split its vocals into lead and backing. Both are billed in minutes like a separation.

Webhooks

Set webhook_url on POST /v1/jobs and we send a signed POST when the job finishes, succeeded or failed, instead of you polling. Delivery is best effort and never changes the job's own status. A cancelled job sends nothing. The body is {job_id, status, result_uris, error_code}, plus midi_uris when the job asked for MIDI, with result_uris null on a failure and error_code null on success.

The X-Stem-Signature header is the hex HMAC-SHA256 of the raw request body, keyed with your webhook secret from the keys page, where you can also rotate it. Verify against the raw bytes, not re-serialised JSON, and compare in constant time. If the account behind your key has no secret, setting webhook_url is refused with 422 webhook_secret_missing; a webhook is never sent unsigned.

Each delivery also carries X-Stem-Timestamp and X-Stem-Signature-V1, the hex HMAC-SHA256 of the timestamp, a dot and the raw body, so you can reject replays; X-Stem-Delivery identifies the delivery.

Verify (Python)
import hashlib
import hmac


def verify(body: bytes, signature_header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)

GET /v1/jobs/{job_id}

Read a job. status is one of queued, running, succeeded, failed, cancelled. When it has succeeded the response carries signed download links, valid for one hour from the moment you ask; ask again for fresh links while the files are kept (3 to 30 days depending on plan). A job that does not exist or is not yours returns 404 not_found. With midi=true, a succeeded job also has midi_uris: one signed .mid link per stem, keyed by stem name (all six, even with stems=both), plus combined, one file with every stem's notes. A stem with no notes has no link.

Request
curl https://api.stemx.app/v1/jobs/3n8fK2pQ7xW1mZ4vTbYh9Q \
  -H "Authorization: Bearer sk_live_..."
Response (200, succeeded)
{
  "job_id": "3n8fK2pQ7xW1mZ4vTbYh9Q",
  "status": "succeeded",
  "created_at": "2026-10-02T04:12:00+00:00",
  "result_uris": {
    "vocals": "https://storage.googleapis.com/...",
    "drums": "https://storage.googleapis.com/...",
    "bass": "https://storage.googleapis.com/...",
    "guitar": "https://storage.googleapis.com/...",
    "piano": "https://storage.googleapis.com/...",
    "other": "https://storage.googleapis.com/..."
  },
  "analysis": {
    "key": "G major",
    "key_confidence": 0.82,
    "bpm": 128,
    "bpm_confidence": 0.94
  },
  "midi_uris": {
    "vocals": "https://storage.googleapis.com/...",
    "drums": "https://storage.googleapis.com/...",
    "bass": "https://storage.googleapis.com/...",
    "guitar": "https://storage.googleapis.com/...",
    "piano": "https://storage.googleapis.com/...",
    "other": "https://storage.googleapis.com/...",
    "combined": "https://storage.googleapis.com/..."
  }
}

A failed job has status: "failed" and an error_code: input_unreadable, separation_failed, encode_failed, storage_failed, stuck, pitch_failed, lyrics_failed, source_expired, or source_has_no_vocals.

Key and BPM

analysis gives the track's key and tempo with a confidence from 0 to 1 for each. Job responses also carry beats and downbeats (in seconds) and meter when they are detected. It is best effort: on job responses a field that was not detected is omitted, and analysis may be absent altogether, so check for presence rather than null. On /v1/separate the same fields are written out as null. A key is only given when the model is confident: When it answers, it's right about 80% of the time. It answers on about 44% of songs.

GET /v1/jobs

List your jobs, newest first, as {"jobs": [...]}. Query parameters: limit (1 to 100, default 100) and status (one of the five statuses; anything else is 422 bad_status).

DELETE /v1/jobs/{job_id}

Cancel a job that is still queued. Returns the job with status: "cancelled"; it is not billed. A running job cannot be cancelled (409 already_running), nor can a finished one (409 already_terminal).

POST /v1/separate

Synchronous separation for short clips (up to 90 seconds): the response arrives when the stems are ready. Fields: audio, stems (vocals, instrumental, both or six, default both), format (mp3, flac or wav, default mp3). Use POST /v1/jobs for anything longer. MIDI is not available here.

Request
curl -X POST https://api.stemx.app/v1/separate \
  -H "Authorization: Bearer sk_live_..." \
  -F "audio=@clip.wav" -F "stems=both" -F "format=mp3"

The 200 response is {stems, duration_s, processing_ms, model, output_offset_ms, expires_at, analysis}: stems maps each stem name to a signed link, valid for one hour (until expires_at); duration_s is the length of the audio in seconds; model gives the separation model's name and attribution; analysis is described under Key and BPM above.

Errors

Job routes return {"error_code": ..., "detail": ...}; /v1/separate returns {"error": ..., "detail": ...}.

  • 401 invalid_key: missing or unknown key.
  • 402 plan_required: format is wav or flac without a paid plan or a minutes pack.
  • 403 account_disabled: the account behind the key is disabled.
  • 422 bad_params: stems or format is not one of the documented values. 422 missing_audio: a separation sent without audio.
  • 422 invalid_pitch_request, 422 invalid_semitones, 422 invalid_vocal_split_request: a pitch or vocal_split job without its source_job_id (and semitones), or with semitones outside ±12.
  • 404 source_not_found, 400 source_not_ready, 400 source_expired, 400 source_has_no_vocals: source_job_id is not a finished full separation of yours whose files and vocals are still available.
  • 422 bad_webhook_url: webhook_url is not an allowed destination.
  • 503 feature_disabled: that kind of job is switched off right now.
  • 429 quota_exceeded: your balance is used up. The error carries upgrade_url and remaining_s (seconds of allowance left). Buy a pack or upgrade on the pricing page.
  • 429 rate_limited: more than 120 requests a minute on one key; wait the Retry-After seconds.
  • 429 analysis_limit: the 100 analyses a day for the account are used.
  • 429 queue_full: on /v1/separate only, the service is busy; retry after retry_after_s seconds.
  • 422 unsupported_kind: kind is not a kind this API accepts.
  • 422 webhook_secret_missing: you set webhook_url but the account behind your key has no webhook secret; create one on the keys page.
  • 422 webhook_not_supported: you set webhook_url on a kind=analysis job; analysis jobs cannot take a webhook, so poll the job.
  • 413 too_large: file over 100 MB. 413 too_long: audio over 10 minutes.
  • 413 audio_too_long_for_sync: over 90 seconds on /v1/separate; use /v1/jobs.
  • 503 webhook_check_unavailable: webhook_url could not be checked right now; retry shortly.
  • 503 storage_failed: download links could not be signed (GET /v1/jobs/{job_id} and /v1/separate); retry shortly.
  • 503 queue_unavailable and 503 separation_timeout: on /v1/separate only; retry shortly, or submit the track to /v1/jobs.
  • 415 unsupported_media: the file could not be decoded as audio.

Limits

  • Files up to 100 MB and 10 minutes (90 seconds on /v1/separate).
  • 120 requests a minute per key. GET requests count toward it, so poll a job no more than about once every 2 to 5 seconds, or use a webhook instead.
  • Jobs at once: Free 1, Starter 1, Pro 2, Scale 4.

Models

Separation: BS-RoFormer-SW (bs-roformer-infer, MIT; weights enerjazzer/BS-ROFO-SW-Fixed); HT-Demucs 6s (Meta AI Research, demucs MIT). MIDI: YourMT3+ (mimbres/YourMT3) for pitched stems and ADTOF (CC BY-NC-SA) for drums. Lead and backing vocal split: models by UVR (Anjok07 & aufr33), used under their MIT licence. Tempo and key: Beat This! (Foscarin, Schlüter & Widmer, CPJKU); S-KEY (Kong et al., Deezer).