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.

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.
| Step | xAI | Sume |
|---|---|---|
| Submit | Async request | POST /v1/videos, 202 with polling_url |
| Wait | Poll, 10-minute default timeout | GET polling_url, docs suggest a moderate interval such as 30 s |
| Done | Result with video URL | status completed, unsigned_urls[0] |
| Failure | Error categories | status failed with an error field |
| Aspect ratio | Ignored for image input | Rejected, 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
- Move a Vidu Q4 job to Sume: six fields to check before you submit
Vidu Q4 Preview's image-to-video fields (duration, resolution, start image, audio) mapped to Sume's /v1/videos names, with a Python filter for rows that fit.
- Music prompt rejected by policy: rewrite only the flagged part
If Sume Music rejects a prompt on policy, change the flagged content but keep the musical brief. The docs say not to flatten it to a generic bed; here is how.
- n=10 on the Sume Image API: docs say 10, catalog says 4 (Grok 1)
The Image API docs say n goes up to 10, but every catalog model caps lower: 4 for most, 1 for Grok Image, 1 or 4 for Soul. The 400 you get and how to batch.
- Next.js Oct 14 security update: redeploy, then test your Sume webhook
Next.js will ship an out-of-band security update on Oct 14. Prepare your Sume webhook receiver now, then prove it still verifies after you redeploy.
Written by Sume