Asynchronous Workflow and Polling
On this page
Updated November 2, 2026. Based on the xAI Video API reference: the URL is found in
video.url(notvideo_url), a generation has only three statuses (pending,done,failed, the last accompanied by an error code), and rate limits are expressed in requests per second based on the team tier. Sources: Video Generation · Videos Reference · Rate limits.
Understand the video lifecycle
All video operations in the xAI API are asynchronous, and this characteristic shapes your application architecture more than you might expect at first. A chat call or image generation returns a result immediately; video generation, however, requires significant processing time and returns only an acknowledgment. It’s up to you to implement tracking: periodically query the task status, detect completion, and retrieve the file. The complete cycle consists of four steps.
Submit request
POST to the generation, edit, or extend endpoint. Immediate response with a request_id.
Poll status
GET /v1/videos/{request_id} at regular intervals. progress field from 0 to 100.
Retrieve result
When status is done (progress = 100), the video URL is available.
Download immediately
The video URL is temporary. Download it as soon as possible.
Check generation status
Tracking involves a simple GET request to the status endpoint, using the identifier received upon submission.
curl "https://api.x.ai/v1/videos/vid_abc123def456" \
-H "Authorization: Bearer $XAI_API_KEY"
The response exposes the following useful information:
- status : the current generation state
- progress : an integer from 0 to 100 indicating progress
- video.url : the video URL, inside the
videoobject present when status isdone, with its duration (video.duration) - video.respect_moderation : a boolean indicating whether content complies with moderation policy
- error : in case of failure, a code (
invalid_argument,permission_denied,failed_precondition,service_unavailable, orinternal_error) and a message - usage.cost_in_usd_ticks : the request cost, in ten-billionths of a dollar
The progress field deserves a note: it doesn’t drive your control logic, which should always rely on status, but it’s gold for a user interface. Displaying “62%” rather than a static spinner completely changes the perception of a wait that can last several minutes.
Three states, three actions
| Status | Progress | Meaning |
|---|---|---|
pending | 0-99 | Generation in progress |
done | 100 | Complete, URL available |
failed | absent | Generation failed |
As long as the status is pending, generation continues: you simply keep querying at regular intervals, relaying progress to your interface. When it switches to done, the video is ready, and the only thing to do is read video.url and download the file immediately—these URLs are temporary and the documentation doesn’t specify their lifespan, which prohibits storing them in a database for later use.
Finally, the failed status indicates a production failure, and the error field tells you which one. The distinction matters for what follows: service_unavailable (service temporarily overloaded) or internal_error justify retrying later, while invalid_argument or permission_denied—rejected prompt, invalid source image or video—require correcting the request first. The Python SDK, for its part, also signals via an error a deferred request that expired before being retrieved.
Implement polling
The synchronous version fits in a single function. It loops until timeout, exits early on failure by raising its code, and returns the URL as soon as the video is ready.
import httpx
import time
def attendre_video(request_id, api_key, timeout=600, intervalle=5):
"""Wait for video generation completion."""
headers = {"Authorization": f"Bearer {api_key}"}
debut = time.time()
while time.time() - debut < timeout:
resp = httpx.get(
f"https://api.x.ai/v1/videos/{request_id}",
headers=headers
).json()
status = resp.get("status")
progress = resp.get("progress", 0)
if status == "done":
return resp["video"]["url"]
elif status == "failed":
erreur = resp.get("error", {})
raise RuntimeError(f"Generation failed [{erreur.get('code')}] : {erreur.get('message')}")
print(f"In progress... {progress}%")
time.sleep(intervalle)
raise TimeoutError(f"Timeout after {timeout} seconds")
In a service handling multiple requests at once, this blocking version becomes a bottleneck: each wait immobilizes a thread for minutes. The asynchronous transposition frees the event loop between queries and lets you track dozens of generations in parallel.
import httpx
import asyncio
async def attendre_video_async(request_id, api_key, timeout=600, intervalle=5):
"""Asynchronous version of polling."""
headers = {"Authorization": f"Bearer {api_key}"}
async with httpx.AsyncClient() as client:
debut = asyncio.get_event_loop().time()
while asyncio.get_event_loop().time() - debut < timeout:
resp = await client.get(
f"https://api.x.ai/v1/videos/{request_id}",
headers=headers
)
data = resp.json()
if data["status"] == "done":
return data["video"]["url"]
elif data["status"] == "failed":
raise RuntimeError(f"Generation failed : {data.get('error')}")
await asyncio.sleep(intervalle)
raise TimeoutError("Timeout")
Set the polling interval
Choosing the interval balances responsiveness against wasted requests. Five seconds is a good compromise in most cases. For long generations—ten to fifteen seconds of video—ten to fifteen seconds between polls is plenty and reduces traffic. Conversely, an interactive application where the user watches a progress bar justifies dropping to two or three seconds. Below one second, however, you won’t speed anything up: generation proceeds at its own pace, and you only multiply requests for nothing.
Key takeaways
- All video operations are asynchronous: POST to submit, GET to retrieve
- Three possible statuses: pending, done, failed (with an error code)
- The
progressfield (0-100) lets you track advancement - Implement a timeout and a polling interval suited to your use case
- Video URLs are temporary: download immediately