你用 POST /v1/tasks 出图或出视频,任务跑完后从 data.output[0].url 拿到一条 temp.hiapi.ai 的链接——当时能打开,过几天再访问却变成一个 HTML 的「Not Found」页:
HTTP/2 404
content-type: text/html
返回的不是图片/视频字节,而是 Cloudflare 的 404 页面。原因只有一个:任务输出 URL 是带有效期的临时链接,终态里的 expireAt(Unix 时间戳)一到,temp.hiapi.ai 上的对象就被清理,热链失效。
先看懂终态里的字段
轮询任务终态(GET /v1/tasks/{taskId})成功时,关键字段长这样:
{
"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"才是终态成功(失败是"fail",排队/处理中是queued/handling/archiving)。data.output是数组,每个元素带自己的url和expireAt。expireAt是 Unix 整数时间戳(不是 ISO 字符串)。判断是否过期只看它:now >= expireAt即过期。data.storage的值是"temp"——这本身就是提示:它是临时存储,不是给你长期引用的。
有效期多长?实测一个样本的
expireAt - completed = 604800秒(正好 7 天)。但不要把「7 天」写死进代码——以每条输出自己的expireAt为准,不同模型/存储策略可能不一样。
常见原因(按命中概率排序)
- 把热链存进了数据库长期引用:落库时存的是
temp.hiapi.ai/...这条临时 URL,过了expireAt就集体 404。这是最常见的坑。 - 拿到终态没立刻下载:任务跑完后隔了很久(超过 TTL)才去取字节。
- 以为重新轮询能「刷新」链接:对同一个
taskId再 GET 一次,返回的是同一条 url 和同一个expireAt——元数据持久,但对象本身过期后不会再生,重新 GET 不会给你新链接。 - 误把 404 当成任务失败:任务其实早就
success了,只是输出资产过期了,这是两件事。
修复步骤
- 终态一到,立刻下载字节。 轮询到
data.status == "success"后,马上对每个output[].url发 GET 把二进制拉下来,别排队、别延后。 - 转存到你自己的对象存储(R2 / S3 / 本地盘),换成一条你自己的持久 URL。
- 数据库里只存你自己的持久 URL,绝不存
temp.hiapi.ai热链。 - 下载前先校验没过期:
now < expireAt再下;已过期直接走重跑逻辑。 - 过期了只能重跑任务。 没有「用 taskId 换新链」这回事——对同一个 taskId 再 GET,拿到的还是那条失效 url。要重新拿到可下载的结果,只能重新提交一次任务(会重新计费)。
- 批量任务控制下载时序:一批任务里最先完成的那条,
expireAt也最早到期。按完成先后立刻下载,别等整批都跑完再统一下载,否则早完成的可能已临近过期。
最小验证示例
确认你的 key 通、任务接口能出结果、并亲眼看到 expireAt。model id 用裸 id(z-image,别加 /text-to-image 之类后缀,会 400):
# 1. 提交任务
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. 用上一步的 taskId 轮询终态,读出 url 和 expireAt
curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-xxxx \
-H "Authorization: Bearer $HIAPI_API_KEY"
# data.status 变成 "success" 后,data.output[0].url / .expireAt 就有了
认证如果失败,返回的是 HTTP 401 + {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"..."}}——那是 key 的问题,不是过期问题。查不到任务则是 {"code":404,"data":null,"message":"task not found"}——通常是 taskId 抄错或用错了环境的 key。
相关链接
- 任务接口怎么提交、怎么轮询:HiAPI 快速上手文档
- 查你的 API key、看任务历史:控制台
- 有哪些模型可以调:模型市场
- 单个模型页示例(本文验证用的那个):Z-Image 文生图
FAQ
问:expireAt 到底是什么格式?
答:Unix 整数时间戳(秒),比如 1792432981,不是 ISO 字符串。用 now >= expireAt 判断过期即可。
问:链接过期后,能不能重新 GET 一次任务把链接「续期」?
答:不能。对同一个 taskId 再 GET,拿到的还是那条原始 url 和原始 expireAt,对象已被清理。要重新拿到可下载的结果,只能重跑任务(重新计费)。
问:过期后访问返回的是 403 还是 404?
答:是 HTTP 404,而且 body 是 text/html 的 Cloudflare「Not Found」页,不是图片字节、也不是 XML 的 AccessDenied。如果你的下载逻辑只认 200,记得把「非 200 一律按失败」处理。
问:有效期是不是固定 7 天?
答:实测样本是 7 天(604800 秒),但请以每条输出自己的 expireAt 为准,别在代码里写死天数。
问:一批任务怎么下载最稳?
答:按完成先后,谁先 success 就先下谁的字节。最先完成的输出 expireAt 也最早到期,拖到最后统一下载容易踩到临期。





