Move a Grok Imagine polling loop to Sume's /v1/videos in Python

Port an xAI Grok Imagine video poll loop to Sume: submit, idempotent retry, poll states, download and read usage.cost. A runnable Python script under 30 lines.

5 min readSume
All posts

To move a Grok Imagine polling loop to Sume, send one POST /v1/videos with model grok-imagine-video-1.5 and a first-frame image, then poll the returned polling_url until the status is completed. The script below does it in under 30 lines with an Idempotency-Key so a retry cannot bill twice.

It assumes your Sume key is in the environment variable SUME_API_KEY.

What stays and what changes

xAI's page (read 2026-10-10) describes an async flow with polling and a 10-minute default timeout, a 1 to 15 second duration with a default of 8, and 480p as the default resolution. The same shape carries over to Sume: submit, poll, download.

What changes is the vocabulary. Sume's Video Generation docs list the job statuses pending, in_progress, completed, failed and cancelled, and the finished job carries unsigned_urls for the download and usage.cost for the billed amount.

Polling loop mapping (xAI read 2026-10-10)
StepxAISume
SubmitAsync requestPOST /v1/videos, 202 with polling_url
WaitPoll, 10-minute default timeoutGET polling_url, docs suggest a moderate interval such as 30 s
DoneResult with video URLstatus completed, unsigned_urls[0]
FailureError categoriesstatus failed with an error field
Aspect ratioIgnored for image inputRejected, omit it

The script

It sends the image as a first_frame entry in frame_images, which is how the docs describe image-to-video. It gives up after the deadline you set and raises on failed or cancelled. Change timeout to match your own tolerance, not xAI's.

import os, time, uuid, requests

API = "https://api.sume.com"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def run(image_url, prompt, seconds=6, timeout=600):
    body = {"model": "grok-imagine-video-1.5", "prompt": prompt,
            "duration": seconds, "resolution": "720p",
            "frame_images": [{"type": "image_url",
                              "image_url": {"url": image_url},
                              "frame_type": "first_frame"}]}
    r = requests.post(f"{API}/v1/videos", json=body, timeout=60,
                      headers={**H, "Idempotency-Key": str(uuid.uuid4())})
    r.raise_for_status()
    job = r.json()
    deadline = time.time() + timeout
    while time.time() < deadline:
        time.sleep(30)
        s = requests.get(job["polling_url"], headers=H, timeout=60).json()
        if s["status"] == "completed":
            v = requests.get(s["unsigned_urls"][0], headers=H, timeout=300)
            v.raise_for_status()
            open("out.mp4", "wb").write(v.content)
            return s["usage"]["cost"]
        if s["status"] in ("failed", "cancelled"):
            raise RuntimeError(s.get("error") or s["status"])
    raise TimeoutError("still running after the deadline")

print(run("https://example.com/still.png", "slow push-in, soft light"))

Retries and billing

A random UUID per call protects only a single submit. If you retry after a network error, reuse the same key: the docs say a replay returns the original job. A new key creates a new job and a new reservation.

On cost, a 6-second Grok job bills $0.075 (list $0.01 per second times 1.25), and usage.cost should show that figure. If the balance cannot cover the reservation you get a 402 insufficient_credits, and a full workspace queue returns 429 queue_full; the Errors and rate limits page says to back off and keep the same Idempotency-Key.

Replace the example image URL with a public HTTPS image, since Sume fetches it server-side.

Webhooks instead of polling

If you would rather not poll at all, send callback_url (HTTPS only) in the submit body. Sume signs the raw JSON body and sends x-sume-webhook-timestamp and x-sume-webhook-signature, so your receiver must verify the signature with a non-empty secret and reject anything else. The Video Generation docs point to the webhooks guide for the exact steps.

A good pattern is to keep the polling loop as a fallback with a long interval, and let the webhook do the common case. Either way, store the job id returned at submit so you can re-fetch the result later from GET /v1/videos/{id}.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume