One task API, three endpoints: validated parameters, runnable examples, and production patterns

If you are searching for how to use happy horse 1.1, here is the short version: happyhorse-1.1 is not one model endpoint — it is three, all served through hiapi's unified async task API:
| Endpoint (model id) | What it does | Required input |
|---|---|---|
happyhorse-1.1/text-to-video | Generate video from a text prompt | prompt |
happyhorse-1.1/image-to-video | Animate a still image | image_urls |
happyhorse-1.1/reference-to-video | Generate video guided by reference images + a prompt | prompt, reference_image |
All three share the same lifecycle: POST /v1/tasks to submit → get a taskId → poll GET /v1/tasks/<id> (or receive a callback) → download output[0].url. Every parameter table and error message below was validated against the live API — nothing is copied from guesswork.
You need one thing: a hiapi API key. Grab it from your hiapi dashboard — it looks like sk-... and goes in the Authorization: Bearer header. Video generation is billed per second of output; see pricing for current per-second rates on each endpoint.
export HIAPI_KEY="sk-your-key-here"
Submit a task:
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "happyhorse-1.1/text-to-video",
"input": {
"prompt": "A golden retriever puppy runs through a sunlit meadow, slow motion, cinematic",
"aspect_ratio": "16:9",
"resolution": "720p",
"duration": 5
}
}'
The response gives you a task id under data.taskId. Poll it:
curl -s https://api.hiapi.ai/v1/tasks/<taskId> \
-H "Authorization: Bearer $HIAPI_KEY"
When data.status becomes success, the video URL is at data.output[0].url. Download it immediately — output URLs carry an expireAt timestamp and live in temporary storage, so treat them as a pickup window, not permanent hosting.
Here is the same flow as a complete Python script (standard library only):
import json
import time
import urllib.request
API = "https://api.hiapi.ai/v1/tasks"
KEY = "sk-your-key-here"
def api(method, url, payload=None):
req = urllib.request.Request(url, method=method)
req.add_header("Authorization", f"Bearer {KEY}")
req.add_header("Content-Type", "application/json")
data = json.dumps(payload).encode() if payload else None
with urllib.request.urlopen(req, data=data) as resp:
return json.loads(resp.read())
# 1. Submit
task = api("POST", API, {
"model": "happyhorse-1.1/text-to-video",
"input": {
"prompt": "A golden retriever puppy runs through a sunlit meadow, cinematic",
"aspect_ratio": "16:9",
"resolution": "720p",
"duration": 5,
},
})
task_id = task["data"]["taskId"]
print("submitted:", task_id)
# 2. Poll until terminal state
while True:
data = api("GET", f"{API}/{task_id}")["data"]
status = data["status"]
if status == "success":
video_url = data["output"][0]["url"]
break
if status == "fail":
raise RuntimeError(f"task failed: {data.get('error')}")
time.sleep(5)
# 3. Download before the URL expires
urllib.request.urlretrieve(video_url, "output.mp4")
print("saved output.mp4")
The input schemas are strict: unknown fields are rejected with a 400, not silently ignored. Fields like size, seed, negative_prompt, and audio all return additional properties ... not allowed. Here is exactly what each endpoint accepts.
happyhorse-1.1/text-to-video| Field | Required | Type | Allowed values |
|---|---|---|---|
prompt | ✅ | string | your scene description |
aspect_ratio | — | string | 16:9, 9:16, 3:4, 4:3, 4:5, 5:4, 1:1, 9:21, 21:9 |
resolution | — | string | 720p, 1080p |
duration | — | integer | 3–15 (seconds) |
Full walkthrough with prompt tips: happyhorse-1.1 text-to-video tutorial.
happyhorse-1.1/image-to-video| Field | Required | Type | Allowed values |
|---|---|---|---|
image_urls | ✅ | array of strings | exactly 1 publicly reachable image URL (maxItems: 1) |
prompt | — | string | motion/scene guidance for the animation |
resolution | — | string | 720p, 1080p |
duration | — | integer | 3–15 (seconds) |
Two things trip people up here. First, image_urls is an array but currently accepts only one item — pass ["https://..."], not a bare string and not multiple URLs. Second, there is no aspect_ratio on this endpoint (the API rejects it); the output framing follows your source image.
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "happyhorse-1.1/image-to-video",
"input": {
"image_urls": ["https://your-cdn.example.com/product-shot.jpg"],
"prompt": "slow camera push-in, soft studio lighting",
"resolution": "720p",
"duration": 5
}
}'
Deep dive: happyhorse image-to-video tutorial.
happyhorse-1.1/reference-to-video| Field | Required | Type | Allowed values |
|---|---|---|---|
prompt | ✅ | string | scene description |
reference_image | ✅ | array of strings | 1–9 publicly reachable image URLs (maxItems: 9) |
aspect_ratio | — | string | same 9 ratios as text-to-video |
resolution | — | string | 720p, 1080p |
duration | — | integer | 3–15 (seconds) |
This is the "keep my character/product consistent" endpoint: the reference images anchor identity and style while the prompt drives the action. Unlike image-to-video, you get aspect_ratio control back and can pass up to 9 references.
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "happyhorse-1.1/reference-to-video",
"input": {
"prompt": "the character waves at the camera and smiles, city street at dusk",
"reference_image": ["https://your-cdn.example.com/character-front.jpg",
"https://your-cdn.example.com/character-side.jpg"],
"aspect_ratio": "9:16",
"resolution": "720p",
"duration": 5
}
}'
Deep dive: happyhorse-1.1 reference-to-video tutorial.
text-to-video. Fastest path from words to motion.image-to-video. The source image is the first frame's look.reference-to-video. References pin identity; the prompt changes per clip.For anything beyond a script, register a webhook when you create the task — video jobs take a while, and polling from a web request handler is a timeout waiting to happen:
{
"model": "happyhorse-1.1/text-to-video",
"input": { "prompt": "..." },
"callback": { "url": "https://your-app.example.com/hooks/hiapi", "when": "final" }
}
Only "when": "final" is supported — you get exactly one POST when the task reaches a terminal state (any other value returns invalid callback.when: only 'final' is supported). Design your receiver to be idempotent on taskId: store the task id at submit time, and treat a duplicate or already-processed callback as a no-op. Keep a low-frequency polling sweeper as a fallback for callbacks lost to network hiccups.
1. Validation errors (HTTP 400, INVALID_REQUEST). The messages are precise — read them, they name the exact field:
{
"code": 400,
"error_code": "INVALID_REQUEST",
"message": "invalid input: duration: maximum: got 99, want 15; resolution: value must be one of '720p', '1080p'"
}
2. Auth/permission errors (HTTP 401, permission_denied). A bad key, or a key without access to this model:
{
"error": {
"code": "permission_denied",
"message": "This API key cannot use the selected model. ...",
"request_id": "2026..."
}
}
Check the key in your dashboard and include the request_id if you contact support.
3. Generation failures (task status: "fail"). The submit succeeded but generation didn't; the error details are in the task object's error field when you poll or receive the callback. These are the ones worth retrying with backoff — validation and auth errors are not.
Billing is per second of generated video, which makes duration your primary cost lever — a 15-second clip costs 3× a 5-second clip, while resolution matters less. Iterate on prompts at duration: 3 and 720p, then render the final at full length. Current rates are on the pricing page.
Can I combine an image and a text prompt?
Yes — image-to-video accepts an optional prompt alongside the required image_urls, and reference-to-video requires both a prompt and reference_image. Only text-to-video is prompt-only.
How many images can I pass?
image-to-video: exactly one (image_urls is capped at 1 item). reference-to-video: up to nine in reference_image. Both take arrays of publicly reachable URLs, not uploads or base64.
Does happyhorse-1.1 support seed, negative_prompt, or audio parameters?
No. The input schemas are strict and reject all three with a 400 additional properties not allowed error. If you need reproducibility, keep your prompt and parameters identical and version them on your side.
What resolutions and durations are available?
All three endpoints: 720p or 1080p, and 3 to 15 seconds (integer). Defaults apply if you omit them.
Why am I getting 401 permission_denied with a valid-looking key? The key exists but cannot use this model — common with scoped or trial keys. Verify the key's model permissions in the dashboard, or generate a new unrestricted key.
How do I know when my video is ready without polling?
Pass callback: {"url": "...", "when": "final"} at submit time. hiapi POSTs to your URL once, when the task reaches success or fail. Remember the output URL expires — download the file in your callback handler, don't hotlink it.