# Voice API

> Native Voice routes plus OpenAI-compatible speech and transcription. Voice credits packs top up any key.

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](https://docs.scalemax.pro/docs/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.

voice.task.json:

```json
{
  "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. |

**macOS / Linux**

tts.sh:

```bash
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"}'
```

**Windows PowerShell**

tts.ps1:

```powershell
@'
{"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"
```

PowerShell changes quotes inside a JSON argument, so the request body goes into body.json first and curl.exe sends that file. Type curl.exe, not curl: in Windows PowerShell 5.1, curl is a different command.

### 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. |

**macOS / Linux**

dialogue.sh:

```bash
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"}
    }
  }'
```

**Windows PowerShell**

dialogue.ps1:

```powershell
@'
{
  "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"
```

PowerShell changes quotes inside a JSON argument, so the request body goes into body.json first and curl.exe sends that file. Type curl.exe, not curl: in Windows PowerShell 5.1, curl is a different command.

### 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. |

**macOS / Linux**

voice-changer.sh:

```bash
curl -X POST "https://api.scalemax.pro/v1/voice/voice-changer" \
  -H "Authorization: Bearer $SCALEMAX_API_KEY" \
  -F "audio=@clip.wav" \
  -F "voice=<voice_id>" \
  -F "duration_seconds=12"
```

**Windows PowerShell**

voice-changer.ps1:

```powershell
curl.exe -X POST "https://api.scalemax.pro/v1/voice/voice-changer" `
  -H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
  -F "audio=@clip.wav" `
  -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.

**macOS / Linux**

dubbing-init.sh:

```bash
curl -X POST "https://api.scalemax.pro/v1/voice/dubbing/uploads" \
  -H "Authorization: Bearer $SCALEMAX_API_KEY"
```

**Windows PowerShell**

dubbing-init.ps1:

```powershell
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. |

**macOS / Linux**

stt.sh:

```bash
curl -X POST "https://api.scalemax.pro/v1/voice/transcriptions" \
  -H "Authorization: Bearer $SCALEMAX_API_KEY" \
  -F "file=@clip.mp3" \
  -F "language=en" \
  -F "duration_seconds=56" \
  -F "timestamps_granularity=word" \
  -F "diarize=true" \
  -F "tag_audio_events=true" \
  -F "no_verbatim=false"
```

**Windows PowerShell**

stt.ps1:

```powershell
curl.exe -X POST "https://api.scalemax.pro/v1/voice/transcriptions" `
  -H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
  -F "file=@clip.mp3" `
  -F "language=en" `
  -F "duration_seconds=56" `
  -F "timestamps_granularity=word" `
  -F "diarize=true" `
  -F "tag_audio_events=true" `
  -F "no_verbatim=false"
```

**macOS / Linux**

stt-poll.sh:

```bash
curl "https://api.scalemax.pro/v1/voice/tasks/$TASK_ID?wait=8" \
  -H "Authorization: Bearer $SCALEMAX_API_KEY"
```

**Windows PowerShell**

stt-poll.ps1:

```powershell
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`):

transcript.json:

```json
{
  "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. |

**macOS / Linux**

poll.sh:

```bash
curl "https://api.scalemax.pro/v1/voice/tasks/$TASK_ID" \
  -H "Authorization: Bearer $SCALEMAX_API_KEY"
```

**Windows PowerShell**

poll.ps1:

```powershell
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`.

**macOS / Linux**

retry.sh:

```bash
curl -X POST "https://api.scalemax.pro/v1/voice/tasks/$TASK_ID/retry" \
  -H "Authorization: Bearer $SCALEMAX_API_KEY"
```

**Windows PowerShell**

retry.ps1:

```powershell
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).

**macOS / Linux**

download.sh:

```bash
curl "https://api.scalemax.pro/v1/voice/files/$TASK_ID/audio" \
  -H "Authorization: Bearer $SCALEMAX_API_KEY" \
  -o out.mp3
```

**Windows PowerShell**

download.ps1:

```powershell
curl.exe "https://api.scalemax.pro/v1/voice/files/$env:TASK_ID/audio" `
  -H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
  -o out.mp3
```

### POST `/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`.

**macOS / Linux**

estimate.sh:

```bash
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>"}'
```

**Windows PowerShell**

estimate.ps1:

```powershell
@'
{"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"
```

PowerShell changes quotes inside a JSON argument, so the request body goes into body.json first and curl.exe sends that file. Type curl.exe, not curl: in Windows PowerShell 5.1, curl is a different command.

### 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`.

**macOS / Linux**

voices.sh:

```bash
curl "https://api.scalemax.pro/v1/voice/voices?engine=elevenlabs&library=default" \
  -H "Authorization: Bearer $SCALEMAX_API_KEY"
```

**Windows PowerShell**

voices.ps1:

```powershell
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. |

**macOS / Linux**

audio-speech.sh:

```bash
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"
  }'
```

**Windows PowerShell**

audio-speech.ps1:

```powershell
@'
{
  "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"
```

PowerShell changes quotes inside a JSON argument, so the request body goes into body.json first and curl.exe sends that file. Type curl.exe, not curl: in Windows PowerShell 5.1, curl is a different command.

### 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. |

**macOS / Linux**

audio-transcriptions.sh:

```bash
curl -X POST "https://api.scalemax.pro/v1/audio/transcriptions" \
  -H "Authorization: Bearer $SCALEMAX_API_KEY" \
  -F "file=@meeting.mp3" \
  -F "model=whisper-1" \
  -F "language=en" \
  -F "response_format=srt"
```

**Windows PowerShell**

audio-transcriptions.ps1:

```powershell
curl.exe -X POST "https://api.scalemax.pro/v1/audio/transcriptions" `
  -H "Authorization: Bearer $env:SCALEMAX_API_KEY" `
  -F "file=@meeting.mp3" `
  -F "model=whisper-1" `
  -F "language=en" `
  -F "response_format=srt"
```

transcriptions.py:

```python
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}`. |
