Errors

How the API reports failures, the envelope shapes you should parse, and how to handle each category.

The KI-AVA Amharic API uses conventional HTTP status codes and a small set of stable JSON envelopes to report failures. Errors come back in two places: up-front on the HTTP call (request rejected before a job is created), and inside successful job responses (individual files or translation units failed while the job as a whole completed). Both need to be handled.

Error envelope shapes

There are three distinct shapes. Every error you get from the API matches one of them.

Standard error

Used for 400, 401, 403, 404, 409, and 500 responses.

json
{
  "detail": "Invalid API Key."
}

Validation error (422)

Returned when the request body fails schema validation. FastAPI's field-level shape — detail is an array of per-field errors, each with a loc path, a msg, and a type.

json
{
  "detail": [
    {
      "loc": ["body", "audio_urls"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

Per-unit translation error

Nested inside a successful 200 response from GET /translate/{job_id}/results, at outputs[].error. Has a stable code enum (listed below) and a human-readable message.

json
{
  "code": "TRANSLATION_ERROR",
  "message": "Lesan MT API returned a non-retryable error after 3 retries."
}

HTTP status codes

Every endpoint documents its exact set of possible status codes in the API Reference. At a glance:

StatusMeaningTypical triggerRetryable?
400Bad RequestRequest exceeds your tier's file or translation-unit limitNo — fix the request
401UnauthorizedX-API-Key header missingNo — add credentials
403ForbiddenAPI key invalid, deactivated, or lacks admin privilegesNo — use a valid key
404Not FoundJob ID does not exist (or is not of the expected type)No
409ConflictOperation not allowed in the job's current state (e.g. reading results or deleting while still PROCESSING)Yes — after the job leaves the running state
422Unprocessable EntityRequest body failed Pydantic validationNo — fix the request
429Too Many RequestsPer-minute / per-hour / per-day rate limit or max concurrent jobs exceeded for your tierYes — honor Retry-After
500Internal Server ErrorServer misconfiguration (STORAGE_BUCKET_NAME or LESAN_API_KEY not set) or an unhandled exceptionCautiously — contact support if it persists

Error detail strings

The exact detail strings the API emits. Match on these if your client needs to distinguish specific error conditions programmatically.

Statusdetail valueWhen it fires
401Missing API Key. Include X-API-Key header.No X-API-Key header in the request
403Invalid API Key.Key not recognized
403API Key has been deactivated.Key exists but was disabled by an admin
403Admin access requiredNon-admin key used on an admin endpoint
422{"detail": [{"type":…, "loc":[…], "msg":…}]}FastAPI field-level validation (see shape above)
429Rate limit exceeded: N requests/{minute,hour,day}Any rate bucket exceeded; Retry-After header included

Handling 429 rate limits

Rate limits are enforced per API key across three buckets — requests per minute, per hour, and per day — plus a cap on concurrent jobs. All endpoints share the same bucket. When you exceed any limit, the API returns 429 with a Retry-After header (seconds). Sleep at least that long before retrying.

import time, requests

def request_with_retry(method, url, **kwargs):
    while True:
        r = requests.request(method, url, **kwargs)
        if r.status_code != 429:
            return r
        retry_after = int(r.headers.get('Retry-After', 5))
        time.sleep(retry_after)

For long polls, use exponential backoff (start at 2 s, multiply by 1.5, cap at 30 s) between successful calls as well — fixed sub-second polling wastes your budget and rarely returns fresher data.

Job-level vs per-file failures

Transcription and translation jobs can partially succeed. The HTTP call returns 200, but individual files/units are marked failed. You must inspect status and the per-item arrays after polling to a terminal state.

Transcription job statuses

PENDINGPROCESSING → one of COMPLETED, PARTIAL (some files failed), or FAILED (all files failed). A top-level error string is set on FAILED. Per-file errors appear as plain strings on each entry of the results[] array.

json
{
  "job_id": "90310bc7-62a2-45c7-b92c-91fc5ccf3bcc",
  "status": "PARTIAL",
  "error": null,
  "results": [
    {
      "original_url": "https://example.com/good.mp3",
      "status": "COMPLETED",
      "duration": 45.2,
      "processing_time": 12.5,
      "num_segments": 15,
      "transcript_url": "https://storage.googleapis.com/.../transcript.json",
      "error": null
    },
    {
      "original_url": "https://example.com/broken.mp3",
      "status": "FAILED",
      "duration": 0,
      "processing_time": 0.4,
      "num_segments": 0,
      "transcript_url": null,
      "error": "Unable to download audio: HTTP 404"
    }
  ]
}

Translation job statuses

Both transcription and translation share the same status vocabulary: PENDINGPROCESSING → one of COMPLETED, PARTIAL, or FAILED. Detailed per-unit outcomes come from a follow-up GET /translate/{job_id}/results — calling that endpoint while the job is still PENDING or PROCESSING returns 409, so poll status first.

json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "summary": {
    "status": "PARTIAL",
    "success_count": 1,
    "failure_count": 1,
    "total_characters": 15400
  },
  "outputs": [
    {
      "source_uri": "https://storage.googleapis.com/bucket/source/doc1.txt",
      "target_language": "en",
      "status": "COMPLETED",
      "target_uri": "https://storage.googleapis.com/bucket/results/doc1_en.txt",
      "error": null
    },
    {
      "source_uri": "https://storage.googleapis.com/bucket/source/doc2.bin",
      "target_language": "en",
      "status": "FAILED",
      "target_uri": null,
      "error": {
        "code": "UNSUPPORTED_FORMAT",
        "message": "Source file is not valid UTF-8 text."
      }
    }
  ]
}

Per-unit translation error codes

The code field on outputs[].error is one of five stable values:

CodeMeaningRetryable?
DOWNLOAD_ERRORCould not fetch the source file from its URIOnly after fixing the source URI
UNSUPPORTED_FORMATSource file is not valid UTF-8 textNo — convert the file first
TRANSLATION_ERRORLesan MT API returned a non-retryable error or retries were exhaustedServer already retried 3×; resubmit in a new job if needed
UPLOAD_ERRORTranslated file could not be written to the destination bucketOnly after fixing the destination
INTERNAL_ERRORUnexpected exception during unit processingCautiously — contact support if it persists

Recommended client pattern

One reference implementation that handles every documented error category:

python
import time, requests

class ApiError(Exception): pass
class AuthError(ApiError): pass
class ValidationError(ApiError): pass

def call(method, url, api_key, **kwargs):
    headers = {'X-API-Key': api_key, **kwargs.pop('headers', {})}
    delay, cap = 1.0, 30.0
    while True:
        r = requests.request(method, url, headers=headers, **kwargs)
        if r.status_code == 429:
            time.sleep(int(r.headers.get('Retry-After', 5)))
            continue
        if 500 <= r.status_code < 600:
            time.sleep(delay); delay = min(delay * 2, cap)
            continue
        if r.status_code in (401, 403):
            raise AuthError(r.json().get('detail', r.text))
        if r.status_code == 422:
            raise ValidationError(r.json().get('detail'))
        if not r.ok:
            raise ApiError(r.json().get('detail', r.text))
        return r.json()

def surface_unit_errors(results):
    # Transcription: results[].error is a plain string
    # Translation: outputs[].error is {code, message} or null
    for item in results.get('results', []) or results.get('outputs', []):
        err = item.get('error')
        if not err: continue
        if isinstance(err, dict):
            print(f"{item.get('source_uri')}: {err['code']} - {err['message']}")
        else:
            print(f"{item.get('original_url')}: {err}")

See the API Reference for the exact status codes documented on each endpoint, and the Best Practices guide for higher-level reliability tips.