How to generate video with an API in Python, including the polling loop
Video generation is asynchronous. The endpoint returns a job, not a video, and the interesting part of the integration is everything after that.
Submit
import os, requests
BASE = "https://genace.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['GENACE_API_KEY']}"}
resp = requests.post(
f"{BASE}/video/generations",
headers=HEADERS,
json={
"model": "seedance-2.0-fast",
"prompt": "a cyclist on a coastal road at golden hour",
"duration_sec": 5,
},
)
resp.raise_for_status()
job = resp.json()
print(job["job_id"], job["state"], job["credits_held"])
The response is 202 Accepted:
{
"job_id": "abc123",
"state": "queued",
"model": "seedance-2.0-fast",
"created_at": "2026-09-16T10:00:00Z",
"credits_held": 25
}
credits_held tells you what this call reserved before anything rendered.
Worth logging — it is the number that comes back if the job fails.
Poll
The job endpoint takes a wait parameter that blocks server-side for up to N
seconds and returns whatever state it reached. That turns a busy loop into a
handful of long requests:
import time
def wait_for(job_id, timeout=600, chunk=30):
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS,
params={"wait": chunk})
r.raise_for_status()
job = r.json()
if job["state"] in ("succeeded", "failed", "canceled"):
return job
raise TimeoutError(f"{job_id} did not finish in {timeout}s")
wait=0 returns immediately with the current state, which is what you want
for a status endpoint of your own.
The failure case your try/except will miss
This is the part that bites people:
job = wait_for(job_id)
# WRONG — a failed job is a successful HTTP response
url = job["outputs"][0]["url"]
# RIGHT
if job["state"] != "succeeded":
raise RuntimeError(f"{job['state']}: {job.get('error', {}).get('code')}")
url = job["outputs"][0]["url"]
raise_for_status() sees a 200 and is satisfied. The job failed anyway. Code
that only handles exceptions treats this as a finished render and carries on
with no output — which usually surfaces much later as a missing file.
Credits for failed jobs are refunded in full, so a retry costs you one render rather than two. Which errors are worth retrying is covered in job state failed and UPSTREAM_FAIL.
Handle the budget branch explicitly
resp = requests.post(f"{BASE}/video/generations", headers=HEADERS, json=payload)
if resp.status_code == 402:
err = resp.json()["error"]
print(err["message"]) # "Not enough credits: balance is 12, needs 100"
return None
if resp.status_code == 429:
code = resp.json()["error"]["code"]
# RATE_LIMIT_EXCEEDED clears with time; CONCURRENCY_LIMIT_EXCEEDED
# clears when one of your own jobs finishes. Retrying blindly only
# helps in the first case.
...
Both 429s look identical by status code and need opposite responses. Branch on
error.code, not on the status —
RATE_LIMIT_EXCEEDED versus
CONCURRENCY_LIMIT_EXCEEDED.
Switching models is a string
json={"model": "veo3", "prompt": "..."} # no duration_sec — Veo 3 is fixed at 8s
Same endpoint, same auth, same job semantics. Note that Veo 3 rejects
duration_sec entirely; passing one returns
400 INVALID_PARAM naming the field.
Do not poll from inside an agent turn
If this code runs as a tool call in an agent loop, the polling loop is the
wrong shape — it blocks the turn for minutes and fills the context with
nothing. Return the job_id and collect the result elsewhere. The options are
in
calling a video generation API without blocking your agent.
Models covered on this page
Where these facts come from
- codebase: src/app/api/v1/video/generations/route.ts — request and 202 response shape
- keyword check: Bing autocomplete has no validated demand for language-specific generation-API tutorials — this is a developer doc, not a traffic page
- codebase: src/ai/jobs/http.ts — pollUntilTerminal and the wait parameter
- codebase: src/ai/api/errors.ts — error response shape