Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

Error codes

Every KINDI error response is JSON of the form { "detail": "<message>" }. The status code carries the meaning; some statuses include extra fields (call those out below).

Catalogue

StatusdetailWhen it firesExtra response fieldsRetry?
400"key must be valid base64"The BYOK key field isn't strict standard base64no
400"key must decode to 32 bytes (256 bits), got {n}"The BYOK key decoded, but to the wrong lengthno
400"unknown_judge_model"judge_model isn't in the operator's allowlistno
401"Missing authentication"Neither a bearer nor a session cookie was presentedno
401"Missing authentication token"No Authorization header on a Bearer-only endpointno
401"Malformed authentication token"The bearer isn't a well-formed mk_live_<prefix>_<secret>no
401"Invalid authentication token"Unknown, revoked, or wrong-secret key. Revocation propagates in up to 60s per replica (auth cache).no
401"Missing session cookie"A session-auth endpoint was called with no cookieno
401"Invalid or expired session"The session cookie is unknown or past its TTLno
402"insufficient_tokens"Paid balance + free quota both exhaustedneeded_tokens (int), available_tokens (int), topup_url (string)no
403(no body at all)Geo-block: the API is served from Riyadh and reachable only from Saudi IP addresses. Blocked at the edge, before the app. Not an auth failure.no
404"Glossary not found"The glossary_id you sent doesn't exist or isn't yours. Checked before billing, so it isn't charged.no
404"Not Found"Endpoint typono
413"file_too_large"File upload exceeds the 100 MB cap (see Files)no
413{"error": "request_too_large"}An LLM Proxy request body exceeds the proxy body cap; note the proxy's error body shapeno
422"<pydantic validation message>"Schema validation (wrong type, invalid enum, empty text)detail is an array of pydantic error objectsno
429"Rate limit exceeded"The per-key (or per-user) sliding 60-second window is fullRetry-After header (seconds)yes
429"too_many_files_today"The per-user daily file-upload cap is reachedno
500"Masking operation failed"Unhandled error on /api/v1/mask. Deliberately generic; it never echoes your text.yes
500"Redact operation failed"Unhandled error on /api/v1/redactyes
503"masker_unavailable"The detection service is unreachable or shedding loadRetry-After header when the cause is overloadyes
A 403 with an empty body is the geo-block, not an authentication problem. api.kindi.me, dashboard.kindi.me, and admin.kindi.me only accept traffic from Saudi IP addresses; the block happens at the edge proxy, so there is no {"detail": …} to parse. Retrying, re-minting the key, or refreshing the session will not help; the call has to originate inside Saudi Arabia (for example from your own KSA-region backend rather than an end-user's browser abroad). kindi.me and docs.kindi.me are world-readable.
A 503 masker_unavailable request is refunded — the gateway reverses the exact free/paid split it debited before calling the masker, on a best-effort basis. Retrying therefore doesn't double-charge you.
Branch your error handling on the status code first and treat detail as a human-readable hint. The only detail strings stable enough to branch on are the machine codes: insufficient_tokens, masker_unavailable, unknown_judge_model, too_many_files_today, and the file error_code values. Prose messages like "Invalid authentication token" may be reworded.

Retry guidance

Retry only 429, 500, 503. Concretely:
def kindi_with_retry(client, body, max_attempts=4): for attempt in range(max_attempts): r = client.post("/api/v1/mask", json=body) if r.status_code < 400: return r.json() if r.status_code == 429: time.sleep(int(r.headers.get("Retry-After", "1"))) continue if r.status_code in (500, 503) and attempt < max_attempts - 1: time.sleep(2 ** attempt) # 1s, 2s, 4s continue r.raise_for_status()
Do not retry 400 / 401 / 402 / 403 / 404 / 422; those are caller-side issues. Retrying them either burns rate-limit quota (401) or just wastes round-trips (400/422). 402 only resolves with a billing top-up or the next Asia/Riyadh midnight free-quota refill, and 403 (the geo-block) resolves only by calling from a Saudi IP address.
503 masker_unavailable is the one server-side failure that is safe to retry aggressively: the request is refunded, so a retry costs you the same as the first attempt did.

Concrete examples

402 insufficient_tokens

HTTP/1.1 402 Payment Required Content-Type: application/json { "detail": "insufficient_tokens", "needed_tokens": 142, "available_tokens": 38, "topup_url": "/dashboard/billing" }
topup_url is a dashboard-relative path, not an absolute URL. Resolve it against https://dashboard.kindi.me before linking: a verbatim hyperlink would resolve against your API host and 404.

429 Too Many Requests

HTTP/1.1 429 Too Many Requests Retry-After: 17 Content-Type: application/json {"detail":"Rate limit exceeded"}
The limiter is a sliding 60-second window, not a fixed clock-minute bucket, so Retry-After is 60 − the age of the oldest request still in the window: the number of seconds until one slot frees up. It is the only rate-limit header KINDI sends; there is no X-RateLimit-* family. See Rate limits.
Rate-limiter outages are fail-open: if the limiter is briefly unreachable, the request is allowed and a warning is logged server-side. You should never see 429 purely because of an infrastructure issue.

File redaction errors

File redaction is async, so failures come in two layers:
At upload (synchronous HTTP status on POST /api/v1/files/redact):
StatusdetailWhen
400"unsupported_format"Not a supported type, or a corrupt / password-protected file
413"file_too_large"Over the 100 MB cap
429"too_many_files_today"100 uploads per user per Asia/Riyadh day reached
402"insufficient_tokens"Balance exhausted before the job is enqueued
At processing: the upload succeeds (201, state: "queued"), but the job can later finish state: "failed" with an error_code. KINDI fails closed: a file that can't be proven clean is never returned as ready. See the file error_code table for the full list (redaction_incomplete, verify_failed, image_redact_failed, too_many_pages, …). masker_unavailable and internal_error are transient; safe to re-upload.