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/reimaginefrom 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 with402before 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_LIMITwith aRetry-Afterheader.
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/uploadsHeaders
| Header | Type | Required | Description |
|---|---|---|---|
X-API-Key | string | Yes | Your API key |
Content-Type | string | Yes | multipart/form-data to send a file, or application/json to send a url |
Body parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
file | file | One of file / url | — | The MP3 or WAV, as a multipart part. Max 20 MB, 10 s – 10:00 long |
url | string | One 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 |
feature | string | Yes | — | "reimagine" or "remove_vocals" |
trimStartSeconds | number | No | 0 | For 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 |
attestationVersion | string | Yes | — | The rights statement version you showed your user — currently "2026-09-30". Read it from GET /v1/songs/config |
attestationConfirmed | boolean | Yes | — | 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
{
"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
| Code | Status | Description |
|---|---|---|
ATTESTATION_REQUIRED | 400 | attestationVersion missing or not current, or attestationConfirmed not true. Checked before we download a url. details has the current version and statements |
VALIDATION_ERROR | 400 | Bad feature; neither or both of file / url; a url that isn't public https |
INSUFFICIENT_CREDITS | 402 | A Reimagine upload and no credit left for the transcription. Nothing was uploaded or charged. details.cost is 1 |
PAID_PLAN_REQUIRED | 403 | Not on a paid plan |
CATALOGUE_MATCH | 409 | The recording matches a commercial release. The upload is deleted; details.match names the release |
FILE_TOO_LARGE | 413 | Over 20 MB |
UNSUPPORTED_FORMAT / UNREADABLE_AUDIO | 415 | Not an MP3 or WAV we can read |
SONG_TOO_LONG / SONG_TOO_SHORT | 422 | Outside 10 s – 10:00. details.durationSeconds is what we measured |
SOURCE_UNAVAILABLE | 422 | We couldn't download the url |
TRIM_FAILED | 422 | We couldn't cut the window from this file |
UPLOAD_LIMIT | 429 | More 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 |