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.
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#
{ "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#
| Code | Meaning | What to do |
|---|---|---|
400 | Invalid request: an unknown option, an invalid value, an unsupported feature, or an unknown transcript id on a /v2 endpoint | Read the message — it names the problem. Do not retry unchanged. |
401 | The API key is missing, malformed, revoked or expired | Check the Authorization header. Rotate the key if it was revoked. |
402 | The workspace is out of credits | Add credits, or enable auto top-up. See Billing. |
403 | Declared by the API contract; no endpoint currently returns it | Nothing. Handle it as a permanent failure if you are being thorough. |
404 | Unknown transcript id — native /api/v1 endpoints only | Check the id. The /v2 endpoints return 400 in the same situation. |
409 | Idempotency-Key conflict | The key was used for a different request, or belongs to a deleted transcript. Use a new key. |
413 | The body exceeds a size limit | 100 MiB for uploads, 64 KB for transcript creation. Split or shrink. |
429 | Rate limited | Back off and retry. Windows are one minute long. |
503 | Inference, billing, storage or rate limiting is temporarily unavailable | Retry 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:
{
"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 says | Cause |
|---|---|
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#
| Limit | Value | Response when exceeded |
|---|---|---|
| Upload body | 100 MiB (SANDCHEST_MAX_UPLOAD_BYTES) | 413, {"error":"The uploaded audio file exceeds the configured size limit."} |
| Transcript request body | 64 KB | 413, {"error":"The transcription request exceeds the 64 KB size limit."} |
Idempotency-Key | 200 characters | 400, {"error":"Idempotency-Key must contain at most 200 characters."} |
webhook_url | 8192 characters | 400, {"error":"Invalid webhook URL or authentication parameters."} |
| Webhook auth header value | 1000 characters | 400, same message |
| Word-search phrase | 5 words | 400, {"error":"Phrases are currently restricted to five words at most."} |
List limit | 1–200 | 400, {"error":"'limit' must be less than or equal to 200"} |
List created_on | Within the last 90 days | 400, {"error":"'created_on' cannot be more than 90 days in the past"} |
| Multichannel channels | 32 | The 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:
{ "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.
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#
| Deadline | Value |
|---|---|
| SDK polling timeout | 30 minutes (pollingTimeout), then SandchestError with status 408 |
| SDK polling interval | 75 ms by default; raise it for long files |
| Webhook attempt timeout | 10 seconds per attempt |
| Webhook attempts | Up to 10, roughly 10 seconds apart |
| Uploaded audio retention | 24 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:
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.
| Code | Retry? |
|---|---|
400, 402, 403, 404, 409, 413 | No. Fix the request first. |
401 | No, unless you have just rotated the key. |
429 | Yes, after the window resets. |
503 | Yes, with exponential backoff. |
| Network error or timeout | Yes — with an Idempotency-Key. |
Verify#
Provoke each class of failure and check you get the code you expect:
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"401
400
400
404Next#
Billing — what a second of audio costs and how credits work.