Voice API
Native Voice routes plus OpenAI-compatible speech and transcription. Voice credits packs top up any key.
On this page · 4 sections
All routes authenticate with Authorization: Bearer $SCALEMAX_API_KEY. Voice spends Voice credits on the key you already use. API-credit dollar balances are not used. Auto-only keys are refused at the scope gate. Match the billing surface: a token key uses https://api.scalemax.pro/token/v1; a credit key uses https://api.scalemax.pro/v1. Do not infer the URL from the key name. Errors use the OpenAI envelope plus x-request-id. Native generation is async (202 + task). OpenAI-compatible speech and transcription wait and return the finished body.
Overview and pricing: Voice.
Task object
Every native create/list/get/retry response uses this customer-visible shape, except POST /v1/voice/voices/cloned, which returns a voice.clone object (with task_id). File URLs are ScaleMax paths; they require the same key. Failed tasks report usage.credits = 0 and usage.units = 0. usage.credits is the Voice-credit debit; usage.units is the same number. usage.cost_usd is always "0.0000"when present — Voice never charges API-credit dollars.
{
"id": "<uuid>",
"object": "voice.task",
"kind": "tts",
"engine": "elevenlabs",
"model": "eleven_multilingual_v2",
"voice": "<voice_id>",
"status": "completed",
"progress": 100,
"title": "Hello world",
"group_id": null,
"group_index": null,
"input": {
"text": "…",
"language": "en",
"voice_settings": {},
"export_transcript": false
},
"result": {
"audio_url": "https://api.scalemax.pro/v1/voice/files/<id>/audio",
"srt_url": null,
"source_url": null,
"characters_used": 12,
"duration_seconds": null,
"transcript": null
},
"usage": { "credits": 12, "units": 12, "cost_usd": "0.0000" },
"error": null,
"created_at": "…",
"updated_at": "…"
}status is one of pending, processing, completed, failed, cancelled. kind is tts, dialogue, voice_changer, dubbing, stt, or clone. engine is elevenlabs, minimax, or capcut. List responses for stt omit the full result.transcript (it can be megabytes) and send result.transcript_preview instead: { text, words, language, speakers, duration_seconds }. GET /v1/voice/tasks/{id} returns the full transcript.
Native routes
Prefix /v1/voice. Base URL https://api.scalemax.pro/v1.
POST /v1/voice/tts
Submit text-to-speech. Returns 202 and a task. engine is inferred from model when omitted. text must fit the model max_chars.
| Field | Type | Required | Notes |
|---|---|---|---|
engine | string | no | elevenlabs | minimax | capcut. Inferred from model when omitted. |
model | string | yes | Catalogue id, e.g. eleven_multilingual_v2. |
voice | string | yes | Voice id from GET /voices, or a cloned-voice id we issued. |
text | string | yes | Plain text. Length ≤ model max_chars. |
language | string | yes | ISO code (en, vi) for ElevenLabs/CapCut; full name (English) for MiniMax. |
voice_settings | object | no | Engine-specific (stability, similarity, style, speaker boost, speed, pitch, vol). |
export_transcript | boolean | no | If true, estimate includes +15% and result may include srt_url. |
group_id | uuid | no | Split-scene / SRT batch id. |
group_index | integer | no | Order inside the group. |
title | string | no | Customer-visible name. |
curl -X POST "https://api.scalemax.pro/v1/voice/tts" \
-H "Authorization: Bearer $SCALEMAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"eleven_multilingual_v2","voice":"<voice_id>","text":"Hello","language":"en"}'@'
{"model":"eleven_multilingual_v2","voice":"<voice_id>","text":"Hello","language":"en"}
'@ | Set-Content -Path body.json
curl.exe -X POST "https://api.scalemax.pro/v1/voice/tts" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
-H "Content-Type: application/json" `
--data-binary "@body.json"POST /v1/voice/dialogue
Multi-speaker script. At least two speakers. Text lines must be Name: line. Estimate is total characters × the highest speaker multiplier.
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | yes | Script in Name: line form. |
speakers | object | yes | Map of speaker name → {engine?, model, voice, language, voice_settings?}. |
pause_between_turns | number | no | Seconds between turns, 0–3. |
title | string | no | Customer-visible name. |
curl -X POST "https://api.scalemax.pro/v1/voice/dialogue" \
-H "Authorization: Bearer $SCALEMAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Ada: Hello.\nBen: Hi there.",
"speakers": {
"Ada": {"model":"eleven_multilingual_v2","voice":"<id_a>","language":"en"},
"Ben": {"model":"eleven_multilingual_v2","voice":"<id_b>","language":"en"}
}
}'@'
{
"text": "Ada: Hello.\nBen: Hi there.",
"speakers": {
"Ada": {"model":"eleven_multilingual_v2","voice":"<id_a>","language":"en"},
"Ben": {"model":"eleven_multilingual_v2","voice":"<id_b>","language":"en"}
}
}
'@ | Set-Content -Path body.json
curl.exe -X POST "https://api.scalemax.pro/v1/voice/dialogue" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
-H "Content-Type: application/json" `
--data-binary "@body.json"POST /v1/voice/voice-changer
Speech-to-speech. Multipart. Audio ≤ 50 MB / 5 minutes. Estimate = ceil(duration/60)×1000 credits.
Temporarily unavailable. Every call currently returns 503 voice_changer_unavailable before the upload is read. Nothing is reserved or charged, and speech-to-speech models are left out of GET /v1/voice/models until it is back.
| Field | Type | Required | Notes |
|---|---|---|---|
audio | file | yes | mp3/wav/m4a/ogg/webm/aac/flac. ≤50 MB. |
voice | string | yes | Target voice id. |
model | string | no | STS catalogue models only. |
remove_background_noise | boolean | no | Noise removal flag. |
voice_settings | string (JSON) | no | JSON object as a string field. |
duration_seconds | number | yes | 1–300. Used for the reservation ceiling. |
curl -X POST "https://api.scalemax.pro/v1/voice/voice-changer" \
-H "Authorization: Bearer $SCALEMAX_API_KEY" \
-F "[email protected]" \
-F "voice=<voice_id>" \
-F "duration_seconds=12"curl.exe -X POST "https://api.scalemax.pro/v1/voice/voice-changer" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
-F "[email protected]" `
-F "voice=<voice_id>" `
-F "duration_seconds=12"POST /v1/voice/dubbing/uploads
Start a chunked dubbing upload. Returns { upload_id, chunk_size }. The upload id is ours; it expires after two hours.
curl -X POST "https://api.scalemax.pro/v1/voice/dubbing/uploads" \
-H "Authorization: Bearer $SCALEMAX_API_KEY"curl.exe -X POST "https://api.scalemax.pro/v1/voice/dubbing/uploads" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY"POST /v1/voice/dubbing/uploads/{upload_id}/chunks
Upload one chunk. Multipart. Chunk ≤ 100 MB.
| Field | Type | Required | Notes |
|---|---|---|---|
index | integer | yes | Zero-based chunk index. |
chunk | file | yes | ≤100 MB. At most 512 chunks per upload. |
POST /v1/voice/dubbing/uploads/{upload_id}/complete
Finish the upload and start dubbing. Returns 202 + task. Estimate = ceil(duration_seconds) × 50 credits (3000/min). Source language is required; there is no auto-detect.
| Field | Type | Required | Notes |
|---|---|---|---|
total_chunks | integer | yes | Must match the chunks already sent. |
filename | string | no | Original filename. |
content_type | string | no | Source media type. |
source_language | string | yes | ISO 639-1. Required; no auto. |
target_language | string | yes | ISO 639-1. |
engine | string | no | Default minimax. |
voice | string | yes | Target voice id. |
duration_seconds | number | yes | Used for the reservation ceiling. |
POST /v1/voice/transcriptions
Speech-to-text. Multipart. File ≤ 90 MB / 90 minutes. Returns 202 and a task. Poll GET /v1/voice/tasks/{id}?wait=8 until completed or failed. Estimate = ceil(duration_seconds × 0.35) credits (21 per minute). On a completed GET-by-id, result.transcript is the structured transcript. List responses carry result.transcript_preview instead of the full transcript. Retry returns 400 retry_not_supported.
| Field | Type | Required | Notes |
|---|---|---|---|
file | file | yes | Audio ≤90 MB. MP3, WAV, M4A, AAC, OGG, Opus, FLAC, WebM, AIFF. |
language | string | yes | ISO 639-1/3, 2–3 letters (en, eng). |
duration_seconds | number | yes | 1–5400. Used for the reservation ceiling. |
timestamps_granularity | string | no | none | word | character. |
diarize | boolean | no | Label speakers. |
num_speakers | integer | no | 1–32. Hint for diarization. |
diarization_threshold | number | no | 0–1.5. Only when num_speakers is omitted. |
tag_audio_events | boolean | no | Tag non-speech events such as (laughter). |
no_verbatim | boolean | no | Remove filler words / false starts. |
curl -X POST "https://api.scalemax.pro/v1/voice/transcriptions" \
-H "Authorization: Bearer $SCALEMAX_API_KEY" \
-F "[email protected]" \
-F "language=en" \
-F "duration_seconds=56" \
-F "timestamps_granularity=word" \
-F "diarize=true" \
-F "tag_audio_events=true" \
-F "no_verbatim=false"curl.exe -X POST "https://api.scalemax.pro/v1/voice/transcriptions" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
-F "[email protected]" `
-F "language=en" `
-F "duration_seconds=56" `
-F "timestamps_granularity=word" `
-F "diarize=true" `
-F "tag_audio_events=true" `
-F "no_verbatim=false"curl "https://api.scalemax.pro/v1/voice/tasks/$TASK_ID?wait=8" \
-H "Authorization: Bearer $SCALEMAX_API_KEY"curl.exe "https://api.scalemax.pro/v1/voice/tasks/${env:TASK_ID}?wait=8" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY"Completed result.transcript shape (minus upstream transcription_id):
{
"language_code": "eng",
"language_probability": 1.0,
"text": "Hello from the recording.",
"audio_duration_secs": 56.1,
"words": [
{
"text": "Hello",
"start": 0.12,
"end": 0.48,
"type": "word",
"speaker_id": "speaker_0",
"logprob": -0.12
}
]
}words[].type is word, spacing, or audio_event. List preview: result.transcript_preview = { text, words, language, speakers, duration_seconds } where text is the first ~240 characters.
GET /v1/voice/tasks
List this key's tasks only. { object: 'list', data: task[], has_more }. For kind=stt, each row carries result.transcript_preview instead of the full transcript.
| Field | Type | Required | Notes |
|---|---|---|---|
kind | string | no | Filter by task kind. |
group_id | uuid | no | Filter by split-scene / SRT group. |
page | integer | no | 1-based page. |
page_size | integer | no | Page size. |
GET /v1/voice/tasks/{id}
Fetch one task. A poll of a non-terminal task is the state machine: ScaleMax refreshes upstream (throttled to once per 2 s per task) and settles or releases on transition. A background sweeper covers abandoned tasks. Pass wait (integer seconds, 0–20, default 0) to long-poll: the request waits until the task finishes or the limit, then returns the same task body it would on a normal GET.
| Field | Type | Required | Notes |
|---|---|---|---|
wait | integer | no | Long-poll seconds, clamped 0–20. Returns early when the task reaches a terminal status. |
curl "https://api.scalemax.pro/v1/voice/tasks/$TASK_ID" \
-H "Authorization: Bearer $SCALEMAX_API_KEY"curl.exe "https://api.scalemax.pro/v1/voice/tasks/$env:TASK_ID" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY"DELETE /v1/voice/tasks/{id}
Soft-delete a terminal task and free stored audio. 409 task_not_terminal while the task is still open. Releases no money — settlement already happened.
POST /v1/voice/tasks/{id}/retry
New task row and a new reservation, reusing the stored input. The new row links retry_of. Speech-to-text returns 400 retry_not_supported.
curl -X POST "https://api.scalemax.pro/v1/voice/tasks/$TASK_ID/retry" \
-H "Authorization: Bearer $SCALEMAX_API_KEY"curl.exe -X POST "https://api.scalemax.pro/v1/voice/tasks/$env:TASK_ID/retry" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY"GET /v1/voice/files/{id}/{audio|srt|source}
Stream the result file. Auth required. Files are cached server-side for replay. Correct content-type and Content-Disposition; HTTP Range is honoured (including on a cache hit).
curl "https://api.scalemax.pro/v1/voice/files/$TASK_ID/audio" \
-H "Authorization: Bearer $SCALEMAX_API_KEY" \
-o out.mp3curl.exe "https://api.scalemax.pro/v1/voice/files/$env:TASK_ID/audio" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
-o out.mp3POST /v1/voice/estimate
Price a request without spending. Body matches /tts or { kind, duration_seconds? } for changer/dubbing/stt/clone. Returns credits, chars, max_chars, billing, units_remaining, and insufficient_units.billing is voice_units or voice_pack. There is no cost_usd on this response; if an older gateway still sends it, ignore it. A job the remaining credits cannot cover is refused on submit with HTTP 402 insufficient_voice_credits.
curl -X POST "https://api.scalemax.pro/v1/voice/estimate" \
-H "Authorization: Bearer $SCALEMAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"eleven_multilingual_v2","text":"Hello","language":"en","voice":"<voice_id>"}'@'
{"model":"eleven_multilingual_v2","text":"Hello","language":"en","voice":"<voice_id>"}
'@ | Set-Content -Path body.json
curl.exe -X POST "https://api.scalemax.pro/v1/voice/estimate" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
-H "Content-Type: application/json" `
--data-binary "@body.json"GET /v1/voice/models
Enabled catalogue rows. { object: 'list', data }. Query engine= to filter.
GET /v1/voice/languages
Languages for an engine. { object: 'list', data }. Query engine=. Cached about an hour.
GET /v1/voice/voices
List voices. Response is { object: 'list', data, has_more }. engine=elevenlabs&library=default|shared, engine=minimax&library=system, or engine=capcut&library=system. Filters: page, page_size, search, gender, age, accent, language, category, sort, use_cases, emotion. Preview URLs are rewritten to /v1/voice/voices/{engine}/{voice_id}/preview.
curl "https://api.scalemax.pro/v1/voice/voices?engine=elevenlabs&library=default" \
-H "Authorization: Bearer $SCALEMAX_API_KEY"curl.exe "https://api.scalemax.pro/v1/voice/voices?engine=elevenlabs&library=default" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY"GET /v1/voice/voices/cloned
Cloned voices this key owns. { object: 'list', data }. Ids are ours; TTS with voice=<our clone id> resolves server-side.
POST /v1/voice/voices/cloned
Start a MiniMax clone. Multipart. Returns 202 with a voice.clone object (not a task; task_id points at the 1000-credit clone task). Sample ≤ 20 MB.
| Field | Type | Required | Notes |
|---|---|---|---|
file | file | yes | Voice sample ≤20 MB. |
name | string | yes | Display name. |
language_tag | string | no | Language of the sample. |
gender | string | no | Presented gender tag. |
need_noise_reduction | boolean | no | Clean the sample first. |
preview_text | string | no | ≤200 characters. |
DELETE /v1/voice/voices/cloned/{id}
Delete a cloned voice we issued.
POST /v1/voice/voices/cloned/{id}/retry
Retry a failed clone.
OpenAI-compatible routes
POST /v1/audio/speech
Synchronous TTS: submit, poll up to 120 s, stream audio/mpeg. Creates the same Voice task as native TTS.model is optional and defaults to eleven_multilingual_v2.response_format other than mp3 is 400 — upstream audio is mp3 only.
| Field | Type | Required | Notes |
|---|---|---|---|
model | string | no | Optional. Default eleven_multilingual_v2. A TTS catalogue id, or an OpenAI id: tts-1 and tts-1-hd map to eleven_multilingual_v2, gpt-4o-mini-tts to eleven_flash_v2_5. |
input | string | yes | Text to speak. |
voice | string | yes | Voice id. |
response_format | string | no | Default mp3. Anything else → 400. |
speed | number | no | Forwarded into voice_settings when the model supports speed. |
language | string | no | Default en. |
engine | string | no | Inferred from model when omitted. |
curl -X POST "https://api.scalemax.pro/v1/audio/speech" \
-H "Authorization: Bearer $SCALEMAX_API_KEY" \
-H "Content-Type: application/json" \
-o hello.mp3 \
-d '{
"model": "eleven_multilingual_v2",
"voice": "<voice_id>",
"input": "Hello",
"language": "en"
}'@'
{
"model": "eleven_multilingual_v2",
"voice": "<voice_id>",
"input": "Hello",
"language": "en"
}
'@ | Set-Content -Path body.json
curl.exe -X POST "https://api.scalemax.pro/v1/audio/speech" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
-H "Content-Type: application/json" `
-o hello.mp3 `
--data-binary "@body.json"POST /v1/audio/transcriptions
The OpenAI speech-to-text API, so the OpenAI SDKs, Open WebUI, LibreChat and n8n work with only the base URL and key changed. Multipart upload, file ≤ 25 MB. The call waits for the transcript and returns it in the requested format. It runs the same job as /v1/voice/transcriptions, so a transcript can also be fetched later from GET /v1/voice/tasks/{id} (the id is in the x-scalemax-voice-task header).
Billing. Voice credits by audio length — about 21 per audio minute (at most 0.35 credits per second), priced on the length the transcriber measured, the same as /v1/voice/transcriptions. OpenAI clients do not send a duration, so the call first holds an upper bound worked out from the file (the exact length for WAV; otherwise the file size, up to 90 minutes). The final charge is never more than that hold, and the rest is released. A failed job is not charged. The key needs enough free Voice credits for the hold, which for a compressed file is usually several times the final charge.
Waiting. Short jobs answer normally with x-scalemax-voice-task, x-scalemax-credits (credits charged) and x-request-id. For json and verbose_json, a job still running after about 20 seconds gets a 200 straight away with a space sent every 15 seconds to keep the connection open, then the JSON (leading spaces are valid JSON). Such a response carries no x-scalemax-credits header, and a job that then fails or runs past 10 minutes is reported inside that body as an { "error": … } object. text, srt and vtt cannot be padded: after 90 seconds they return 504 upstream_timeout naming the task, and the job keeps running. Fetch it from GET /v1/voice/tasks/{id}; it is charged once.
| Field | Type | Required | Notes |
|---|---|---|---|
file | file | yes | Audio ≤ 25 MB: flac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, opus, wav, webm. Larger files → 413 too_large. |
model | string | yes | One of whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe, gpt-4o-transcribe-diarize, scribe_v1, scribe_v2. All run on Scribe v2; gpt-4o-transcribe-diarize also labels speakers. Anything else → 400 with param model. |
language | string | no | ISO 639-1 (en, fr). Default en: the transcriber needs a language, so set it for non-English audio. |
response_format | string | no | json (default) → { text }; text; srt; vtt; verbose_json → { task, language, duration, text, words?, segments? }. |
timestamp_granularities[] | string[] | no | verbose_json only. word adds words [{ word, start, end }]; segment (the default) adds segments [{ id, start, end, text, … }]. |
temperature | number | no | 0–2. Forwarded to the transcriber. |
prompt | string | no | Accepted for compatibility and ignored. |
curl -X POST "https://api.scalemax.pro/v1/audio/transcriptions" \
-H "Authorization: Bearer $SCALEMAX_API_KEY" \
-F "[email protected]" \
-F "model=whisper-1" \
-F "language=en" \
-F "response_format=srt"curl.exe -X POST "https://api.scalemax.pro/v1/audio/transcriptions" `
-H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
-F "[email protected]" `
-F "model=whisper-1" `
-F "language=en" `
-F "response_format=srt"from openai import OpenAI
client = OpenAI(
api_key="YOUR_SCALEMAX_KEY",
base_url="https://api.scalemax.pro/v1", # a token key uses https://api.scalemax.pro/token/v1
)
with open("meeting.mp3", "rb") as audio:
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=audio,
language="en",
response_format="verbose_json",
timestamp_granularities=["word", "segment"],
)
print(transcript.text)
for word in transcript.words or []:
print(word.start, word.end, word.word)Error codes
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request | Schema, missing fields, unsupported response_format, script/speaker validation, text over the model max_chars, or duration_seconds outside the kind's cap. |
| 402 | insufficient_voice_credits | Not enough Voice credits on this key. Buy a Voice pack in Voice → Credits. |
| 403 | key_disabled | Auto-only key, or Voice is not enabled for this key. |
| 404 | not_found | Unknown task, file, clone, or model id for this key. |
| 409 | task_not_terminal | DELETE of a still-open task. |
| 413 | request_too_large | JSON body or OpenAI-compatible speech payload over the request cap. |
| 413 | too_large | Multipart media over the route's byte cap (changer 50 MB, STT 90 MB, OpenAI transcription 25 MB, clone 20 MB, dubbing chunk 100 MB). |
| 429 | rate_limit_exceeded | Upstream submit semaphore full (message mentions busy; Retry-After: 2) or the key's request/concurrency cap. |
| 503 | provider_unavailable | Runtime switch voice_enabled is off, or Voice is otherwise unavailable. |
| 503 | voice_changer_unavailable | The Voice Changer is temporarily switched off. The upload is not read and nothing is charged. |
| 504 | upstream_timeout | A synchronous OpenAI-compatible call ran out of wait time. The job keeps running; the message names the task to fetch from GET /v1/voice/tasks/{id}. |