
Turning a still image into a short video clip — a product shot that slowly pans, a portrait that comes alive, a landscape with drifting clouds — is one API call plus a poll with grok-imagine/image-to-video on hiapi. This guide walks through the exact request: a copy-paste curl version, a complete Python script, the parameters the model actually validates, and the errors you'll hit in practice.
Goal: send one reference image plus an optional motion prompt to POST /v1/tasks, wait for the task to finish, and download an MP4 of 6–30 seconds.
You need two things:
All hiapi models — image, video, anything — go through the same unified task endpoint, so this flow transfers directly to other models.
Create the task:
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-<your-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "grok-imagine/image-to-video",
"input": {
"image_urls": ["https://your-cdn.example.com/product-shot.jpg"],
"prompt": "slow push-in on the subject, soft studio light, subtle parallax",
"duration": 6
}
}'
Three things to get right, because the API validates all of them:
model is the bare id grok-imagine/image-to-video — no vendor prefix, no version suffix.input.image_urls is required and is an array. Leave it out and you get back invalid input: image_urls: missing required field "image_urls".duration is an integer number of seconds between 6 and 30. Send 99 and the API answers invalid input: duration: maximum: got 99, want 30; send a string and you get got string, want integer. Omit it to use the default.The create response is a small JSON envelope; the one field you need is data.taskId. Poll it:
curl -s https://api.hiapi.ai/v1/tasks/<taskId> \
-H "Authorization: Bearer sk-<your-key>"
Read data.status. "success" and "fail" are the terminal states — anything else means the task is still running, so wait a few seconds and poll again. On success the video lives at data.output[0].url.
One important detail: output URLs are signed and carry an expireAt. Download the MP4 to your own storage as soon as the task succeeds; don't hotlink or store the URL for later.
A complete script — create, poll, download:
import time
import requests
API = "https://api.hiapi.ai/v1/tasks"
KEY = "sk-<your-key>"
HEADERS = {"Authorization": f"Bearer {KEY}"}
payload = {
"model": "grok-imagine/image-to-video",
"input": {
"image_urls": ["https://your-cdn.example.com/product-shot.jpg"],
"prompt": "slow push-in on the subject, soft studio light, subtle parallax",
"duration": 6,
},
}
resp = requests.post(API, json=payload, headers=HEADERS, timeout=60)
resp.raise_for_status()
task_id = resp.json()["data"]["taskId"]
print("task created:", task_id)
deadline = time.time() + 600 # video tasks can take a few minutes
while time.time() < deadline:
task = requests.get(f"{API}/{task_id}", headers=HEADERS, timeout=30).json()["data"]
status = task.get("status")
if status == "success":
video_url = task["output"][0]["url"] # signed URL with expireAt
clip = requests.get(video_url, timeout=120).content
with open("grok-imagine-clip.mp4", "wb") as f:
f.write(clip)
print("saved grok-imagine-clip.mp4")
break
if status == "fail":
err = task.get("error") or {}
raise RuntimeError(f"task failed: {err.get('code')}: {err.get('message')}")
time.sleep(5)
else:
raise TimeoutError("task did not finish within 10 minutes")
Prompting tip for image-to-video: the reference image already defines what is in the frame, so spend the prompt on motion only — camera moves, lighting shifts, what animates. Prompts that re-describe the scene tend to fight the reference instead of animating it.
Callbacks instead of polling. For batch workloads, polling N tasks every few seconds gets noisy. Add a top-level callback object to the create request and hiapi will POST the terminal task state to your endpoint:
{
"model": "grok-imagine/image-to-video",
"input": { "image_urls": ["https://your-cdn.example.com/product-shot.jpg"] },
"callback": { "url": "https://your-server.example.com/hooks/hiapi", "when": "final" }
}
Two validated constraints: callback.url must be an http(s) URL, and callback.when only supports "final" (you get one call at the terminal state, not progress events). Treat delivery as at-least-once: key your handler on taskId and make it idempotent, and keep a slow polling loop as a fallback for the rare missed callback.
Errors you'll actually see:
| Response | Meaning | What to do |
|---|---|---|
401 with "code": "permission_denied" | Key is invalid or can't use this model | Check the key in the dashboard; the error includes a request_id for support |
400 INVALID_REQUEST | Input failed validation | The message names the exact field, e.g. duration: maximum: got 99, want 30 — fix and resend |
404 task not found | Wrong or foreign taskId on GET | Check the id you stored from data.taskId |
503 nsfw_moderation_unavailable | The content-moderation layer (which runs at create time) is momentarily unavailable | Transient — retry with backoff |
task status: "fail" | Generation itself failed | Read data.error.code / message; failed tasks are safe to resubmit |
Retries are cheap before create, careful after. A 400 or 503 at create time means no task exists yet — retry freely. Once you have a taskId, don't blind-resubmit on timeouts; poll the id you have first, otherwise you can end up paying for duplicate generations.
How long can the generated video be?
duration accepts integers from 6 to 30 (seconds). Values outside that range are rejected at create time with a 400 that names the limit.
Can I upload the reference image directly in the request?
No — image_urls takes URLs. Host the image anywhere that serves public HTTPS (CDN, object storage, presigned URL) and pass that URL.
Do I have to poll, or can I get pushed a result?
Both work. Polling GET /v1/tasks/<taskId> is simplest for scripts; for production batches add callback: {"url": ..., "when": "final"} and receive one POST when the task reaches a terminal state.
How long is the output URL valid?
It's a signed URL with an expireAt timestamp. Download the file immediately after status turns "success" and store it yourself.
What does it cost? Video models are priced per generation depending on settings — see the current numbers on the pricing page.
Can I generate a video from text alone, without a reference image?
Yes — use the sibling model grok-imagine/text-to-video. The request shape is the same minus image_urls.