Finetuning.aiFinetuning.ai

POST /v1/songs/uploads

Upload a song for Reimagine or Remove vocals

Upload an MP3 or WAV — as a file, or as a URL for us to fetch — for Reimagine or Remove vocals. We check it, keep a window of up to 3:00, record your rights confirmation, and return the upload. For Reimagine we start listening for the lyrics straight away.

  • Reimagine uploads cost 1 credit for the transcription, charged when it starts. It counts toward the first POST /v1/reimagine from this upload (1 more credit, so 2 in total), and comes back automatically if the transcription fails. With no credit left, the upload is refused with 402 before anything is stored. Remove vocals uploads are free; the render costs 1.
  • Caps: 10 uploads per rolling hour and 30 per rolling 24 hours per account, counted together with the web app. Over either: 429 UPLOAD_LIMIT with a Retry-After header.

Show your user the rights statements. attestationConfirmed: true means your end user saw the statements from GET /v1/songs/config for this feature and confirmed them. Don't send it without showing them.

Uploads are deleted 24 hours after they arrive (expiresAt).

Request

POST https://pub.finetuning.ai/v1/songs/uploads

Headers

HeaderTypeRequiredDescription
X-API-KeystringYesYour API key
Content-TypestringYesmultipart/form-data to send a file, or application/json to send a url

Body parameters

ParameterTypeRequiredDefaultDescription
filefileOne of file / url—The MP3 or WAV, as a multipart part. Max 20 MB, 10 s – 10:00 long
urlstringOne of file / url—A public https URL we download the MP3 or WAV from. Same limits. The file name in the URL becomes the upload's name
featurestringYes—"reimagine" or "remove_vocals"
trimStartSecondsnumberNo0For songs over 3:00: where the 3-minute window starts. Clamped so the window fits inside the song. Ignored for songs of 3:00 or less
attestationVersionstringYes—The rights statement version you showed your user — currently "2026-09-30". Read it from GET /v1/songs/config
attestationConfirmedbooleanYes—Must be true (the string "true" in a multipart form), and only after your end user has seen and confirmed the statements

Send exactly one of file and url. In a multipart request, every other field is a form field.

Example request — upload a file

curl -X POST https://pub.finetuning.ai/v1/songs/uploads \
  -H "X-API-Key: ft_live_your_key_here" \
  -F "file=@my-song.mp3" \
  -F "feature=reimagine" \
  -F "trimStartSeconds=30" \
  -F "attestationVersion=2026-09-30" \
  -F "attestationConfirmed=true"
import { readFile } from 'node:fs/promises'

const form = new FormData()
form.append('file', new Blob([await readFile('my-song.mp3')], { type: 'audio/mpeg' }), 'my-song.mp3')
form.append('feature', 'reimagine')
form.append('trimStartSeconds', '30')
form.append('attestationVersion', '2026-09-30')
form.append('attestationConfirmed', 'true')

const res = await fetch('https://pub.finetuning.ai/v1/songs/uploads', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.FINETUNING_API_KEY! },
  body: form,
})
const { data: upload } = await res.json()

Example request — upload from a URL

curl -X POST https://pub.finetuning.ai/v1/songs/uploads \
  -H "X-API-Key: ft_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/audio/my-song.mp3",
    "feature": "remove_vocals",
    "attestationVersion": "2026-09-30",
    "attestationConfirmed": true
  }'
const res = await fetch('https://pub.finetuning.ai/v1/songs/uploads', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.FINETUNING_API_KEY!, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    url: 'https://example.com/audio/my-song.mp3',
    feature: 'remove_vocals',
    attestationVersion: '2026-09-30',
    attestationConfirmed: true,
  }),
})

Response

201 Created
{
  "data": {
    "id": "22970e5b-c403-494c-b10d-846df6dde7c5",
    "feature": "reimagine",
    "name": "my-song",
    "format": "mp3",
    "status": "transcribing",
    "durationSeconds": 180,
    "originalDurationSeconds": 212.4,
    "window": { "startSeconds": 30, "lengthSeconds": 180 },
    "lyricsCap": 3240,
    "lyrics": null,
    "transcript": null,
    "lowConfidenceWords": [],
    "fingerprintStatus": "skipped",
    "errorMessage": null,
    "pricing": { "transcriptionPaid": true, "transcriptionCreditApplied": false, "nextReimagineCredits": 1, "retranscribeCredits": 0 },
    "audioUrl": "https://dev.finetuning.ai/media/uploads/22970e5b-…?exp=1790866400&sig=…",
    "createdAt": "2026-09-30T13:53:20.151Z",
    "expiresAt": "2026-10-01T13:53:20.151Z",
    "deleted": false,
    "generations": []
  }
}

The upload object is described in full on GET /v1/songs/uploads/:id. A Remove vocals upload starts with status: "ready"; a Reimagine upload starts with "transcribing".

Errors

CodeStatusDescription
ATTESTATION_REQUIRED400attestationVersion missing or not current, or attestationConfirmed not true. Checked before we download a url. details has the current version and statements
VALIDATION_ERROR400Bad feature; neither or both of file / url; a url that isn't public https
INSUFFICIENT_CREDITS402A Reimagine upload and no credit left for the transcription. Nothing was uploaded or charged. details.cost is 1
PAID_PLAN_REQUIRED403Not on a paid plan
CATALOGUE_MATCH409The recording matches a commercial release. The upload is deleted; details.match names the release
FILE_TOO_LARGE413Over 20 MB
UNSUPPORTED_FORMAT / UNREADABLE_AUDIO415Not an MP3 or WAV we can read
SONG_TOO_LONG / SONG_TOO_SHORT422Outside 10 s – 10:00. details.durationSeconds is what we measured
SOURCE_UNAVAILABLE422We couldn't download the url
TRIM_FAILED422We couldn't cut the window from this file
UPLOAD_LIMIT429More than 10 uploads in the last hour or 30 in the last 24 hours (web and API together). The Retry-After header and details.retryAfterSeconds give the seconds until you can upload again; details.limit / details.windowSeconds name the cap

On this page