First-last frame control, async task polling, and callbacks — with copy-pasteable curl and Python.

Turn a still image into a short clip with hiapi's seedance-2.5/image-to-video model — first-last frame control, async task polling, and callbacks, with copy-pasteable curl and Python.
sk-...) from the API Keys dashboard.requests installed (pip install requests).Every generation model on hiapi runs through one unified endpoint, POST /v1/tasks. seedance-2.5/image-to-video is called exactly like every other model on the platform — same auth header, same async task lifecycle, only model and input change. Note that seedance-2.5/image-to-video (with the /image-to-video suffix) is the full, correct model id for this model family — it's not an optional modality tag you can drop.
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-<your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.5/image-to-video",
"input": {
"prompt": "the boat drifts forward slowly, camera holds steady",
"first_frame_url": "https://your-cdn.example.com/start-frame.jpg",
"duration": 5,
"resolution": "720p"
}
}'
A successful call returns a task id immediately — generation itself happens asynchronously:
{"code":200,"data":{"taskId":"tk-hiapi-01XXXXXXXXXXXXXXXXXXXXXXXX"},"message":"success"}
curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-01XXXXXXXXXXXXXXXXXXXXXXXX \
-H "Authorization: Bearer sk-<your-api-key>"
While the clip is rendering, status is "handling". Once it finishes:
{
"code": 200,
"data": {
"taskId": "tk-hiapi-01XXXXXXXXXXXXXXXXXXXXXXXX",
"model": "seedance-2.5/image-to-video",
"status": "success",
"storage": "temp",
"created": 1786327257,
"completed": 1786327396,
"output": [
{"artifactId": "72582", "type": "video", "url": "https://temp.hiapi.ai/.../result.mp4", "expireAt": 1786932195}
]
},
"message": "success"
}
output[0].url is a temporary, expiring link — expireAt is a Unix timestamp. Download or re-host the clip right away; don't store the hot link.
import time
import requests
API_KEY = "sk-<your-api-key>"
BASE = "https://api.hiapi.ai/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def create_task(prompt, first_frame_url, duration=5, resolution="720p", last_frame_url=None):
payload = {
"model": "seedance-2.5/image-to-video",
"input": {
"prompt": prompt,
"first_frame_url": first_frame_url,
"duration": duration,
"resolution": resolution,
},
}
if last_frame_url:
payload["input"]["last_frame_url"] = last_frame_url
resp = requests.post(f"{BASE}/tasks", headers=HEADERS, json=payload, timeout=30)
resp.raise_for_status()
return resp.json()["data"]["taskId"]
def wait_for_result(task_id, interval=5, timeout=600):
deadline = time.time() + timeout
while time.time() < deadline:
resp = requests.get(f"{BASE}/tasks/{task_id}", headers=HEADERS, timeout=30)
resp.raise_for_status()
data = resp.json()["data"]
if data["status"] == "success":
return data["output"][0]["url"]
if data["status"] == "failed":
raise RuntimeError(f"task {task_id} failed: {data}")
time.sleep(interval)
raise TimeoutError(f"task {task_id} did not finish in {timeout}s")
task_id = create_task(
prompt="the boat drifts forward slowly, camera holds steady",
first_frame_url="https://your-cdn.example.com/start-frame.jpg",
)
video_url = wait_for_result(task_id)
print(video_url)
Add last_frame_url alongside first_frame_url to pin down both ends of the clip — hiapi interpolates the motion in between:
{
"model": "seedance-2.5/image-to-video",
"input": {
"prompt": "smooth dolly-in, soft daylight",
"first_frame_url": "https://your-cdn.example.com/start-frame.jpg",
"last_frame_url": "https://your-cdn.example.com/end-frame.jpg",
"duration": 6,
"resolution": "720p"
}
}
Two things worth knowing here, both confirmed against the live schema:
last_frame_url only works together with first_frame_url — you can't specify only the last frame.aspect_ratio on this model only accepts the value "adaptive". Unlike text-to-video models where you pick "16:9" or "9:16" explicitly, an image-to-video clip always inherits its aspect ratio from first_frame_url and can't be overridden — crop or pad your source image to the ratio you want before calling the API.For anything beyond a one-off script, don't poll — register a callback and let hiapi push the result to you:
{
"model": "seedance-2.5/image-to-video",
"input": { "...": "..." },
"callback": { "url": "https://your-server.example.com/hiapi/callback", "when": "final" }
}
callback sits next to input, not inside it. when currently only accepts "final" — you'll get exactly one POST when the task reaches a terminal state (success or failed), not incremental progress events.
duration: integer, 4–30 seconds.resolution: "480p" or "720p" only.Values outside these ranges are rejected before any generation starts, so validate client-side and you'll never pay for a request that was going to fail anyway.
The task API doesn't take a client-supplied idempotency key — every POST /v1/tasks call creates a new task and, for a paid model, a new charge. If a request times out on your end, check whether you already captured a taskId from that attempt and poll or wait on the callback for it instead of blindly re-submitting the same request.
An invalid or under-permissioned key fails synchronously, before any task is created:
HTTP 401
{"error":{"code":"permission_denied","message":"This API key cannot use the selected model. Please check permissions or use another key. If the issue persists, contact support with request ID: <id>","request_id":"<id>","type":"hiapi_error"}}
Treat permission_denied as: wrong or revoked key, or a key scoped without access to seedance-2.5/image-to-video — check both in the API Keys dashboard before assuming your request body is wrong.
Do I need both first_frame_url and last_frame_url?
No. first_frame_url alone gives you standard image-to-video. Add last_frame_url only when you also want to pin the ending frame — it requires first_frame_url to be set.
Can I set a 16:9 or 9:16 aspect ratio?
No — aspect_ratio only accepts "adaptive" for this model. Prepare your first_frame_url image at the aspect ratio you want the output in.
Why did I get a 401 with a valid-looking key?
permission_denied means the key exists but isn't authorized for this specific model. Check your key's model access in the dashboard, not just whether the key itself is valid.
How long can a clip be?
duration accepts any integer from 4 to 30 seconds.
Is there a synchronous version of this endpoint?
No — every model on hiapi, including this one, runs through the same async POST /v1/tasks → poll or callback pattern. There's no synchronous image-to-video call.
What happens to the output URL if I don't download it right away?
output[0].url expires — expireAt in the response is a Unix timestamp. Download the clip or re-host it to your own storage as soon as the task completes.