ListenHubOpenAPI
API Reference

Voice Cloning

Upload reference audio to create a reusable private voice, confirm it, and use its speaker ID with the speech and TTS endpoints.

The Voice Cloning API turns a short recording into a reusable private voice. Upload reference audio, poll until cloning finishes, confirm the result, and you get a speakerId that works on /v1/speech, /v1/tts and /v1/audio/speech like any other voice. Cloned voices belong to the account behind your API key and show up in GET /v1/speakers/list.

All endpoints live under https://api.marswave.ai/openapi/v1/voice-clone and authenticate with Authorization: Bearer $LISTENHUB_API_KEY.

Cloning a voice requires the consent of the person being cloned. Every create request must carry consentConfirmed=true, which is your declaration that you hold that consent — the request is rejected without it, and the declaration is stored with the task. You remain responsible for obtaining and honoring that consent.

Every response is wrapped in { "code": 0, "message": "", "data": { ... } }. A non-zero code means an error — see Error Handling. The examples below read fields from data.

Limits and Cost

ItemValue
Reference audio1–6 files, single file ≤5MB, ≤20MB total
Languageszh, en, ja
Rate limit5 create requests per minute, per user
PlanPaid plans only — a free-tier confirmation returns NEED_UPGRADE
ConfirmationsFree within your plan's per-period quota, then 300 credits each
Stored voicesCapped per plan (maxSpeakers); deleting a voice frees a slot
Unconfirmed tasksExpire after 7 days — confirm the voice to keep it

Beyond the quota, a confirmation only charges when you pass useCredits=true. Without it the request returns NEED_CREDIT and nothing is charged.

Two Ways to Clone

Two-step (default) — upload, listen to the preview, then decide:

  1. POST /v1/voice-clone/clone returns a taskId.
  2. Poll GET /v1/voice-clone/clone/{taskId} until status is completed; the response carries demoAudioUrl, a preview of the temporary voice.
  3. POST /v1/voice-clone/confirm with a name and gender turns the task into a permanent private voice and returns its speakerId.

One-shot — set autoConfirm=true (plus name and gender) on the create call. The poll that first sees cloning finish also confirms the voice and returns speakerId in that same response. No second request.

With autoConfirm=true, the polling request is what charges credits. Repeated or concurrent polls never double-charge — confirmation is guarded by an atomic lock, and a second attempt is rejected as already confirmed.

Reading the Poll Response

GET /v1/voice-clone/clone/{taskId} has three terminal shapes. Check them in this order:

OutcomeHow to detectWhat you get
Cloning failedstatus: "failed"errorCode and errorMessage
Cloned, not confirmedstatus: "completed" and no speakerIddemoAudioUrl; confirmError when auto-confirm failed
ConfirmedspeakerId presentspeakerId, ready for the speech endpoints

The middle row is the one that is easy to miss with autoConfirm=true: cloning succeeded but saving the voice did not — out of credits, quota full, or the voice limit reached. confirmError says which. The clone is still there; fix the cause and call POST /v1/voice-clone/confirm explicitly.

StatusMeaning
pendingTask created, waiting to be processed
processingCloning in progress
completedCloning finished — preview available, may not be confirmed
failedCloning failed; errorMessage explains why

Retrying

StatusWhenWhat to do
429Another confirmation for the same account is in flight, or you exceeded 5 creates per minuteWait for Retry-After (2s by default) and retry
503The confirmation dependency is temporarily unavailableWait for Retry-After (5s by default) and retry

Both are safe to retry — neither charges credits.

Create a Clone Task

POST /v1/voice-clone/clone

Sends multipart/form-data, not JSON. Repeat the audioFiles field once per file.

# Two-step
curl -X POST "https://api.marswave.ai/openapi/v1/voice-clone/clone" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -F "audioFiles=@reference.mp3" \
  -F "language=en" \
  -F "consentConfirmed=true"

# One-shot: clone and confirm in the same flow
curl -X POST "https://api.marswave.ai/openapi/v1/voice-clone/clone" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -F "audioFiles=@part-1.mp3" \
  -F "audioFiles=@part-2.mp3" \
  -F "language=ja" \
  -F "consentConfirmed=true" \
  -F "autoConfirm=true" \
  -F "name=My API Voice" \
  -F "gender=female" \
  -F "useCredits=true"
import { readFile } from 'node:fs/promises'

const form = new FormData()
form.append('audioFiles', new Blob([await readFile('reference.mp3')]), 'reference.mp3')
form.append('language', 'en')
form.append('consentConfirmed', 'true')

const response = await fetch('https://api.marswave.ai/openapi/v1/voice-clone/clone', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}` },
  body: form,
})
const { data } = await response.json()
console.log('Task:', data.taskId)
import os
import requests

with open('reference.mp3', 'rb') as audio:
    response = requests.post(
        'https://api.marswave.ai/openapi/v1/voice-clone/clone',
        headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
        files=[('audioFiles', ('reference.mp3', audio, 'audio/mpeg'))],
        data={'language': 'en', 'consentConfirmed': 'true'},
    )

data = response.json()['data']
print('Task:', data['taskId'])

Request parameters:

FieldTypeRequiredDescription
audioFilesfileYes1–6 reference audio files. Repeat the field for multiple files
languagestringYeszh, en, or ja
consentConfirmedbooleanYesMust be true — your declaration that you hold the cloned person's consent
modestringNoupload (default and only accepted value)
autoConfirmbooleanNoConfirm the voice inside the poll that finds cloning finished. Defaults to false
namestringWith autoConfirmVoice name, up to 50 characters
genderstringWith autoConfirmmale, female, or other
useCreditsbooleanNoAuthorizes the 300-credit charge once your quota is used up. Defaults to false

Returns:

{
  "code": 0,
  "message": "",
  "data": {
    "taskId": "6915bde9cca4d3c8ecb3eaf5",
    "status": "pending"
  }
}

Poll a Clone Task

GET /v1/voice-clone/clone/{taskId}

curl -X GET "https://api.marswave.ai/openapi/v1/voice-clone/clone/{taskId}" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY"
const response = await fetch(
  `https://api.marswave.ai/openapi/v1/voice-clone/clone/${taskId}`,
  { headers: { Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}` } },
)
const { data } = await response.json()

if (data.status === 'failed') throw new Error(data.errorMessage)
if (data.speakerId) console.log('Ready to speak with:', data.speakerId)
else if (data.confirmError) console.warn('Cloned but not saved:', data.confirmError)
else if (data.demoAudioUrl) console.log('Preview:', data.demoAudioUrl)
import os
import requests

response = requests.get(
    f'https://api.marswave.ai/openapi/v1/voice-clone/clone/{task_id}',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
)
data = response.json()['data']

if data['status'] == 'failed':
    raise RuntimeError(data['errorMessage'])
if data.get('speakerId'):
    print('Ready to speak with:', data['speakerId'])
elif data.get('confirmError'):
    print('Cloned but not saved:', data['confirmError'])
elif data.get('demoAudioUrl'):
    print('Preview:', data['demoAudioUrl'])

Cloned, waiting for confirmation:

{
  "code": 0,
  "message": "",
  "data": {
    "status": "completed",
    "demoAudioUrl": "https://assets.listenhub.ai/voice-clone/demo-6915bde9.mp3"
  }
}

Confirmed:

{
  "code": 0,
  "message": "",
  "data": {
    "status": "completed",
    "demoAudioUrl": "https://assets.listenhub.ai/voice-clone/demo-6915bde9.mp3",
    "speakerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5"
  }
}

Confirm a Clone Task

POST /v1/voice-clone/confirm

Turns a completed task into a permanent private voice. Repeating it for the same task returns ALREADY_CONFIRMED and charges nothing.

curl -X POST "https://api.marswave.ai/openapi/v1/voice-clone/confirm" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "taskId": "6915bde9cca4d3c8ecb3eaf5",
    "name": "My API Voice",
    "gender": "female",
    "useCredits": true
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/voice-clone/confirm', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ taskId, name: 'My API Voice', gender: 'female', useCredits: true }),
})
const { data } = await response.json()
console.log('Speaker:', data.speakerId)
FieldTypeRequiredDescription
taskIdstringYesA completed clone task
namestringYesVoice name, up to 50 characters
genderstringYesmale, female, or other
useCreditsbooleanNoAuthorizes the 300-credit charge beyond your quota. Defaults to false

Returns:

{
  "code": 0,
  "message": "",
  "data": { "speakerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5" }
}

Speak with a Cloned Voice

Pass the speakerId wherever a voice is expected:

curl -X POST "https://api.marswave.ai/openapi/v1/speech" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scripts": [
      {
        "content": "This sentence is spoken by my own cloned voice.",
        "speakerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5"
      }
    ]
  }'

List Private Voices

GET /v1/voice-clone/speakers

curl -X GET "https://api.marswave.ai/openapi/v1/voice-clone/speakers" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY"
{
  "code": 0,
  "message": "",
  "data": {
    "speakers": [
      {
        "id": "6915c0a2cca4d3c8ecb3eb01",
        "name": "My API Voice",
        "speakerInnerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5",
        "language": "en",
        "gender": "female",
        "demoAudioUrl": "https://assets.listenhub.ai/voice-clone/demo-6915bde9.mp3",
        "createdAt": "2026-07-30T09:10:11.000Z"
      }
    ],
    "quota": 2,
    "isLimitReached": false,
    "maxSpeakers": 2,
    "remainingConfirmations": 1
  }
}
FieldDescription
speakers[].speakerInnerIdThe ID to pass to the speech and TTS endpoints
quotaConfirmations included per subscription period
remainingConfirmationsConfirmations left in the current period
maxSpeakersHow many private voices your plan may keep at once
isLimitReachedtrue once this period's confirmations are used up

Get, Rename, or Delete a Voice

MethodPathDescription
GET/v1/voice-clone/speakers/{speakerId}One private voice
PUT/v1/voice-clone/speakers/{speakerId}Update name and/or gender (send at least one)
DELETE/v1/voice-clone/speakers/{speakerId}Delete the voice
# Rename
curl -X PUT "https://api.marswave.ai/openapi/v1/voice-clone/speakers/{speakerId}" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Narrator (EN)" }'

# Delete — frees one slot against maxSpeakers
curl -X DELETE "https://api.marswave.ai/openapi/v1/voice-clone/speakers/{speakerId}" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY"

GET and PUT return the voice:

{
  "code": 0,
  "message": "",
  "data": {
    "id": "6915c0a2cca4d3c8ecb3eb01",
    "speakerInnerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5",
    "name": "Narrator (EN)",
    "language": "en",
    "gender": "female",
    "demoAudioUrl": "https://assets.listenhub.ai/voice-clone/demo-6915bde9.mp3",
    "createdAt": "2026-07-30T09:10:11.000Z",
    "updatedAt": "2026-07-30T10:02:44.000Z"
  }
}

DELETE returns { "speakerId": "..." }. Deleting frees a slot; the confirmations already spent this period are not returned.

Errors

ErrorMeaning
NEED_UPGRADEVoice cloning requires a paid plan
NEED_CREDITQuota used up and useCredits was not set — nothing was charged
SPEAKER_LIMIT_REACHEDYou already hold the maximum number of private voices; delete one first
ALREADY_CONFIRMEDThis task was already confirmed; no second charge
AUDIO_DURATION_INVALIDThe reference audio is too short or too long
NO_VALID_SPEECHNo speech detected in the reference audio
TASK_FAILEDCloning failed; errorMessage carries the detail

See Error Handling for the full error envelope.

On this page