Error Codes
VoiceMaker returns standard HTTP status codes along with a consistent JSON error
body. Every error carries a stable, machine-readable errorCode — branch on
it rather than on the error/detail text, which may be reworded — and a
requestId you can quote when contacting support.
Error response format
{
"error": "Invalid request",
"detail": "Request body did not match the expected schema.",
"errorCode": "invalid_payload",
"requestId": "8f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"issues": [
{ "field": "voice_id", "code": "too_small", "message": "Required" }
]
}| Field | Always present | Description |
|---|---|---|
error | yes | Short human-readable title. |
detail | yes | What went wrong and, where possible, what to do. |
errorCode | yes | Stable snake_case code from the catalog below. Never renamed once shipped; new codes may be added. |
requestId | yes | Request id (also sent as the X-Request-Id header). |
issues | validation errors only | The offending fields as { field, code, message }. |
Rate-limit responses (429) and cold-start responses (503) also send a
Retry-After header with the number of seconds to wait.
HTTP status codes
| Status | Meaning | When it happens |
|---|---|---|
200 | OK | Synchronous request succeeded. |
201 | Created | Stream token minted. |
202 | Accepted | Async job queued (transcription, animation, async TTS). |
400 | Bad Request | Invalid JSON, schema violation, unsupported language, missing voice_id. |
401 | Unauthorized | Missing, invalid, or expired API key or stream token. |
402 | Payment Required | Not enough purchased credits for the request. |
403 | Forbidden | Key revoked, project disabled, plan not API-enabled, missing scope, or a stream token of the wrong kind. |
404 | Not Found | Job does not exist for your project, or no such endpoint. |
413 | Payload Too Large | Audio exceeds your plan's (or the endpoint's) file-size limit. |
415 | Unsupported Media Type | Wrong Content-Type, or a media file that is not audio/video. |
422 | Unprocessable Entity | Valid request but rejected values (unknown voice_id, unreachable audio URL). |
426 | Upgrade Required | A streaming endpoint was called without a WebSocket upgrade. |
429 | Too Many Requests | Rate limit, queue, concurrency or concurrent-stream limit reached. |
500 | Internal Server Error | Unexpected processing error — safe to retry. |
503 | Service Unavailable | Engine warming up or a dependency temporarily unavailable — retry after Retry-After. |
Error-code catalog
The SDK exception column is the class the official SDKs raise for the code
(same names in JavaScript and Python; every SDK error also exposes the code
as err.code / e.code). Subclasses are listed where they exist — for example
UnknownVoiceError is also a ValidationError, and ServiceWarmingError is
also a ServiceUnavailableError.
Authentication & authorisation
| Code | Status | Meaning | What to do | SDK exception |
|---|---|---|---|---|
missing_api_key | 401 | No API key was sent with the request. | Send the key in the Authorization: Bearer header. | AuthenticationError |
invalid_api_key | 401 | The API key is malformed, unknown, or belongs to the other environment (test vs live). | Check the key for typos — keys look like vmk_live_… or vmk_test_…. Create one in the dashboard under Developer → API keys. | AuthenticationError |
api_key_expired | 401 | The API key has passed its expiry date. | Create a new key in the dashboard and send it in the Authorization: Bearer header. | AuthenticationError |
api_key_revoked | 403 | The API key was revoked. | Create a new key in the dashboard and send it in the Authorization: Bearer header. | ForbiddenError |
project_disabled | 403 | The developer project that owns this key is disabled. | Re-enable the project in the dashboard, or use a key from an active project. | ForbiddenError |
api_access_not_enabled | 403 | The account's plan does not include API access and it has no purchased credits. | Upgrade to Growth, Pro or Enterprise, or buy credits; GET /usage shows the current balance. | ForbiddenError |
insufficient_scope | 403 | The key lacks the scope this call needs (transcriptions:read or transcriptions:write). | Create a key with the required scope in the dashboard. | ForbiddenError |
rate_limited | 429 | The key exceeded its per-minute or daily request limit. | Wait the number of seconds given in the Retry-After header before retrying, and spread requests over time. | RateLimitError |
invalid_stream_token | 401 | The stream token is invalid, expired, or was already used. | Mint a fresh token with POST /auth/stream-token immediately before connecting — tokens are single-use and short-lived. | StreamTokenError |
stream_token_kind_mismatch | 403 | The stream token was minted for the other kind of stream. | Mint with POST /auth/stream-token with type: "asr" for /transcribe/stream and type: "tts" for /tts/stream. | StreamTokenError |
Request validation
| Code | Status | Meaning | What to do | SDK exception |
|---|---|---|---|---|
invalid_json | 400 | The request body was not valid JSON. | The SDK always sends JSON — if you see this, report it with the requestId. | ValidationError |
invalid_json_shape | 400 | The request body must be a JSON object. | The SDK always sends an object — if you see this, report it with the requestId. | ValidationError |
invalid_payload | 400 | One or more request fields failed validation. | Inspect issues for the offending fields. | ValidationError |
invalid_query | 400 | One or more query parameters failed validation. | Inspect issues for the offending parameters. | ValidationError |
invalid_cursor | 400 | The pagination cursor could not be decoded. | Pass the next_cursor value from the previous GET /transcriptions page unchanged. | ValidationError |
invalid_date_range | 400 | start_date must be before end_date. | Check the dates passed to GET /usage/breakdown. | ValidationError |
invalid_multipart_body | 400 | The multipart upload could not be parsed. | Retry POST /transcribe (multipart); if it persists, report it with the requestId. | ValidationError |
invalid_multipart_payload | 400 | The multipart form fields failed validation. | Inspect issues for the offending fields. | ValidationError |
missing_audio | 400 | The upload contained no audio file. | Pass the path of an existing audio or video file to POST /transcribe (multipart). | ValidationError |
unsupported_content_type | 415 | The request Content-Type is not one the endpoint accepts. | Use POST /transcribe for URLs and POST /transcribe (multipart) for local files. | UnsupportedMediaTypeError |
voice_id_required | 400 | Every text-to-speech request needs a voice. | Pick an id from GET /tts/voices and pass it as voice_id to POST /tts/generate or voiceId to /tts/stream. | ValidationError |
unsupported_language | 400 | The language is not in the platform's language catalog. | Call GET /tts/languages for the codes currently enabled. | UnsupportedLanguageError |
streaming_unsupported_language | 400 | The language is served by batch synthesis only. | Use POST /tts/generate for this language, or pick one with streaming_supported: true from GET /tts/languages. | UnsupportedLanguageError |
Transcription input
| Code | Status | Meaning | What to do | SDK exception |
|---|---|---|---|---|
empty_file | 400 | The uploaded file is empty. | Check the file path passed to POST /transcribe (multipart). | ValidationError |
invalid_file_url | 400 | The audio URL is not a valid URL. | Pass an absolute http(s) URL to POST /transcribe. | ValidationError |
unsupported_file_url_protocol | 400 | Only http and https URLs are accepted. | Host the file on an http(s) URL and pass that to POST /transcribe. | ValidationError |
file_url_credentials_not_allowed | 400 | The audio URL must not embed a username or password. | Use a public or pre-signed URL instead. | ValidationError |
file_url_unreachable | 422 | The server could not reach the audio URL. | Make sure the URL is reachable from the public internet (not localhost or a private network). | ValidationError |
file_url_fetch_failed | 422 | The audio URL did not return a usable file. | Verify the URL returns the raw media file with a 2xx response. | ValidationError |
empty_remote_file | 422 | The audio URL returned an empty file. | Verify the URL serves the media file. | ValidationError |
file_too_large | 413 | The file exceeds the endpoint's hard upload limit. | Trim or compress the audio before submitting. | FileSizeLimitError |
file_size_limit_exceeded | 413 | The file exceeds your plan's file-size limit. | Compress the audio, split it, or upgrade your plan. | FileSizeLimitError |
unsupported_media | 415 | The file could not be read as audio or video. | Submit a standard format such as mp3, wav, m4a, mp4 or webm. | UnsupportedMediaTypeError |
invalid_media_metadata | 415 | The media file could not be inspected safely. | Re-encode the file into a standard format such as mp3, wav, m4a, mp4 or webm. | UnsupportedMediaTypeError |
missing_audio_stream | 415 | The file has no audio track. | Submit a file that contains audio. | UnsupportedMediaTypeError |
unsupported_media_type | 415 | The media format is not supported. | Submit a standard format such as mp3, wav, m4a, mp4 or webm. | UnsupportedMediaTypeError |
file_inspection_unavailable | 503 | Media inspection is temporarily unavailable on the server. | Retry shortly. | ServiceUnavailableError |
file_storage_unavailable | 503 | Media storage is temporarily unavailable on the server. | Retry shortly. | ServiceUnavailableError |
ingestion_failed | 500 | The server could not process the submitted audio. | Retry; if it persists, report it with the requestId. | ServerError |
Voices
| Code | Status | Meaning | What to do | SDK exception |
|---|---|---|---|---|
unknown_voice_id | 422 | The voice id does not match an enabled preset or one of your custom voices. | Use GET /tts/voices to see the available voices. | UnknownVoiceError |
voice_not_ready | 422 | The custom voice is still processing, failed, or is missing its reference audio. | Wait for the voice to finish processing in the dashboard (GET /tts/voices only lists ready clones), or pick another voice. | VoiceNotReadyError |
Credits & limits
| Code | Status | Meaning | What to do | SDK exception |
|---|---|---|---|---|
insufficient_credits | 402 | Not enough purchased credits for this request — free credits cannot be spent through the API. | Buy credits in the dashboard; GET /usage shows the current balance. | InsufficientCreditsError |
queue_limit_reached | 429 | Too many of your jobs are already waiting in the queue. | Wait for queued jobs to start (poll the job endpoints) before submitting more. | QueueLimitError |
concurrency_limit_reached | 429 | Your plan's maximum number of processing jobs is already running. | Retry after the number of seconds in Retry-After, or call POST /tts/generate with mode: "async" and poll GET /tts/{id}. | ConcurrencyLimitError |
stream_concurrency_limit | 429 | Your plan's concurrent-stream cap is already in use. | Close an active stream (close an active stream) and reconnect. | StreamConcurrencyError |
stream_capacity | 503 | All streaming capacity is in use platform-wide. | Retry shortly. | ServiceUnavailableError |
Availability
| Code | Status | Meaning | What to do | SDK exception |
|---|---|---|---|---|
tts_warming | 503 | The speech engine is cold-starting. | Retry in about the number of seconds in Retry-After, or call POST /tts/generate with mode: "async" and poll GET /tts/{id}. | ServiceWarmingError |
streaming_unavailable | 503 | Real-time streaming is not configured on this deployment. | Use the batch methods (POST /transcribe, POST /tts/generate) or contact support. | ServiceUnavailableError |
token_service_unavailable | 503 | The stream-token service is temporarily unavailable. | Retry POST /auth/stream-token shortly. | ServiceUnavailableError |
websocket_required | 426 | The endpoint only accepts WebSocket connections. | Use /transcribe/stream / /tts/stream, which connect over WebSocket. | VoiceMakerAPIError |
Processing
| Code | Status | Meaning | What to do | SDK exception |
|---|---|---|---|---|
synthesis_failed | 500 | The speech engine could not generate audio. | Retry; if it persists, report it with the requestId. | ServerError |
audio_conversion_failed | 500 | The generated audio could not be converted to the requested output_format. | Retry, or request output_format: "wav". | ServerError |
audio_upload_failed | 500 | The generated audio could not be stored. | Retry; if it persists, report it with the requestId. | ServerError |
explain_failed | 500 | The explain service could not process the text. | Retry; if it persists, report it with the requestId. | ServerError |
stream_setup_failed | 500 | The server could not initialise the streaming session. | Retry; if it persists, report it with the requestId. | ServerError |
internal_error | 500 | An unexpected error occurred on the server. | Retry; if it persists, report it with the requestId. | ServerError |
not_found | 404 | No job with that id exists for your project. | Check the id — jobs are only visible to the project whose key created them. | NotFoundError |
endpoint_not_found | 404 | No API endpoint matches the request. | Check baseUrl in the base URL — it must be the API origin with no path. | NotFoundError |
Any code this page does not list yet is still delivered in the same body
shape; the SDKs map it by HTTP status and keep the raw code on err.code.
Streaming errors
The WebSocket endpoints (/transcribe/stream, /tts/stream) answer a refused
connection with a plain HTTP error from the catalog above before the
upgrade — 401, 402 (insufficient_credits for the reserve block), 403,
400/422 (voice_id_required, unknown_voice_id, voice_not_ready,
unsupported_language, streaming_unsupported_language), 429
(stream_concurrency_limit), or 503 (tts_warming, stream_capacity,
streaming_unavailable). Browsers hide that status from JavaScript, so the
JavaScript SDK emits an error event with code upgrade_rejected and the
likely causes; the Python SDK reads the body from the rejected handshake and
raises the same typed exception a REST call would.
Once a session is open, errors arrive as { "type": "error", "code", "message" }
frames. The session may continue afterwards or be closing, as noted:
| Code | Meaning | What to do |
|---|---|---|
invalid_message | The server received a control frame it could not parse or does not accept in the current state. | Only use the connection's methods (sendAudio()/sendText(), finalize(), close()); stop sending after close(). |
text_too_large | A single sendText() call exceeded the per-frame character limit. | Split the text with chunkText() and call sendText() per chunk. |
session_char_limit | The session's total character budget is exhausted; audio already submitted is still delivered. | Open a new /tts/stream session for the remaining text. |
insufficient_credits | Purchased credits ran out mid-session; the server delivers what was already paid for, then closes (code 4002). | Buy credits in the dashboard; GET /usage shows the current balance. |
idle_timeout | No audio/text or KeepAlive was received for the idle window; the session closes (code 4008). | Keep the default keepAliveMs, or send data at least every 30 seconds. |
upstream_unavailable | The speech engine behind the stream failed or is at capacity; the session closes (code 1011 or 1013). | Reconnect after a short delay. |
internal_error | An unexpected server-side error ended the session (code 1011). | Reconnect; if it persists, report it with the session_id. |
Close codes
| Code | Meaning |
|---|---|
1000 | Session ended normally. |
1006 | Connection dropped without a close frame — the upgrade was rejected, the network failed, or the server went away. |
1011 | The speech engine or the server hit an internal error. |
1013 | The speech engine is at capacity; reconnect after a short delay. |
4002 | Purchased credits were exhausted. |
4008 | Idle timeout — no data or KeepAlive for 30 seconds. |
4009 | The session reached its maximum duration or character budget. |
SDK-side errors
The SDKs also raise errors of their own before or instead of a request. They
share the code field with API errors:
| Code | Raised by the SDK when | Exception |
|---|---|---|
api_key_required | This call needs an API key and the client was created without one. | ConfigurationError |
credential_required | Streaming needs an API key or an ephemeral stream token. | ConfigurationError |
websocket_unavailable | This runtime has no global WebSocket implementation. | ConfigurationError |
invalid_argument | An argument failed client-side validation before any request was made. | InvalidArgumentError |
stream_not_open | Data was sent on a stream that is not open. | StreamClosedError |
network_error | The request never reached the API (DNS, connection refused, TLS, CORS, offline). | NetworkError |
request_timeout | The API did not answer within the client's timeout. | RequestTimeoutError |
poll_timeout | The job did not reach a terminal status before the polling deadline. | PollTimeoutError |
Common error scenarios
Voice required (400)
Every text-to-speech request needs a voice_id (batch POST /dev/v1/tts/generate
and the /dev/v1/tts/stream WebSocket alike). A missing or blank voice_id is
refused before anything else is validated:
{
"error": "Voice required",
"detail": "Specify a voice_id.",
"errorCode": "voice_id_required",
"requestId": "..."
}Pick an ID from GET /dev/v1/tts/voices.
Unknown voice (422)
{
"error": "Unknown voice",
"detail": "voice_id 'foo' does not match an enabled voice preset or one of your custom voices. Use GET /dev/v1/tts/voices for the list of available voices.",
"errorCode": "unknown_voice_id",
"requestId": "..."
}Voice not ready (422)
The voice_id matched one of your custom cloned voices, but the voice cannot
synthesize yet (still processing, failed, or missing its reference assets).
{
"error": "Voice not ready",
"detail": "Custom voice 'My Studio Clone' is not ready for synthesis (status: PROCESSING)",
"errorCode": "voice_not_ready",
"requestId": "..."
}Insufficient credits (402)
{
"error": "Insufficient credits",
"detail": "Not enough purchased credits. Required: 1,500 credits.",
"errorCode": "insufficient_credits",
"requestId": "..."
}File too large (413)
{
"error": "File too large",
"detail": "Audio file exceeds the 25MB limit for your current plan.",
"errorCode": "file_size_limit_exceeded",
"requestId": "..."
}Engine warming up (503)
Synchronous TTS on a cold engine answers 503 with Retry-After: 60. Retry
after that, or submit with mode: "async" and poll GET /dev/v1/tts/{id}.
{
"error": "TTS engine warming up",
"detail": "The TTS engine is starting up (cold start). Retry in ~1 minute, or submit with mode:\"async\" to queue the job and poll GET /dev/v1/tts/:id.",
"errorCode": "tts_warming",
"requestId": "..."
}Rate limited (429)
{
"error": "Rate limit exceeded",
"detail": "This developer API key has exceeded its current rate limit",
"errorCode": "rate_limited",
"requestId": "..."
}Inspect the Retry-After header to learn how long to wait before retrying.
Handling errors in the SDKs
Both SDKs raise typed exceptions whose message names the SDK method that failed, rewrites endpoint references into SDK calls, and ends with the code and request id:
tts.generate(): Unknown voice — voice_id 'foo' does not match an enabled voice
preset or one of your custom voices. Use tts.listVoices() for the list of
available voices. [unknown_voice_id · request 8f1c2d3e]import { UnknownVoiceError, ServiceWarmingError, VoiceMakerAPIError } from "myvoicemaker";
try {
await client.tts.generate({ text, voice_id });
} catch (err) {
if (err instanceof UnknownVoiceError) { /* pick from client.tts.listVoices() */ }
else if (err instanceof ServiceWarmingError) { /* retry in err.retryAfter s, or mode: "async" */ }
else if (err instanceof VoiceMakerAPIError) { console.error(err.code, err.requestId, err.hint); }
}from myvoicemaker import UnknownVoiceError, ServiceWarmingError, VoiceMakerAPIError
try:
client.tts.generate(text, voice_id=voice_id)
except UnknownVoiceError:
... # pick from client.tts.list_voices()
except ServiceWarmingError as e:
... # retry in e.retry_after s, or mode="async"
except VoiceMakerAPIError as e:
print(e.code, e.request_id, e.hint)Every SDK error exposes code, method, hint, description, retryable,
retryAfter/retry_after, requestId/request_id and issues; the full
class trees are in each SDK's README.