Skip to content

Errors and limits

Every status code Sandchest returns and what to do about it, plus the size limits, the rate limit, and the deadlines you should design around.

Open .md

Every failure is the same shape — { "error": "..." } with a status code that means something. This page maps each code to a cause and a fix, then lists the limits worth knowing before you hit them.

The error shape#

JSON
{ "error": "Your credit balance is exhausted. Add credits to continue transcribing." }

That is the whole body. Messages are stable and safe to match on, and they never echo your request back — no URLs, no vocabulary, no credentials.

Status codes#

CodeMeaningWhat to do
400Invalid request: an unknown option, an invalid value, an unsupported feature, or an unknown transcript id on a /v2 endpointRead the message — it names the problem. Do not retry unchanged.
401The API key is missing, malformed, revoked or expiredCheck the Authorization header. Rotate the key if it was revoked.
402The workspace is out of creditsAdd credits, or enable auto top-up. See Billing.
403Declared by the API contract; no endpoint currently returns itNothing. Handle it as a permanent failure if you are being thorough.
404Unknown transcript id — native /api/v1 endpoints onlyCheck the id. The /v2 endpoints return 400 in the same situation.
409Idempotency-Key conflictThe key was used for a different request, or belongs to a deleted transcript. Use a new key.
413The body exceeds a size limit100 MiB for uploads, 64 KB for transcript creation. Split or shrink.
429Rate limitedBack off and retry. Windows are one minute long.
503Inference, billing, storage or rate limiting is temporarily unavailableRetry with backoff. This is transient, and safe to retry with an Idempotency-Key.

Errors that are not HTTP errors#

A transcript can fail after it was accepted. The create call returns 200, and the failure shows up on the object:

JSON
{
  "id": "tr_9k2mQvR7pKd3sTn1yBhL",
  "status": "error",
  "error": "Parakeet does not support the requested language: ja",
  "words": null,
  "text": null
}

Always branch on status before reading words. Common causes:

error saysCause
Parakeet does not support the requested language: …The code is accepted by the API but outside the deployed model's coverage. See Languages.
language_detection cannot be performed on files with no spoken audio.Silent or music-only audio with detection on. Pin a language_code instead.

Failed transcripts are not billed — metering only counts completed audio.

Size limits#

LimitValueResponse when exceeded
Upload body100 MiB (SANDCHEST_MAX_UPLOAD_BYTES)413, {"error":"The uploaded audio file exceeds the configured size limit."}
Transcript request body64 KB413, {"error":"The transcription request exceeds the 64 KB size limit."}
Idempotency-Key200 characters400, {"error":"Idempotency-Key must contain at most 200 characters."}
webhook_url8192 characters400, {"error":"Invalid webhook URL or authentication parameters."}
Webhook auth header value1000 characters400, same message
Word-search phrase5 words400, {"error":"Phrases are currently restricted to five words at most."}
List limit1–200400, {"error":"'limit' must be less than or equal to 200"}
List created_onWithin the last 90 days400, {"error":"'created_on' cannot be more than 90 days in the past"}
Multichannel channels32The transcript fails with a channel-count error

An empty upload body is rejected too: 400 with {"error":"The uploaded audio file is empty."}.

Self-hosted deployments set their own upload limit — see Self-hosting.

Rate limits#

120 requests per minute per workspace by default, counted in fixed one-minute windows across every endpoint. Over the limit:

429 Too Many Requests
{ "error": "Too many requests. Please retry shortly." }

POST /v2/upload also sets a retry-after header with the number of seconds until the current window resets. Other endpoints do not, but the window is always at most 60 seconds, so waiting a minute always clears it.

TypeScript
async function withRateLimitRetry<T>(call: () => Promise<T>): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await call();
    } catch (error) {
      if (!(error instanceof SandchestError) || error.status !== 429 || attempt >= 5) throw error;
      await new Promise((resolve) => setTimeout(resolve, Math.min(60_000, 2 ** attempt * 1000)));
    }
  }
}

The limit counts requests, not audio. Polling a long transcript every 75 milliseconds will hit it long before your uploads do — poll every second or two, or use a webhook and stop polling entirely.

Self-hosted deployments change it with SANDCHEST_RATE_LIMIT_PER_MINUTE.

Deadlines#

DeadlineValue
SDK polling timeout30 minutes (pollingTimeout), then SandchestError with status 408
SDK polling interval75 ms by default; raise it for long files
Webhook attempt timeout10 seconds per attempt
Webhook attemptsUp to 10, roughly 10 seconds apart
Uploaded audio retention24 hours (SANDCHEST_AUDIO_RETENTION_HOURS)

The SDK's polling timeout is a client-side deadline. When it expires the transcript keeps going — fetch it later by id.

Retrying safely#

Retries are only safe when they cannot create a second transcript. Send an Idempotency-Key on every creation request you might retry:

shell
curl -sS https://stt-api.sandchest.com/v2/transcript \
  -H "Authorization: $SANDCHEST_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: job-8842" \
  -d '{"audio_url":"'"$UPLOAD_URL"'"}'
  • Same key, same request → the original transcript, and no second charge.
  • Same key, a different request → 409.
  • No key → a second transcript, and a second charge.

Derive the key from something stable in your own system — a job id, a recording id, a content hash. Not a random value, or a retry after a crash will not match.

CodeRetry?
400, 402, 403, 404, 409, 413No. Fix the request first.
401No, unless you have just rotated the key.
429Yes, after the window resets.
503Yes, with exponential backoff.
Network error or timeoutYes — with an Idempotency-Key.

Verify#

Provoke each class of failure and check you get the code you expect:

shell
API="https://stt-api.sandchest.com"

# 401 — no key
curl -sS -o /dev/null -w '%{http_code}\n' "$API/v2/transcript?limit=1"

# 400 — an option that does not exist
curl -sS -o /dev/null -w '%{http_code}\n' "$API/v2/transcript" \
  -H "Authorization: $SANDCHEST_API_KEY" -H "Content-Type: application/json" \
  -d '{"audio_url":"https://example.com/a.mp3","not_a_real_option":true}'

# 400 — an unknown transcript id on the compatibility endpoint
curl -sS -o /dev/null -w '%{http_code}\n' "$API/v2/transcript/tr_does_not_exist" \
  -H "Authorization: $SANDCHEST_API_KEY"

# 404 — the same id on the native endpoint
curl -sS -o /dev/null -w '%{http_code}\n' "$API/api/v1/transcripts/tr_does_not_exist" \
  -H "Authorization: $SANDCHEST_API_KEY"
expected output
401
400
400
404

Next#

Billing — what a second of audio costs and how credits work.