AI video API error codes: what they mean and when to retry
Video generation jobs fail more often than ordinary API calls: models get busy, refuse prompts or time out. This guide lists every error you can get back, which ones are worth retrying, and how to retry safely.
Two places an error can show up
A video or image job can go wrong at two moments, and your code has to handle both:
- When you submit.
POST /v1/generationsanswers with an HTTP error status and a body like{"error": {"code": "...", "message": "..."}}. Nothing was started and nothing is charged. - While it runs. The submit succeeded with
202, but the job later ends withstatus: "failed". Itserrorhas acode, amessageyou can show to the person who asked, andretryable.
A failed job is never charged on any model. The price is held from your balance when you submit and released if the job fails.
HTTP errors when you submit
| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | invalid_request | The body is not JSON, lacks model or input, or has a field the API does not know. | No. Fix the body. |
| 401 | unauthorized | The API key is missing or wrong. | No. Check the key. |
| 402 | insufficient_credits | The balance does not cover the price held for the job. | After a top-up. |
| 403 | account_disabled, generation_not_enabled | The account is paused, or the key can read prices but not generate. | No. Contact support. |
| 404 | not_found | No generation with that id on your account. | No. |
| 409 | idempotency_conflict | The Idempotency-Key was already used with a different body. | With a new key. |
| 409 | not_ready | You asked for the file before the job succeeded. | Yes, once it has succeeded. |
| 413 | body_too_large | The JSON body is over 1 MB. Upload files and send links instead. | No. Shrink the body. |
| 422 | invalid_input | A value the model does not take, such as a length or size it does not list. The message says which. | No. Change the value. |
| 422 | max_cost_exceeded | Every route costs more than your max_cost_usd. | No. Raise the cap or lower the settings. |
| 422 | model_not_available | No model with that id. | No. Check the id. |
| 429 | rate_limited | Too many requests in the same second from one account. | Yes, after Retry-After. |
| 429 | concurrency_limit | The account already has its maximum number of jobs running. | Yes, when one finishes. |
| 500 | server_error | Something broke on our side. | Yes, with the same Idempotency-Key. |
| 503 | busy | A moment of load. | Yes, after Retry-After, same key. |
| 503 | model_unavailable | No route for this model is up right now. | Yes, shortly. |
The rule of thumb: 429, 500 and 503 are worth retrying with backoff; anything in the 4xx range except 429 means the request itself has to change.
Why a job fails after it starts
| error.code | retryable | What happened |
|---|---|---|
content_policy | no | The model refused the prompt or media, or withheld its result. reason says why: sexual, minors, copyright, real_person or other. |
invalid_input | no | A reference could not be read, or the model rejected a value. The message says which. |
model_declined | no | The model answered in words instead of making the picture or video. Describe the result itself in the prompt. |
reference_changed | no | A reference file changed after it was checked. Send it again. |
max_cost_exceeded | no | The only route able to run costs more than your cap. |
model_busy | yes | Every route was at capacity. Try again in a minute. |
model_unavailable | yes | No route could take the job right now. |
timeout | yes | No route returned a result in time, or your max_wait_s passed. |
all_routes_failed | yes | Every route failed for reasons on its side. |
Behind the scenes a job that hits a busy or broken route moves on to the next route by itself, so by the time you see model_busy or all_routes_failed every route has already been tried. The attempts field says how many. Waiting a minute or two before trying again is usually enough.
Content refusals
content_policy is the failure people hit most. The model, not the API, decides it, and the reason tells you which rule fired. Common causes are a famous person's name or face in the prompt or a reference picture (real_person), brands and well-known characters (copyright), and anything that reads as involving a minor. Retrying the same request will not help; reword the prompt or change the picture. Models differ in how strict they are, so the same prompt can pass on one and be refused on another.
A retry loop that does the right thing
This Python helper retries what is worth retrying and stops on what is not. Two details matter:
- Resending a submit after a
503or a dropped connection uses the sameIdempotency-Key, so it can never start a second, paid job. - Trying again after a job has failed uses a new key. The same key would just hand back the failed job.
import os
import time
import uuid
import requests
API = "https://api.revana.si/v1"
AUTH = {"Authorization": f"Bearer {os.environ['REVANA_API_KEY']}"}
RETRY_HTTP = {429, 500, 503}
class GenerationError(Exception):
pass
def submit(body):
"""POST a job. Retries 429, 500 and 503 with the same Idempotency-Key, so a retry never starts a second job."""
headers = {**AUTH, "Idempotency-Key": str(uuid.uuid4())}
for attempt in range(6):
r = requests.post(f"{API}/generations", headers=headers, json=body, timeout=60)
if r.ok:
return r.json()
if r.status_code in RETRY_HTTP:
time.sleep(float(r.headers.get("Retry-After", 2 ** attempt)))
continue
err = r.json()["error"] # 400, 401, 402, 403, 409, 413, 422: fix the request, do not retry
raise GenerationError(f"{r.status_code} {err['code']}: {err['message']}")
raise GenerationError("still busy after 6 tries")
def wait(job):
while job["status"] in ("queued", "running"):
time.sleep(5)
job = requests.get(f"{API}/generations/{job['id']}", headers=AUTH).json()
return job
def generate(body, tries=3):
for n in range(tries):
job = wait(submit(body)) # each call uses a new key: a failed job is final
if job["status"] == "succeeded":
return job
err = job["error"]
if not err["retryable"]:
raise GenerationError(f"{err['code']}: {err['message']}")
time.sleep(60 * (n + 1)) # model_busy, timeout and the like: wait, then try again
raise GenerationError(f"gave up after {tries} tries: {err['code']}")
job = generate({
"model": "seedance-2.0-fast",
"input": {"prompt": "Waves roll onto black sand at sunset.", "resolution": "480p", "duration": 5},
"max_cost_usd": 0.3,
})
print(job["output"]["urls"][0], job["cost_usd"])The sample job is a 5 second 480p Seedance 2.0 Fast clip, which costs $0.25 if it succeeds and nothing if it fails. The max_cost_usd cap stops a wrong setting from costing more than expected.
Avoiding errors in the first place
- Read the options before you send.
GET /v1/modelsand the models page list every model's sizes, lengths and reference limits. A value outside them is refused at once with422 invalid_input. - Upload files instead of embedding them.
POST /v1/uploadstakes pictures up to 30 MB, clips up to 50 MB and sound up to 15 MB and returns a link valid for 14 days, which keeps the request body small. - Check your balance in code.
GET /v1/balancebefore a large batch saves a run of402errors. Credit packs are on the pricing page.
For ways to cut what successful jobs cost, see how to cut AI video API costs. The full error reference is in the docs; get an API key to try it.
Frequently asked questions
Am I charged when an AI video generation fails?
No. The price is held when you submit and released if the job fails, whatever the reason.
Which errors should I retry?
On submit, retry 429, 500 and 503 after the Retry-After delay. On a failed job, retry when error.retryable is true: model_busy, model_unavailable, timeout and all_routes_failed.
What does content_policy mean?
The model refused the prompt or media, or withheld what it made. The reason field says which rule: sexual, minors, copyright, real_person or other. Change the request rather than retrying it.
How do I retry without paying twice?
Send an Idempotency-Key header and reuse it when you resend after a timeout or a 503. You get the same generation back, not a second one.
What does 422 invalid_input mean?
The model does not take a value you sent, such as a length, size or number of reference pictures. The message names the field and what it takes.
Build on an API that never bills a failure
One API key for every video and image model, with automatic fallback between routes. Prepaid credit from $10, charged only for jobs that succeed.