HiAPI
Blog

Tutorial

How to Call happyhorse-1.1/image-to-video on the hiapi API: curl and Python Guide

Nov 3, 2026 9 min read
hiapi.ai 博客封面图:错位堆叠的骨白纸页与一片锈红内页,扁平几何风格。

What this endpoint does and when to use it

happyhorse-1.1/image-to-video turns a single still image into a short video. You send the endpoint a first-frame image, and the model animates forward from that frame. The practical consequence is continuity: the subject, framing, and look of the starting image carry into the generated clip. That property is what we mean by identity preservation here — the model is not inventing a fresh scene from a text prompt alone, it is continuing a scene you have already authored as an image.

This makes the endpoint a good fit when the visual identity matters more than raw improvisation:

  • You already have a reference still — a portrait, a product shot, a character sheet — and want motion that stays faithful to it.
  • You are producing short clips in a pipeline where every other asset was designed as an image first.
  • You need an animatic or a motion preview for a storyboard frame.

It is a weaker fit when you want the model to freely reinterpret a scene from scratch, or when you have no still to anchor the output. In those cases, an image-first workflow usually means generating the anchor image separately, then handing that result to this endpoint.

The call itself is asynchronous: you submit a job, then poll until the video is ready. The rest of this guide walks through that lifecycle end to end.

Before you send a request

Two things need to be in place before your first call.

An API key

All calls go through the hiapi platform with an API key attached as a bearer credential. Keep the key in an environment variable rather than in source control, so the same code works across local development and CI:

export HIAPI_API_KEY="your_key_here"

For Python, read it at runtime with os.environ["HIAPI_API_KEY"]. Never log the raw header value.

A first-frame image you can reference

The endpoint needs an image to start from. Decide up front how you will hand it over — as a URL the platform can fetch, or as an uploaded/base64-encoded asset, depending on what your account and the endpoint documentation support. The important thing is that the reference is stable and reachable at submission time. A signed URL that expires in five minutes is a poor choice if the job may queue.

It also helps to know your intended output aspect and duration before you call, because the first frame usually constrains them. Preparing that decision up front saves a re-submission later.

Once the key is exported and the image reference is settled, you are ready to submit.

Building the first-frame input

The first frame is the anchor for everything the model generates after it. Practically, you should treat it as the contract for identity: whatever is clearly visible in that image — face, clothing, logo, color palette, camera angle — is what the model has to work from. If a detail is ambiguous or occluded in the still, the output will be ambiguous too.

A few preparation guidelines that hold regardless of the exact model parameters:

  • Choose a clean, well-lit frame. Compression artifacts and heavy noise give the model less to preserve. A sharp, evenly lit image is a better anchor than a dramatic but muddy one.
  • Put the subject where you want the motion to start. Cropping and framing in the still is cheaper than hoping the model reframes.
  • Keep the aspect ratio of your intended output. If the final video is landscape, prepare a landscape anchor; padding later does not restore lost framing.
  • Avoid contradictory cues. A still that reads one way but implicitly suggests off-frame action gives the model an unstable starting point.

Do not assume model-specific knobs beyond what the endpoint documents. The safest approach is to make the image itself carry the intent, then let the request fields handle only what they explicitly cover.

A working request with curl

Submitting a job is a single POST. The structure is: authenticated request to the endpoint, JSON body carrying the model identifier and the image input.

curl -sS -X POST "https://api.hiapi.ai/v1/video/generations" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "happyhorse-1.1/image-to-video",
    "input": {
      "image": "https://your-bucket.example.com/first-frame.jpg"
    }
  }'

Step by step:

  1. Endpoint. The POST target is the video generation route on the hiapi API. Keep it in a variable if you script against multiple environments.
  2. Auth header. Authorization: Bearer $HIAPI_API_KEY — the shell expands the environment variable you exported earlier.
  3. Content type. application/json, because the body is JSON.
  4. Body. model selects happyhorse-1.1/image-to-video; input.image is your first-frame reference. Consult the endpoint reference for the exact accepted image field names and shapes in your account.

A successful response is not the video — it is a job handle. Expect an identifier and a status field, typically something like queued or processing. Capture the ID; you will use it for polling in the next steps. Run the command once with the response printed so you can see the shape before you start scripting.

The same request in Python

The Python version is a direct port, not a new integration. Same route, same headers, same JSON body.

import os
import requests

API_KEY = os.environ["HIAPI_API_KEY"]
URL = "https://api.hiapi.ai/v1/video/generations"

payload = {
    "model": "happyhorse-1.1/image-to-video",
    "input": {
        "image": "https://your-bucket.example.com/first-frame.jpg"
    },
}

resp = requests.post(
    URL,
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json=payload,
    timeout=60,
)
resp.raise_for_status()
job = resp.json()
print(job)

Mapping notes:

  • requests with json=payload serializes the dict exactly as the curl -d string did — do not also set Content-Type manually if you use json=; the library handles it. The explicit header above is harmless but redundant.
  • raise_for_status() turns HTTP-level failures into exceptions early, before you start polling a job that does not exist.
  • The timeout is a client-side guard on the submission call only. Generation runs server-side and is not bound by it.

Keep the payload in a variable and reuse it. When you later change the model or the image, you should only edit one dict, not two parallel implementations.

Handling the async job and polling

Image-to-video generation takes longer than a request/response cycle. The API therefore returns immediately with a job reference and expects you to check back. The lifecycle is: submit, receive ID, poll status, fetch result.

Extract the job ID from the submission response — the field name depends on the response schema documented for the endpoint, so read it once and hardcode the key lookup, not the value:

job_id = job["id"]  # check the endpoint docs for the exact field

Then poll on an interval:

import time

poll_url = f"https://api.hiapi.ai/v1/video/generations/{job_id}"

while True:
    status = requests.get(
        poll_url,
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=30,
    ).json()

    if status["status"] == "succeeded":
        break
    if status["status"] == "failed":
        raise RuntimeError(status)
    time.sleep(5)

Rules that keep polling sane:

  • Poll on a fixed interval, not in a tight loop. A few seconds between checks is enough for a job of this size; hammering the endpoint adds load without producing the result sooner.
  • Cap the total wait. A while True with no ceiling is a hang waiting to happen. Track elapsed time and fail loudly after a reasonable budget.
  • Handle both terminal states. Success and failure are both answers. Treat unknown status strings as non-terminal but log them so you notice schema changes.
  • Do not poll from many parallel workers for one job. One poller per job, or coordinate through a queue.

Once the status is terminal, fall through to downloading the result.

Downloading and storing the output

The finished video is exposed as an asset on the completed job response — typically a URL, sometimes with an expiry. Your first move after a successful poll is to fetch that reference and copy the bytes into storage you control.

video_url = status["output"]["video_url"]  # inspect the real response shape

with requests.get(video_url, stream=True, timeout=120) as r:
    r.raise_for_status()
    with open(f"out/{job_id}.mp4", "wb") as f:
        for chunk in r.iter_content(chunk_size=1 << 20):
            f.write(chunk)

Storage guidance that applies to any pipeline:

  • Copy, do not link, for durable artifacts. Signed or time-limited URLs are transport, not storage. Persist the file, then store your own object key alongside the job ID in your database.
  • Stream large files. iter_content keeps memory flat; loading a whole video into a variable ties your memory ceiling to asset size.
  • Record provenance. Save job_id, the input image reference, and the model identifier with the asset. When you need to reproduce a clip, that record is the recipe.
  • Check the HTTP status before writing. A partial write from an error response is worse than a clean failure, because it looks like a valid file.

With the file on disk or in object storage, the pipeline is closed and you can move to the next frame.

Errors and pitfalls to watch for

Most failures at this endpoint are predictable. The short list:

  • Malformed or unreachable first-frame input. The most common cause of a rejected submission. Check that the image URL resolves from the public internet, the format is one the endpoint accepts, and the payload nests the image field correctly. Reproduce with the curl call to isolate auth problems from payload problems.
  • Polling before the job exists. Reading the ID from the wrong field, or polling a route that does not match the submission response, yields 404s that look like job failures. Print the submission response once and pin the key lookup against it.
  • Unbounded waiting. No timeout means a stuck job hangs a worker indefinitely. Set a budget, fail, and alert.
  • Lost asset links. Generation succeeds, but the URL is consumed once and never copied. Store the bytes at fetch time and keep your own key; do not rely on the returned link as long-term storage.
  • Auth leakage. Keys pasted into notebooks and committed by accident are a recurring problem. Load from the environment, rotate on suspicion.
  • Assuming parameters the endpoint does not document. If you need a knob that is not listed, it is not there. Encode intent in the first-frame image and the documented fields only.

Handle these and the async flow is boring — which is what you want in a production pipeline. For endpoint-specific field names and accepted values, keep the endpoint reference open while you build.

Related articles

View all articles →

Start building with persistent AI artifacts

Get your API key and call image, video and audio models through one task-based API.

Read the API docs