What you'll build
Most teams end up calling three or four different AI providers for one product: a text-to-image model for thumbnails, a text-to-video model for social clips, maybe a TTS model for voiceover. Each provider ships its own SDK, its own auth scheme, its own polling logic. hiapi collapses all of that into one endpoint pattern: POST /v1/tasks to create a task, GET /v1/tasks/:id to check it, for every generation model on the platform.
This recipe shows the integration pattern using three real, currently live models — z-image (text-to-image), qwen-image-2.0 (text-to-image, different schema), and seedance-2.5 (text-to-video) — so you can see where the API stays identical and where only the input object changes per model.
Prerequisites
- An hiapi API key from your dashboard
curlor any HTTP client that can send JSON- No SDK required — every example below is a plain HTTP call
Every request needs one header:
Authorization: Bearer <your-api-key>
An invalid or missing key returns a 401-style error before any model is touched:
{
"error": {
"code": "permission_denied",
"message": "invalid api key",
"request_id": "req_...",
"type": "hiapi_error"
}
}
There is no keyless or anonymous mode — every /v1/tasks call must carry a valid bearer token. See authentication for key rotation and scoping.
Minimal runnable example
1. Create a task
The create call always has the same shape: a model id and a model-specific input object. Here's an image task with z-image:
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "z-image",
"input": {
"prompt": "a minimalist workspace, soft morning light, isometric illustration",
"aspect_ratio": "16:9"
}
}'
The response returns immediately — generation happens asynchronously:
{
"code": 200,
"message": "ok",
"data": {
"taskId": "tk-hiapi-xxxxxxxxxxxx",
"status": "queued"
}
}
Now the same pattern with qwen-image-2.0. Same endpoint, same envelope — only input differs (this model takes a size string instead of aspect_ratio):
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen-image-2.0",
"input": {
"prompt": "a minimalist workspace, soft morning light, isometric illustration",
"size": "2048*2048"
}
}'
And a video task with seedance-2.5/text-to-video — still the same envelope, now with duration and a wider aspect_ratio enum:
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.5/text-to-video",
"input": {
"prompt": "a paper airplane gliding through a sunlit office, slow motion",
"duration": 5,
"aspect_ratio": "16:9"
}
}'
This is the core lesson: the task envelope never changes, only input does. Every model on hiapi documents its own input schema on its model page — always check the specific model's schema before building a new integration, since fields like aspect_ratio, size, and duration are not interchangeable across models.
2. Poll for the result
curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-xxxxxxxxxxxx \
-H "Authorization: Bearer $HIAPI_API_KEY"
status moves through queued → handling → archiving → a terminal state of success or fail. On success, data.output is an array — most image/video models return one entry, but a single task can return multiple output types (e.g. a video model returning video plus a thumbnail):
{
"code": 200,
"message": "ok",
"data": {
"taskId": "tk-hiapi-xxxxxxxxxxxx",
"status": "success",
"output": [
{ "type": "image", "url": "https://...", "expireAt": "2026-10-09T00:00:00Z" }
]
}
}
expireAt matters — output URLs are ephemeral by default. Download the bytes (or re-upload to your own storage) as soon as the task finishes. If you need the platform to retain the file longer-term, set storage: "persistent" on creation or promote it afterward — see storage.
On failure, you get an error object instead of output:
{
"code": 200,
"message": "ok",
"data": {
"taskId": "tk-hiapi-xxxxxxxxxxxx",
"status": "fail",
"error": { "code": "...", "message": "..." }
}
}
A minimal Python polling loop that works identically for every model above:
import time
import requests
def run_task(model: str, input_payload: dict, api_key: str) -> list[dict]:
headers = {"Authorization": f"Bearer {api_key}"}
create = requests.post(
"https://api.hiapi.ai/v1/tasks",
headers=headers,
json={"model": model, "input": input_payload},
).json()
task_id = create["data"]["taskId"]
time.sleep(3) # initial wait before first poll
while True:
poll = requests.get(
f"https://api.hiapi.ai/v1/tasks/{task_id}", headers=headers
).json()
status = poll["data"]["status"]
if status == "success":
return poll["data"]["output"]
if status == "fail":
raise RuntimeError(poll["data"]["error"])
time.sleep(4) # 3-5s between polls; use 5-10s for video models
Production patterns
Use callbacks instead of polling in production
Polling is fine for a script or a CLI tool, but at any real traffic volume, prefer a callback: pass a callback object on task creation and hiapi POSTs the terminal result to your endpoint instead of you hammering GET /v1/tasks/:id in a loop.
{
"model": "z-image",
"input": { "prompt": "...", "aspect_ratio": "1:1" },
"callback": { "url": "https://yourapp.com/webhooks/hiapi", "when": "final" }
}
when: "final" means you only get one POST, on the terminal state (success or fail) — no intermediate handling/archiving noise. If you've wired up a callback and nothing is arriving, check your endpoint is publicly reachable and returns a 2xx quickly; see this related writeup on the most common cause of silently-missing callbacks.
Idempotent retries
Network blips happen. If you retry a POST /v1/tasks call after a timeout without protection, you risk creating (and getting billed for) a duplicate task. Send an Idempotency-Key header (any string up to 255 bytes, typically a UUID you generate per logical request) and retries of the same key return the original task instead of creating a new one:
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Idempotency-Key: 8f14e45f-ceea-467e-bd5f-...-9a2c" \
-H "Content-Type: application/json" \
-d '{"model": "z-image", "input": {"prompt": "...", "aspect_ratio": "1:1"}}'
Handle validation errors per model
Because every model has its own input schema, the most common failure mode is a 400 on a field that's valid for one model but not another — e.g. sending aspect_ratio to a model that expects size:
{
"code": 400,
"data": null,
"error_code": "INVALID_REQUEST",
"message": "invalid input: aspect_ratio: value must be one of '1:1', '4:3', '3:4', '16:9', '9:16'"
}
If you're building a multi-model router (letting users or your own logic pick a model at runtime), keep a small per-model input-builder registry rather than one shared payload shape:
MODEL_INPUT_BUILDERS = {
"z-image": lambda prompt, **kw: {
"prompt": prompt, "aspect_ratio": kw.get("aspect_ratio", "1:1")
},
"qwen-image-2.0": lambda prompt, **kw: {
"prompt": prompt, "size": kw.get("size", "2048*2048")
},
"seedance-2.5/text-to-video": lambda prompt, **kw: {
"prompt": prompt,
"duration": kw.get("duration", 5),
"aspect_ratio": kw.get("aspect_ratio", "16:9"),
},
}
def build_task_payload(model: str, prompt: str, **kw) -> dict:
return {"model": model, "input": MODEL_INPUT_BUILDERS[model](prompt, **kw)}
This keeps the per-model quirks in one place instead of scattered across every call site. A nonexistent or cross-account taskId returns a plain 404:
{ "code": 404, "message": "task not found", "data": null }
And a transient 503 should be retried with exponential backoff rather than surfaced straight to the user — rate limits and capacity limits are documented in rate limits.
Related reading
- Async task API reference — full
create/detailcontract - Authentication — API key scoping and rotation
- Storage — persisting output beyond the default expiry
- Why your task callback might not be firing
- Browse all models — every model's own
inputschema lives on its page - Pricing — cost is per-model and per-output, not flat per request
FAQ
Do I need a different SDK or API key per model?
No. One API key, one endpoint pattern (/v1/tasks), for every model on the platform. Only the input object inside the request body changes.
Why doesn't my response have output yet right after creating the task?
Generation is asynchronous. The create call only returns taskId and status: "queued" — you have to poll GET /v1/tasks/:id (or use a callback) until status reaches success or fail.
Can I call the API without authentication for a quick test?
No — every /v1/tasks call requires a valid Authorization: Bearer key. There's no keyless or demo mode for production calls; generate a key from your dashboard first.
What happens if I poll after the output URL has expired?
The task record itself persists, but the output[].url from a non-persistent task will stop resolving after expireAt. Download the file immediately on success, or set storage: "persistent" at creation time if you need it to stick around.
How do I know which fields a given model accepts?
Check that model's page under Models — each one documents its exact input schema. Don't assume aspect_ratio, size, or duration carry over between models; as shown above, z-image and qwen-image-2.0 already disagree on how to express image shape.









