Skip to main content
Lesson 10 of 12 · Create Videos with Grok

Asynchronous Workflow and Polling

Lesson 10 of 12 Updated November 2, 2026 Intermediate
On this page
  1. Understand the video lifecycle
  2. Check generation status
  3. Three states, three actions
  4. Implement polling
  5. Set the polling interval
  6. Key takeaways

Updated November 2, 2026. Based on the xAI Video API reference: the URL is found in video.url (not video_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.

1

Submit request

POST to the generation, edit, or extend endpoint. Immediate response with a request_id.

2

Poll status

GET /v1/videos/{request_id} at regular intervals. progress field from 0 to 100.

3

Retrieve result

When status is done (progress = 100), the video URL is available.

4

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 video object present when status is done, 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, or internal_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

StatusProgressMeaning
pending0-99Generation in progress
done100Complete, URL available
failedabsentGeneration 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 progress field (0-100) lets you track advancement
  • Implement a timeout and a polling interval suited to your use case
  • Video URLs are temporary: download immediately