Grok Imagine Video 1.5 in TypeScript: first frame, poll, download
A Node 18+ script that animates one still with grok-imagine-video-1.5 on POST /v1/videos, polls the job, and saves the MP4. Fields Sume refuses are listed.

To animate a still with Grok Imagine from TypeScript, POST to https://api.sume.com/v1/videos with model: "grok-imagine-video-1.5" and one frame_images entry whose frame_type is first_frame, then poll GET /v1/videos/{id} until status is completed. The script below does that with Node 18 fetch and no dependencies.
The script
Save it as grok.mjs and run SUME_API_KEY=... node grok.mjs. The image URL must be public HTTPS. The Idempotency-Key header makes a retried submit return the original job, so a network timeout does not bill twice.
const base = "https://api.sume.com/v1";
const headers = {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
};
const res = await fetch(`${base}/videos`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": "grok-demo-001" },
body: JSON.stringify({
model: "grok-imagine-video-1.5",
prompt: "Slow push-in, steam rising from the cup, soft morning light",
duration: 6,
resolution: "720p",
frame_images: [{
type: "image_url",
image_url: { url: "https://example.com/cup.jpg" },
frame_type: "first_frame",
}],
}),
});
if (res.status !== 202) throw new Error(`submit ${res.status}: ${await res.text()}`);
const { id } = await res.json();
for (;;) {
await new Promise((r) => setTimeout(r, 10_000));
const job = await (await fetch(`${base}/videos/${id}`, { headers })).json();
if (job.status === "failed" || job.status === "cancelled") throw new Error(job.error ?? job.status);
if (job.status === "completed") break;
}
const mp4 = await fetch(`${base}/videos/${id}/content?index=0`, { headers });
const { writeFile } = await import("node:fs/promises");
await writeFile("cup.mp4", Buffer.from(await mp4.arrayBuffer()));
console.log("saved cup.mp4");What the Grok row refuses
The Grok row on Sume is narrower than the model on xAI's site. The request body must leave out these fields or the API answers 400:
- No text-only request: a start image is required.
- No
last_frameentry and noinput_references. - No
aspect_ratio: the output follows the source image's ratio. - No
size,seedor non-emptyprovider.options(every video model refuses these on/v1/videos). durationis 4 to 15 seconds andresolutionis 480p or 720p.
Polling or a webhook
Docs suggest a moderate polling interval, around 30 seconds, because a video job can take from half a minute to several minutes. A 10-second loop is fine for a short clip, but use a longer one for batches. If you would rather not poll, send an HTTPS callback_url and verify the signature on the webhook.
Sources
Related posts
More in Developers
- AI image edit changed my crop: aspect_ratio auto vs omitted on Sume
On edit calls, aspect_ratio auto matches the reference; leaving the field out is not the same thing. Ideogram 4.5 keeps the source shape when you omit it.
- Stream partial images from the Sume Image API? 400, use async events
stream: true returns 400 streaming_not_supported and every catalog row reports supports_streaming false. For progress, submit async and read job events.
- 402 insufficient_credits: read required and available credits
The Sume 402 body gives details.required_credits and details.available_credits, whole credits. Compute the shortfall in Python and stop a batch cleanly.
- k6 2.3 --once and --scenario: smoke-test the Sume API without spend
Use k6 --once and --scenario for a Sume API smoke test that costs nothing: models, a deliberate 400, and a paced balance read that counts 429s.
Written by Sume