Appearance
Singing
Singing voices let a cloned voice sing. Cinara Encore replaces the singer in a song, or converts a vocal, in the voice. Cinara Tune can correct the pitch of a vocal.
The flow:
- Start from a cloned voice. Only cloned voices can get a singing voice. Its consent recording counts toward training.
- Add recordings until the recordings hold at least 90 seconds of speech. Each extra recording starts with its own consent statement for the same speaker, then the speaker keeps reading. Poll each one.
- Train the singing voice and poll the training job.
- Sing a finished song or an uploaded song or vocal in the voice, and poll the song job.
In this area, voiceId is the cloned voice's UUID (id from GET /v1/voices), not its providerVoiceId.
Endpoints:
GET /v1/singing-voicesPOST /v1/singing-voices/{voiceId}/recordingsGET /v1/singing-recordings/{id}POST /v1/singing-voices/{voiceId}/trainGET /v1/singing-trainings/{id}POST /v1/singingGET /v1/singing/{id}GET /v1/singing/{id}/files/{name}
List singing voices
GET /v1/singing-voices
Lists every cloned voice with its singing voice status, plus singing limits and credits.
Response
200 OK
json
{
"voices": [
{
"voiceId": "5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05",
"name": "Priya narrator",
"speakerName": "Priya Raman",
"status": "collecting",
"recordings": [
{ "kind": "clone", "heardSeconds": 72.9, "verifiedAt": "2026-09-15T09:14:02.551930+00:00" },
{ "kind": "extra", "heardSeconds": 41.3, "verifiedAt": "2026-09-15T14:02:18.004Z" }
],
"seconds": 114.2,
"neededSeconds": 90,
"canAddRecording": true,
"trainingId": null,
"error": null
}
],
"available": true,
"credits": { "training": 5000, "recording": 100, "perSecond": 20 },
"limits": {
"minTrainingSeconds": 90,
"maxRecordings": 6,
"trainingEpochs": 150,
"maxSongSeconds": 360,
"minRecordingSeconds": 20,
"maxRecordingSeconds": 300
},
"pitches": ["as-sung", "octave-lower", "octave-higher"]
}| Field | Description |
|---|---|
voices[].status | collecting, training, ready or failed. |
voices[].recordings | The clone's consent recording (kind: "clone") and extra recordings (kind: "extra"), with the seconds of speech heard in each. |
voices[].seconds | Speech across all recordings. Training needs neededSeconds. |
voices[].canAddRecording | false when the voice has 6 extra recordings or is training. |
voices[].trainingId | The latest training job's id. |
voices[].error | Why the last training failed. |
available | false when singing voices aren't available right now. |
limits.maxRecordings | Extra recordings per singing voice. |
limits.maxSongSeconds | Longest song that can be sung: 360 seconds. |
Credits
Free.
Example
bash
curl https://api.cinara.ai/v1/singing-voices \
-H "Authorization: Bearer $CINARA_API_KEY"Add a recording
POST /v1/singing-voices/{voiceId}/recordings
Checks an extra recording for a singing voice and adds it once its consent statement passes.
Needs a verified phone.
Get the statement from POST /v1/voices/consent with the same speaker name as the cloned voice (case and spacing don't matter). The recording starts with the statement, then the speaker keeps reading.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
key | string | yes | The storage key of a finished audio upload. |
challengeId | string | yes | An unused, unexpired statement created by the same member for the same speaker. |
durationSeconds | number | yes | The recording's length. 20–300 seconds. |
fileName | string | no | Up to 200 characters. Defaults to the upload's file name. |
Response
201 Created
json
{
"generation": {
"id": "6d2e8a4c-1b7f-4c3e-9a5d-8f0b2e6c4a17",
"module": "singing_recording",
"status": "running",
"input": {
"voiceId": "5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05",
"voiceName": "Priya narrator",
"speakerName": "Priya Raman",
"challengeId": "a7e2c9b1-4d3f-4e8a-b6c5-1f0d9e2a7b36",
"key": "workspaces/3f6b2a1c-8d4e-4b7a-9c2d-5e1f0a9b8c7d/uploads/2b8d4f6a-7c1e-4a9b-8d3f-6e0c2a5b9d41/reading-2.wav",
"fileName": "reading-2.wav",
"contentType": "audio/wav",
"durationSeconds": 48
},
"output": { "stage": "queued", "step": 1, "stageLabel": "Checking the consent statement" },
"credits": 100,
"error": null,
"created_at": "2026-09-15T14:01:50.330812+00:00",
"completed_at": null
},
"credits": 44656
}Errors
| Status | error | Message |
|---|---|---|
| 404 | not_found | Voice not found |
| 409 | invalid_request | This voice is training. Add recordings after it finishes. |
| 400 | invalid_request | A singing voice can have up to 6 extra recordings |
| 400 | invalid_request | Upload the recording first |
| 400 | invalid_request | The recording needs at least 20 seconds: the statement, then reading. |
| 400 | invalid_request | Recordings can be up to 5 minutes |
| 400 | invalid_request | This consent statement has expired or was already used. Get a new one. |
| 400 | invalid_request | Get a consent statement for Priya Raman. |
| 404 | not_found | That upload isn't finished. Upload the recording again. |
| 400 | invalid_request | Upload an audio recording |
| 400 | invalid_request | This consent statement was already used. Get a new one. |
| 402 | insufficient_credits | This needs 100 credits |
| 403 | phone_unverified | Verify your phone number to start creating. |
| 502 | generation_failed | The recording check couldn't start. Your credits were refunded. |
Credits
100 credits per recording checked. Refunded if the check fails.
Example
bash
curl -X POST "https://api.cinara.ai/v1/singing-voices/5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05/recordings" \
-H "Authorization: Bearer $CINARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"key": "workspaces/3f6b2a1c-8d4e-4b7a-9c2d-5e1f0a9b8c7d/uploads/2b8d4f6a-7c1e-4a9b-8d3f-6e0c2a5b9d41/reading-2.wav", "challengeId": "a7e2c9b1-4d3f-4e8a-b6c5-1f0d9e2a7b36", "durationSeconds": 48}'Get a recording check
GET /v1/singing-recordings/{id}
Checks a recording job, moves it forward, and returns it. Poll until status isn't running.
Response
200 OK
json
{
"generation": {
"id": "6d2e8a4c-1b7f-4c3e-9a5d-8f0b2e6c4a17",
"module": "singing_recording",
"status": "succeeded",
"input": {
"voiceId": "5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05",
"voiceName": "Priya narrator",
"speakerName": "Priya Raman",
"challengeId": "a7e2c9b1-4d3f-4e8a-b6c5-1f0d9e2a7b36",
"key": "workspaces/3f6b2a1c-8d4e-4b7a-9c2d-5e1f0a9b8c7d/uploads/2b8d4f6a-7c1e-4a9b-8d3f-6e0c2a5b9d41/reading-2.wav",
"fileName": "reading-2.wav",
"contentType": "audio/wav",
"durationSeconds": 48
},
"output": { "consent": { "matchedShare": 0.917, "codeHeard": true, "speakers": 1, "heardSeconds": 41.3 } },
"credits": 100,
"error": null,
"created_at": "2026-09-15T14:01:50.330812+00:00",
"completed_at": "2026-09-15T14:02:18.004772+00:00"
},
"credits": 44656
}A failed check has one of these error messages:
The one-time code wasn't heard. Read the whole statement aloud, including the code words, then keep reading.The consent statement wasn't read clearly enough. Read it word for word, then keep reading.More than one voice was heard. Record only the person whose voice this is.Keep reading for at least 20 seconds.The consent statement is missing. Start again.The recording check took too long and was stopped.This file couldn't be read. Try MP3, WAV, M4A, MP4 or WebM.The recording check failed.
Errors
| Status | error | Message |
|---|---|---|
| 404 | not_found | Not found |
Credits
Free to read.
Polling example
bash
while :; do
RES=$(curl -s "https://api.cinara.ai/v1/singing-recordings/$ID" -H "Authorization: Bearer $CINARA_API_KEY")
[ "$(echo "$RES" | jq -r .generation.status)" != "running" ] && break
sleep 5
done
echo "$RES" | jq '{status: .generation.status, error: .generation.error, consent: .generation.output.consent}'Train a singing voice
POST /v1/singing-voices/{voiceId}/train
Starts training a singing voice from the voice's recordings. No request body.
Needs a verified phone.
The recordings (the clone's consent recording and every extra recording) must hold at least 90 seconds of speech. Only one training runs per voice at a time.
Response
201 Created
json
{
"generation": {
"id": "f0a4c8e2-6b1d-4e7a-9c3f-2d5b8e1a6c90",
"module": "singing_training",
"status": "running",
"input": {
"voiceId": "5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05",
"voiceName": "Priya narrator",
"singingVoiceId": "3c7e1a9d-5b2f-4d8e-a0c6-9e4b1f7d2a58",
"seconds": 114,
"recordings": 2
},
"output": { "phase": "prepare", "stage": "queued", "step": 1, "stageLabel": "Preparing the recordings" },
"credits": 5000,
"error": null,
"created_at": "2026-09-15T14:10:03.119254+00:00",
"completed_at": null
},
"credits": 39656
}Errors
| Status | error | Message |
|---|---|---|
| 503 | unavailable | Singing voices aren't available yet. |
| 404 | not_found | Voice not found |
| 409 | invalid_request | This singing voice is already training. |
| 400 | invalid_request | Record about 12 more seconds first (78 of 90 s so far). |
| 402 | insufficient_credits | This needs 5,000 credits |
| 403 | phone_unverified | Verify your phone number to start creating. |
| 502 | generation_failed | Making the singing voice couldn't start. Your credits were refunded. |
Credits
5,000 credits per training. Refunded if training fails.
Example
bash
curl -X POST "https://api.cinara.ai/v1/singing-voices/5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05/train" \
-H "Authorization: Bearer $CINARA_API_KEY"Get a training job
GET /v1/singing-trainings/{id}
Checks a training job, moves it forward, and returns it. Poll until status isn't running.
output.phase goes prepare (preparing and cleaning the recordings), then train (training the singing voice), then done. When it succeeds, the singing voice's status becomes ready.
Response
200 OK
json
{
"generation": {
"id": "f0a4c8e2-6b1d-4e7a-9c3f-2d5b8e1a6c90",
"module": "singing_training",
"status": "running",
"input": {
"voiceId": "5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05",
"voiceName": "Priya narrator",
"singingVoiceId": "3c7e1a9d-5b2f-4d8e-a0c6-9e4b1f7d2a58",
"seconds": 114,
"recordings": 2
},
"output": { "phase": "train", "step": 2, "stage": "processing", "stageLabel": "Training the singing voice" },
"credits": 5000,
"error": null,
"created_at": "2026-09-15T14:10:03.119254+00:00",
"completed_at": null
}
}A finished job has output: { "phase": "done", "step": 2, "stageLabel": "Ready" }.
A failed job has an error such as A recording couldn't be prepared for training., Training took too long and was stopped. or Making the singing voice failed. The singing voice's status becomes failed with the same error.
Errors
| Status | error | Message |
|---|---|---|
| 404 | not_found | Not found |
Credits
Free to read.
Polling example
bash
while :; do
RES=$(curl -s "https://api.cinara.ai/v1/singing-trainings/$ID" -H "Authorization: Bearer $CINARA_API_KEY")
[ "$(echo "$RES" | jq -r .generation.status)" != "running" ] && break
sleep 30
done
echo "$RES" | jq '{status: .generation.status, error: .generation.error}'Sing a song in a voice
POST /v1/singing
Sings a song, or converts a vocal, in a ready singing voice.
Needs a verified phone.
The source is either a finished generation in this workspace (for example a music song) or an uploaded audio file.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
voiceId | string | yes | The cloned voice's UUID. Its singing voice must be ready. |
generationId | string | one of | A succeeded generation with audio. Its file named song is used, otherwise its first audio file. |
key | string | one of | The storage key of a finished audio upload. Used when generationId isn't given. |
durationSeconds | number | no | A hint: the song's length in seconds. Cinara reads the real length from the file and uses this only when it can't. |
fileName | string | no | For uploads. Up to 200 characters. Defaults to the upload's file name. |
pitch | string | no | as-sung (default), octave-lower or octave-higher. Moves only the vocal by an octave; the music is untouched. |
vocalsOnly | boolean | no | true when the source is a vocal on its own. The result is a dry vocal. Default false. |
autotune | boolean | no | Correct the pitch to the nearest note with Cinara Tune. Needs vocalsOnly: true. Default false. |
Songs can be up to 6 minutes.
Response
201 Created
json
{
"generation": {
"id": "9b5d1f3a-2e8c-4a7b-b4d6-0c3e7a9f1b62",
"module": "singing",
"status": "running",
"input": {
"voiceId": "5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05",
"voiceName": "Priya narrator",
"singingVoiceId": "3c7e1a9d-5b2f-4d8e-a0c6-9e4b1f7d2a58",
"title": "Paper Boats – Priya narrator",
"fileName": "Paper Boats.mp3",
"source": "generation",
"generationId": "0b8f6d2e-5c1a-4e7b-9f3d-2a6c8e1b4d70",
"contentType": "audio/mpeg",
"durationSeconds": 88.4,
"durationMeasured": true,
"pitch": "as-sung",
"vocalsOnly": false,
"autotune": false
},
"output": { "stage": "queued", "step": 1, "stageLabel": "Singing in Priya narrator's voice" },
"credits": 1768,
"error": null,
"created_at": "2026-09-15T15:30:44.908117+00:00",
"completed_at": null
},
"credits": 37888
}Errors
| Status | error | Message |
|---|---|---|
| 503 | unavailable | Singing voices aren't available yet. |
| 404 | not_found | Voice not found |
| 400 | invalid_request | Priya narrator doesn't have a singing voice yet. |
| 400 | invalid_request | Choose how to sing it: as sung, an octave lower or an octave higher |
| 400 | invalid_request | Pitch correction works on a vocal on its own. Upload just the vocal to use it. |
| 404 | not_found | That song isn't available. Choose a finished song. |
| 400 | invalid_request | Choose a song or upload one |
| 404 | not_found | That upload isn't finished. Upload it again. |
| 400 | invalid_request | Upload an audio file |
| 400 | invalid_request | Songs can be up to 6 minutes |
| 402 | insufficient_credits | This needs 1,768 credits |
| 403 | phone_unverified | Verify your phone number to start creating. |
| 502 | generation_failed | Singing couldn't start. Your credits were refunded. |
Credits
max(1, ceil(seconds × 20)), where seconds is the song's length: stored with a Cinara song, or read from an uploaded file. A song over 6 minutes is refused before anything is charged.
When an upload's length can't be read (for example WebM, OGG or FLAC), a durationSeconds over 6 minutes is refused, a reserve is held on the longer of durationSeconds and an estimate from the file size, and the charge is settled to the length of the finished song. input.durationMeasured is false then.
Refunded if singing fails.
Example
bash
curl -X POST https://api.cinara.ai/v1/singing \
-H "Authorization: Bearer $CINARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"voiceId": "5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05", "generationId": "0b8f6d2e-5c1a-4e7b-9f3d-2a6c8e1b4d70"}'Get a song job
GET /v1/singing/{id}
Checks a singing job, moves it forward, and returns it with its file once finished.
With autotune, the job has two steps: singing, then Tuning the vocal.
Response
200 OK
json
{
"generation": {
"id": "9b5d1f3a-2e8c-4a7b-b4d6-0c3e7a9f1b62",
"module": "singing",
"status": "succeeded",
"input": {
"voiceId": "5a0e3c7b-91d2-4f68-b3a4-6c2e8d1f9b05",
"voiceName": "Priya narrator",
"singingVoiceId": "3c7e1a9d-5b2f-4d8e-a0c6-9e4b1f7d2a58",
"title": "Paper Boats – Priya narrator",
"fileName": "Paper Boats.mp3",
"source": "generation",
"generationId": "0b8f6d2e-5c1a-4e7b-9f3d-2a6c8e1b4d70",
"contentType": "audio/mpeg",
"durationSeconds": 88.4,
"durationMeasured": true,
"pitch": "as-sung",
"vocalsOnly": false,
"autotune": false
},
"output": { "step": 1, "stageLabel": "Done" },
"credits": 1768,
"error": null,
"created_at": "2026-09-15T15:30:44.908117+00:00",
"completed_at": "2026-09-15T15:33:12.447903+00:00"
},
"files": [
{
"name": "song",
"label": "Song in your voice",
"contentType": "audio/mpeg",
"size": 3538944,
"url": "https://api.cinara.ai/media/1789519992/Hq2mX…/workspaces/…/singing/9b5d1f3a-2e8c-4a7b-b4d6-0c3e7a9f1b62/song.mp3"
}
],
"credits": 37888
}The file is named song ("Song in your voice"), or vocal ("Vocal in your voice") when vocalsOnly is true. files is empty until the job succeeds.
Failures: Singing took too long and was stopped. or Singing failed.
Errors
| Status | error | Message |
|---|---|---|
| 404 | not_found | Not found |
Credits
Free to read.
Polling example
bash
while :; do
RES=$(curl -s "https://api.cinara.ai/v1/singing/$ID" -H "Authorization: Bearer $CINARA_API_KEY")
[ "$(echo "$RES" | jq -r .generation.status)" != "running" ] && break
sleep 10
done
echo "$RES" | jq '.files'Download a song file
GET /v1/singing/{id}/files/{name}
Downloads the finished file (song or vocal) as an attachment.
Response
200 OK with the file, Content-Type, Content-Length and Content-Disposition: attachment; filename="<file name>-<name>.mp3".
Errors
| Status | error | Message |
|---|---|---|
| 404 | not_found | File not found |
Credits
Free.
Example
bash
curl -L "https://api.cinara.ai/v1/singing/9b5d1f3a-2e8c-4a7b-b4d6-0c3e7a9f1b62/files/song" \
-H "Authorization: Bearer $CINARA_API_KEY" -o song-in-my-voice.mp3