Tutorial
HiAPI task output URL expiration (expireAt): download and re-store results before they go invalid
The terminal output[].url carries an expireAt; once expired it returns 404 — how to detect it, how to download in time, and how to recover after expiration.
You use POST /v1/tasks to generate an image or video, and after the task finishes you get a temp.hiapi.ai link from data.output[0].url — it opens fine at the time, but a few days later it turns into an HTML "Not Found" page:
HTTP/2 404
content-type: text/html
What comes back is not image/video bytes but a Cloudflare 404 page. There is only one reason: task output URLs are temporary links with a validity period. Once the expireAt (Unix timestamp) in the terminal state arrives, the object on temp.hiapi.ai is cleaned up and the hotlink becomes invalid.
First, understand the fields in the terminal state
When polling the task terminal state (GET /v1/tasks/{taskId}) succeeds, the key fields look like this:
{
"code": 200,
"data": {
"taskId": "tk-hiapi-xxxxxxxx",
"status": "success",
"storage": "temp",
"created": 1791828181,
"completed": 1791828201,
"output": [
{
"artifactId": "...",
"type": "image",
"url": "https://temp.hiapi.ai/....png",
"expireAt": 1792432981
}
]
},
"message": "success"
}
data.status == "success"is the only terminal success state (failure is"fail"; queued/processing arequeued/handling/archiving).data.outputis an array, and each element carries its ownurlandexpireAt.expireAtis a Unix integer timestamp (not an ISO string). To check expiration, look only at it:now >= expireAtmeans expired.data.storagehas the value"temp"— that itself is a hint: it is temporary storage, not meant for long-term reference.
How long is the validity period? A measured sample had
expireAt - completed = 604800seconds (exactly 7 days). But do not hardcode "7 days" into your code — always go by each output's ownexpireAt; different models/storage policies may differ.
Common causes (ordered by likelihood of occurrence)
- Storing the hotlink in a database for long-term reference: when persisting, you store the temporary URL
temp.hiapi.ai/..., and afterexpireAtthey all 404 together. This is the most common pitfall. - Not downloading immediately upon getting the terminal state: fetching the bytes long after the task finishes (beyond the TTL).
- Assuming re-polling can "refresh" the link: GETting the same
taskIdagain returns the same url and the sameexpireAt— the metadata persists, but the object itself is not regenerated after expiration, and re-GETting will not give you a new link. - Mistaking the 404 for a task failure: the task actually reached
successlong ago; only the output asset expired. These are two separate things.
Fix steps
- Download the bytes immediately when the terminal state arrives. After polling to
data.status == "success", immediately send a GET to eachoutput[].urlto pull the binary — do not queue it, do not delay. - Re-store it in your own object storage (R2 / S3 / local disk) and use your own persistent URL instead.
- Store only your own persistent URL in the database; never store the
temp.hiapi.aihotlink. - Validate before downloading that it has not expired: download only if
now < expireAt; if already expired, go straight to the re-run logic. - Once expired, the only option is to re-run the task. There is no such thing as "exchanging the taskId for a new link" — GETting the same taskId again still returns the expired url. To get a downloadable result again, you must submit the task anew (billed again).
- Control download timing for batch tasks: the first task to finish in a batch also has the earliest
expireAt. Download immediately in completion order instead of waiting for the whole batch to finish and downloading all at once, otherwise early finishers may be close to expiration.
Minimal verification example
Confirm your key works, the task API can produce a result, and you can see expireAt with your own eyes. Use the bare model id (i.e., z-image; do not add suffixes like /text-to-image, or you will get a 400):
# 1. Submit a task
curl -s https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "z-image",
"input": { "prompt": "a red apple on a wooden table" }
}'
# → {"code":200,"data":{"taskId":"tk-hiapi-..."},"message":"success"}
# 2. Poll the terminal state with the taskId from the previous step, and read url and expireAt
curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-xxxx \
-H "Authorization: Bearer $HIAPI_API_KEY"
# once data.status becomes "success", data.output[0].url / .expireAt are available
If authentication fails, the response is HTTP 401 + {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"..."}} — that is a key problem, not an expiration problem. If the task cannot be found, it is {"code":404,"data":null,"message":"task not found"} — usually a mistyped taskId or a key from the wrong environment.
Related links
- How to submit tasks and how to poll: HiAPI quickstart documentation
- Check your API key and view task history: Console
- Which models are available: Model marketplace
- Example single-model page (the one used for verification in this article): Z-Image text-to-image
FAQ
Q: What exactly is the format of expireAt?
A: A Unix integer timestamp (seconds), such as 1792432981, not an ISO string. Use now >= expireAt to determine expiration.
Q: After the link expires, can I GET the task again to "renew" the link?
A: No. GETting the same taskId again still returns the original url and original expireAt, and the object has already been cleaned up. To get a downloadable result again, you must re-run the task (billed again).
Q: After expiration, does access return 403 or 404?
A: It is HTTP 404, and the body is a Cloudflare "Not Found" page with text/html — not image bytes, and not an XML AccessDenied. If your download logic only accepts 200, remember to treat "anything non-200 as failure".
Q: Is the validity period fixed at 7 days?
A: The measured sample was 7 days (604800 seconds), but go by each output's own expireAt; do not hardcode the number of days in your code.
Q: What is the most reliable way to download a batch of tasks?
A: In completion order — whoever reaches success first gets its bytes downloaded first. The earliest finished output also has the earliest expireAt; dragging everything to a final bulk download easily hits near-expiration cases.
Related articles
View all articles →-
Tutorial
How to Call hailuo-2.3-fast/image-to-video via the HiAPI API: curl and Python
A first working request for the fast, low-cost image-to-video tier — from model ID to downloaded MP4
9 min read -
Tutorial
How to Use hailuo-2.3/text-to-video via the HiAPI API: curl, Python, and a Working Request
11 min read -
Tutorial
Kandinsky 6.0 Pro Text-to-Video on HiAPI: curl and Python Tutorial
A request-first walkthrough: build the prompt-to-clip call, submit it, poll for completion, and download the result
11 min read