API Guides

Errors and rate limits

Classify failure outcomes, preserve structured evidence, and retry only bounded safe operations.

On any failure, first preserve the signal, stop the affected work, and decide whether you know the outcome. Retry only when the operation is safe to repeat, the structured state permits it, and an app deadline remains.

1. Classify where the failure surfaced

SurfaceWhat you observeFirst action
TransportConnect, read, inactivity, or disconnect failure with no platform responseTreat the outcome as unknown until the operation contract proves it safe to repeat; stop and close the affected transport
HTTPNon-2xx status and usually a structured ErrorResponse bodyPreserve status and body, then branch on code and retryable
Socket.IOStructured error event with code, message, retryable, timestamp, and optional idStop feeding the routed request or stream before deciding on recovery
SDKTyped batch exception, structured Socket.IO callback, or a generic rejection after projectionUse the richest typed or callback signal available; don't branch on exception text

A missing response isn't the same as retryable: true. A structured retryable: true is also not sufficient by itself: uploads and interrupted streams can remain unsafe or ambiguous to repeat.

2. Preserve HTTP and SDK fields

Protected Batch REST and realtime HTTP operations use the HTTP ErrorResponse shape:

FieldMeaning
errorLegacy identifier; keep for diagnostics and compatibility
codeMachine-readable category for app branching
detailOptional human-readable detail; don't branch on its wording
messageLegacy optional message on some authentication responses
job_idOptional Batch job or Realtime HTTP stream UUID tied to the failure
request_idOptional Batch request-correlation identifier
retryableWhether the server classifies this request failure as retryable
timestampServer timestamp
dataOptional structured limit or validation evidence; preserve its fields and units

This validated batch example means “fix the multipart body and don't retry it unchanged”:

{
  "error": "error.api.error.multipart.file.missing",
  "code": "VALIDATION_REQUIRED_FIELD",
  "detail": "error.api.error.multipart.file.missing",
  "retryable": false,
  "timestamp": "2026-01-15T10:30:00Z"
}

Always check the HTTP status before selecting a success parser. Preserve a raw body when structured parsing fails.

SDK 0.18.0 exposes different projections:

SDK surfaceStructured information
JavaScript batchBatchTranscribeError exposes statusCode, payload, code, retryable, jobId, detail, timestamp, capacity, and rawBody; rate-limit errors add retryAfter
Python batchBatchTranscribeError exposes status_code, payload, code, retryable, job_id, detail, timestamp, capacity, and raw_body; rate-limit errors add retry_after
Socket.IO callbacksSDK ErrorResponse preserves optional id, message, code, retryable, timestamp, retry_after_seconds, data, reason, and retry_scope
TTS server-error rejectionJavaScript rejects with generic Error; Python raises generic RuntimeError; the rejection keeps the message only, so record structured fields in onError / on_error
Workload-limit codesSDK 0.18.0 preserves data, classifies the current limit codes, and routes an id-bearing rejection to the active request context. The global callback still fires first; the synthesis call then rejects with its generic message-only error.
TTS content-policy codesSDK 0.18.0 exports TTS_INPUT_NOT_ALLOWED and TTS_MODERATION_UNAVAILABLE, classifies both as TTS-owned, and routes an id-bearing error to the active synthesis context

Still pre-validate TTS input before sending and always pass an explicit deadline (timeoutSeconds in JavaScript, timeout_seconds in Python). Read code and data from the global error callback before the request rejects; the generic exception keeps only the message.

3. Apply the decision table

Observed stateOutcome interpretationAction
400 or validation codeRequest was invalidCorrect UUID, parameters, framing, file, or audio; don't retry unchanged
401 or authentication codeKey is missing or invalidFix credentials before another request
403 with AUTH_FORBIDDENThe platform accepted the key but it lacks the capabilityUpdate access through your organization's key-management flow; don't retry unchanged
Other 403 responseA gateway or intermediary rejected the requestPreserve the raw body or support identifier, then verify URL, route, credentials, and any deployment-specific header requirements
404 / TRANSCRIPTION_JOB_NOT_FOUNDRequested job isn't available under that UUIDStop polling that UUID and investigate the stored identifier
405 / METHOD_NOT_ALLOWEDPath or method is wrongCorrect routing before another request
Batch 422 with a validation code such as VALIDATION_FILE_CORRUPTFile is unsupported, corrupt, empty, or zero-durationCorrect the input; don't retry the same bytes
Batch 422 / AUDIO_DURATION_EXCEEDED with data.bound audio_durationThe audio is valid but decodes to longer than the accepted durationSplit the recording to data.limit seconds or less, or submit a shorter file; don't retry unchanged
Batch 422 / FILE_COUNT_EXCEEDEDToo many parts for one request. data.bound says which, and data.unit counts it: file_parts counts audio files, multipart_parts counts every multipart partFor file_parts, send at most data.limit audio file per request; for multipart_parts, keep the whole form within data.limit parts; don't retry unchanged
Realtime TTS 422 / CHARACTER_COUNT_EXCEEDEDText, or a voice reference's transcript, is longer than its ceiling. data.bound says which: tts_input_characters or tts_voice_reference_text_charactersShorten the text using data.limit; don't retry unchanged
Realtime TTS 422 / VOICE_REFERENCE_COUNT_EXCEEDEDMore than one entry in voice_references; exactly one is acceptedSend a single reference; don't retry unchanged
Realtime TTS 422 / AUDIO_DURATION_EXCEEDED with data.bound tts_voice_reference_durationThe reference clip is longer than the deployment's configured reference ceiling, which a deployment may set below the model's own limitTrim the clip to data.limit seconds — data.limit is authoritative, not any published default; don't retry unchanged
Realtime TTS 413 / PAYLOAD_TOO_LARGE with data.bound tts_voice_reference_bytesThe reference clip's decoded size is over the ceilingSend a shorter or lower-sample-rate clip; don't retry unchanged
TTS 400 / TTS_INPUT_NOT_ALLOWEDThe content policy rejected the text; the same text will not be acceptedChange the text before sending another request; don't retry it unchanged
TTS 503 / TTS_MODERATION_UNAVAILABLEThe moderation authority could not make a decision, so synthesis failed closedDo not treat this as a policy rejection. Retry only after bounded backoff and while the application deadline remains
402 / CREDITS_EXHAUSTEDFunding is exhausted; an immediate replay cannot restore itStop the affected work and don't retry unchanged. A mid-stream realtime event is terminal and is followed by disconnect.
503 / BILLING_AUTHORIZATION_UNAVAILABLEThe billing authority could not make a decision, so the platform failed closedStop the affected work and retry only after bounded backoff. A mid-stream realtime event is terminal and requires a new stream.
Realtime 409 / ASR_STREAM_EXPIREDAudio inactivity or a lost backend sequence retired this ASR streamRead reason, require retry_scope: "new_stream", preserve committed output, and start a fresh UUID; never replay on the retired id.
Batch 429 / RATE_LIMIT_EXCEEDEDExplicit capacity backpressureQueue or slow submissions, then use a bounded retry policy
HTTP 5xx with retryable: true on a Batch result GET made with save_result=truePreserved result read failedRetry with capped backoff and jitter while the deadline remains
HTTP 5xx with retryable: true on an uploadServer invites retry, but creation outcome can still be ambiguousDon't blindly replay; apply an explicit duplicate-risk policy
Any response with retryable: falseServer says not to retry this request stateStop until input, credentials, route, or configuration changes
Read timeout before any responseNo platform classificationRetry only a read whose contract preserves the result, and only within the deadline
Realtime error or disconnectOld stream state and accepted-audio boundary can be uncertainStop, preserve committed results, clean up, and recover with a fresh UUID if policy permits
TTS voice_id resolution error (SAU-2258)A caller-supplied voice_id that is not a valid UUID is 400 VALIDATION_INVALID_UUID; a well-formed one that does not identify an available voice is 400 TTS_VOICE_NOT_FOUND (both non-retryable). A resolved voice with incomplete or corrupt stored data is a non-retryable 500 TTS_VOICE_RESOLUTION_FAILED; a transient database/storage outage during resolution is a retryable 503 SERVER_DEPENDENCY_FAILURE. Genuine model/capacity/inference failures stay a retryable 500 TTS_SYNTHESIS_FAILEDFix the voice_id or send voice_references; retry only the 503 and 500 TTS_SYNTHESIS_FAILED with backoff, never the two non-retryable 400s or 500 TTS_VOICE_RESOLUTION_FAILED
TTS 400 after supplying both voice selectors, or a malformed referenceSupplying voice_id together with a non-empty voice_references, an explicit empty voice_references array, or reference audio that is not strictly canonical base64 mono PCM16 RIFF/WAVE, is a non-retryable validation errorFor SDK calls, choose exactly one of voice_id or one canonical voice_references entry. Direct HTTP also permits neither selector, which uses deployment-defined voice selection. Never send both.

A deployment gateway can return 429 for a Realtime HTTP request, while the current Fast/live backend can collapse an internal audio-capacity failure into retryable 500 with ASR_TRANSCRIPTION_FAILED. Do not infer a realtime quota, capacity pool, or reset window from either response or from Batch capacity.

4. Treat Batch 429 as capacity backpressure

Batch 429 includes the normal structured fields plus data.capacity:

{
  "error": "error.rate_limit",
  "code": "RATE_LIMIT_EXCEEDED",
  "detail": "error.rate_limit",
  "retryable": true,
  "timestamp": "2026-01-15T10:30:00Z",
  "data": { "capacity": 120.5 }
}

capacity is remaining capacity measured in audio seconds. Use it for queue and admission decisions. It is not a documented quota, reset window, or number of seconds to sleep.

The OpenAPI contract doesn't promise a Retry-After header. SDK retryAfter / retry_after is populated only when a valid header is actually present. If it is absent, apply capped exponential backoff with jitter. Always bound attempts, individual delays, total retry time, and producer concurrency with an overall deadline.

5. Preserve a Batch result before retrying its read

The released SDK doesn't retry batch calls. maxRetries / max_retries is deprecated, ignored, and retained only for compatibility. These tested fixtures retry getResult() / get_result(), not an upload, and explicitly pass saveResult: true / save_result=True. The default false can clear a terminal result after building the response, so a lost response can be followed by cleared. The preservation option makes bounded retry possible but does not define a retention duration. The fixtures use the released typed properties:

batch-error-retry.ts
import {
  BatchTranscribeClient,
  BatchTranscribeError,
  BatchTranscribeRateLimitError,
  Language,
  type TranscriptionResponse,
} from '@humain-voice/sdk';

function requiredEnv(name: 'API_KEY' | 'API_URL'): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required`);
  return value;
}

async function pause(milliseconds: number): Promise<void> {
  await new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
}

async function getResultWithRetry(
  client: BatchTranscribeClient,
  jobId: string,
  attempts = 5,
): Promise<TranscriptionResponse> {
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    try {
      // saveResult prevents a terminal read from clearing the stored result
      // before a retry. It does not establish a retention duration.
      return await client.getResult(jobId, Language.ArEn, { saveResult: true });
    } catch (error: unknown) {
      if (!(error instanceof BatchTranscribeError)) throw error;

      const rateLimited = error instanceof BatchTranscribeRateLimitError;
      const retryable = rateLimited || error.retryable === true;
      console.error({
        statusCode: error.statusCode,
        code: error.code,
        capacity: error.capacity,
      });
      if (!retryable || attempt === attempts) throw error;

      const serverDelayMs = rateLimited && error.retryAfter !== undefined
        ? error.retryAfter * 1_000
        : 0;
      const exponentialDelayMs = 500 * 2 ** (attempt - 1);
      await pause(Math.max(serverDelayMs, exponentialDelayMs) + Math.random() * 250);
    }
  }

  throw new Error('Retry loop exhausted');
}

async function main(): Promise<void> {
  const jobId = process.argv[2];
  if (!jobId) throw new Error('Pass a batch job ID as the first argument');

  const client = new BatchTranscribeClient({
    api_url: requiredEnv('API_URL'),
    api_key: requiredEnv('API_KEY'),
  });
  try {
    const result = await getResultWithRetry(client, jobId);
    console.info(result.status, result.results?.transcript ?? '');
  } finally {
    await client.close();
  }
}

void main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});

Expected behavior: retryable reads use at most five attempts, respect an actual retryAfter / retry_after value when present, add exponential delay and jitter, and rethrow the final failure. The attempt bound doesn't replace an overall app deadline.

6. Keep ambiguous operations out of automatic retry

Complete-file uploads

Batch job creation and fast complete-audio submission don't document an idempotency-key contract. A timeout or disconnect after bytes were sent doesn't prove either success or failure: the server may have accepted the request.

If a jobId or request UUID was returned, retain it and continue through that operation's normal result path. Without a conclusive response, record the ambiguous attempt and repeat only under an app policy that explicitly accepts duplicate work.

Realtime streams

The public Socket.IO and HTTP contracts define neither transparent resume nor idempotent frame replay. On an error or disconnect:

  1. Stop sending audio and close the old stream or response.
  2. Preserve committed results and discard unresolved provisional state.
  3. Record the uncertain audio interval.
  4. Reconnect with bounded backoff and a fresh UUID only if the error and app deadline permit it.
  5. Keep the new stream separate until the app explicitly reconciles timelines.

Don't replay old frames or reuse the old UUID under an assumption of server-side deduplication.

7. Separate timeouts from the overall deadline

ControlWhat it boundsWhat expiry proves
Connect or request timeoutOne network phase or requestThe client stopped waiting; not whether the server acted
Read or inactivity timeoutTime waiting for the next byte, chunk, or eventNo progress arrived in that interval; not total operation duration
Realtime final-wait timeoutWait after the final input frameThe wait ended; not that a final result arrived
Overall app deadlineConnection, work, retries, delays, and cleanup togetherThe app must stop further work

JavaScript TTS timeoutSeconds defaults to 30 seconds of inactivity while waiting for each audio chunk. Python TTS has no timeout unless timeout_seconds is supplied. Realtime ASR SDK close() waits only for protocol-level is_final, a routed error, or its close timeout and can return when that wait expires. None of these controls replaces the overall deadline.

After any expiry, clean up in finally: cancel HTTP reads and close response bodies; stop Socket.IO audio and call disconnect() or exit the Python async context; close the batch client. Don't infer that client cleanup canceled server-side work or in-flight TTS synthesis.

8. Log decisions and test the failure paths

Log the operation, transport, HTTP status or event name, code, retry decision, request or job UUID, attempt, deadline remaining, timeout type, and Batch capacity when present. Don't log API keys, full audio, or sensitive transcript text by default.

Next, run the safe-read fixture, then test invalid input, authentication, explicit 429, read timeout, ambiguous upload timeout, realtime disconnect, final-wait expiry, and cleanup in an isolated test environment.

On this page