Errors
Every failure carries a stable code. Switch on that, never on the message.
Every failure returns the same envelope, plus a Retry-After header where a
retry makes sense:
{ "error": "human-readable prose", "code": "credits_exhausted", "retryAfter": 30 }code is the contract. The error prose is written for humans and will
change, so a client that matches on it will break. Every shipped SDK switches
on code.
One case surprises people: a video job that fails does so inside a 200.
The job was accepted, polled, and finished unsuccessfully — so the HTTP status
describes the poll, and the failure arrives as result.error.code.
Codes
These statuses are verified against what the handlers actually write. A few codes answer with more than one, which is noted inline — that reflects a real inconsistency in the service rather than an omission here.
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
bad_request | 400, 404, 405 | The request was malformed or a field was missing — including a malformed multipart upload. Also used for an unknown route or a wrong method. | Fix the request; retrying as-is will not help. |
unsupported_media | 400 | The file's real type is not one this endpoint accepts. | Check the type against limits.upload in the catalog. |
too_large | 413 | The upload is over the byte or pixel limit. | Resize or re-encode before sending. |
too_long | 400 | The clip is longer than the allowed duration. | Trim it to within limits.videoMaxSeconds. |
turnstile_failed | 403 | The bot challenge was not solved. | Retry the challenge. |
challenge_unavailable | 503 | The challenge provider could not be reached. | Retry shortly; this fails closed on purpose. |
bad_token | 401, 403 | The bearer token is missing or not valid (401), or valid but not allowed here (403). | Create a new key or session. |
token_expired | 401 | The token was valid but has expired. | Create a new one. |
credits_exhausted | 402 | This token has spent its frame budget. | Wait for the reset, or use a key with a larger budget. |
account_budget_exhausted | 429 | Your account has spent its frame allowance for the current window. | Wait for the refill; Retry-After gives the seconds. This is the one limit you cannot work around with a new key or another machine. |
ip_rate_limited | 429 | Your network has reached its hourly or daily budget. | Honour Retry-After. This is the limit a client cannot opt out of. |
global_rate_limited | 503 | The whole service is over its budget. | Back off and retry; this protects every caller. |
busy | 503 | No inference slot was free in time. | Retry after a short delay. |
ai_unavailable | 503 | The detection server is down, or the circuit breaker is open. | Retry later; check aiStatus in the catalog first. |
signin_unavailable | 503 | The sign-in service could not be reached, so the session could not be checked. | Retry. This is not a rejection — do not clear the session on it. |
demo_disabled | 503 | Inference has been switched off by an operator. | Nothing to do client-side. |
job_not_found | 404 | No such video job, or it belongs to another token. | Job ids are scoped to the token that created them. |
session_cooldown | 429 | A new token was requested too soon after the last one. | Reuse the token you have; honour Retry-After. |
store_unavailable | 503 | The service could not reach its own state store. | Retry. Limits fail closed rather than opening up. |
internal | 500 | An unexpected error. | Retry once; report it with the X-Request-Id if it persists. |
What to retry
Retry only 429 and 503, and only for as long as Retry-After says.
Hammering a 429 is how a client gets blocked. A 4xx other than 429 means the
request itself needs changing — retrying it unchanged will fail identically.