异步任务
图片和视频怎么提交、轮询、接收推送和下载,每个状态和字段代表什么,失败时怎么退款。
哪些接口是异步的
图片和视频生成都以异步任务运行:提交后立刻返回任务号,生成在后台进行。对话接口是同步的,不走任务。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /v1/images/generations | 文生图。立刻返回任务号(HTTP 202)。 |
| POST | /v1/images/edits | 图生图。也可以在上一个接口里带 image 参数,两者等价。 |
| GET | /v1/images/jobs/{id} | 查询图片任务的状态与结果。 |
| POST | /v1/videos/generations | 生成视频。立刻返回任务号(HTTP 202)。 |
| GET | /v1/videos/jobs/{id} | 查询视频任务的状态与结果。 |
流程
- 提交任务:调用生成接口,立刻得到 HTTP 202 和任务号
id(形如img_...,视频任务也用这个前缀)。 - 等待:用任务号轮询查询接口,或者在提交时带上
callback_url,等结果推送过来。 - 完成:
status变成succeeded,结果链接在data[0].url;变成failed时原因在error,预扣的积分已经退回。 - 下载:结果链接保留 60 天,需要长期使用就下载到自己的存储。
curl https://nezhagate.com/v1/videos/generations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "seedance-2.0", "prompt": "a paper boat drifting down a rainy street", "duration": 5, "size": "16:9"}'
# HTTP 202
{"id": "img_9c1e...7a", "object": "video.generation.job", "status": "queued", "created": 1791199400, "model": "seedance-2.0"}curl https://nezhagate.com/v1/videos/jobs/img_9c1e...7a -H "Authorization: Bearer YOUR_API_KEY"
任务状态
| 状态 | 含义 |
|---|---|
queued | 排队中。返回里带 queue_position(前面还有几个任务)和 eta_seconds(预计等待秒数)。 |
processing | 生成中。 |
succeeded | 已完成。结果在 data[0].url,计费明细在 usage。 |
failed | 失败。原因在 error,预扣的积分已全额退回。 |
succeeded 和 failed 是最终状态,之后不会再变。
返回字段
| 字段 | 含义 |
|---|---|
id | 任务号。查询和 Webhook 都用它。 |
object | 图片任务是 image.generation.job,视频任务是 video.generation.job。 |
status | queued / processing / succeeded / failed。 |
created | 提交时间,Unix 秒。 |
model | 提交时用的模型 ID。 |
queue_position | 只在 queued 时出现:前面还有几个任务。 |
eta_seconds | 只在 queued 时出现:预计还要等多少秒开始。 |
data[].url | 只在 succeeded 时出现:结果文件的链接(图片或 mp4),保留 60 天。 |
usage | 只在 succeeded 时出现:计费明细。图片是 images(实际交付张数)和 resolution,视频是 clips。 |
error | 只在 failed 时出现:message、type、code、param,按 code 判断原因。 |
{
"id": "img_9c1e...7a",
"object": "video.generation.job",
"status": "succeeded",
"created": 1791199400,
"model": "seedance-2.0",
"data": [{"url": "https://img.nezhagate.com/i/5d41402abc....mp4"}],
"usage": {"clips": 1, "model": "seedance-2.0"}
}{
"id": "img_9c1e...7a",
"object": "video.generation.job",
"status": "failed",
"created": 1791199400,
"model": "seedance-2.0",
"error": {"message": "...", "type": "invalid_request_error", "code": "content_policy", "param": null}
}轮询建议
- 图片每 2–3 秒查一次,视频每 5–10 秒查一次。查询不收费。
- 拿到
succeeded或failed就停止轮询。 - 给自己的程序设一个总等待时间:图片一般几十秒到几分钟,视频从一两分钟到一个多小时不等(长视频、排队时更久),时长参考各模型文档。超过时间也别重新提交,任务还在,稍后接着查。
- 任务属于提交它的账号:同一账号下的任何一把 Key 都能查询,别的账号查询会返回 404
job_not_found。任务记录保留 3 天,过期后查询同样返回 404,但结果链接仍可使用到 60 天。 - 提交请求如果在你这边超时、没拿到任务号,先到控制台的调用日志里看任务是否已经创建,再决定要不要重新提交,避免重复扣费。
用 Webhook 代替轮询
提交时加上 callback_url,任务一结束(成功或失败),网关就把和查询接口完全一样的结果推送到这个地址,带签名,失败会自动重试。也可以在控制台「设置 → Webhook」里配一个账户级地址。
{"model": "seedance-2.0", "prompt": "...", "duration": 5, "callback_url": "https://example.com/webhooks/nezhagate"}计费
- 提交时按价格预扣积分;余额不够会直接返回 402,任务不会创建。
- 成功时按实际交付结算:图片按交付的张数,视频按下单的时长或条数(和预扣一致),多预扣的部分自动退回;失败时预扣的积分全额退回。
- 查询任务、接收推送都不收费。
结果保存多久
data[0].url 是我们存储上的链接,保留 60 天后自动删除。需要长期使用,请下载后存到你自己的存储或 CDN。
任务为什么会失败
失败的任务都已全额退款。下面是任务结果里最常见的几个错误码:
| code | 什么时候出现 | 怎么办 |
|---|---|---|
content_policy | 视频:提示词、参考素材,或生成的画面 / 声音没通过内容审核(声音被拦时 param 为 audio)。 | 改写提示词或更换素材后再提交;原样重交还会被拒。 |
moderation_blocked | 图片:模型方的安全审核拒绝了提示词或参考图(也可能是 content_policy_violation)。 | 改写提示词或更换参考图;原样重交通常还会被拒。 |
render_failed | 视频:这次没有生成出成片,多为偶发。 | 原样重新提交通常就能成功。 |
invalid_material | 视频:上游认为输入素材有问题。这是一个笼统的报错,也常是偶发。 | 先原样重交;反复出现再检查参考素材和提示词长度。 |
render_timeout | 视频:排队或渲染时间过长,上游放弃了这一单。 | 重新提交。 |
wait_timeout | 视频:等满了这条线路允许的最长时间,仍然没有出片。 | 重新提交。 |
upstream_unstable | 视频:渲染过程中连接中断。 | 重新提交。 |
result_fetch_failed | 视频已经生成,但我们反复下载或保存成片都没有成功。 | 重新提交。 |
invalid_request | 视频:线路不接受这组参数(例如时长、比例、清晰度的组合)。 | 按 message 调整参数后再提交。 |
upstream_error | 其他失败:所有线路都试过仍然失败、排队 15 分钟还没开始、上游拒绝但没有说明原因等,message 写明具体情况。 | 通常重新提交就能成功。 |
完整示例
提交、轮询、下载一条龙,包含总等待时间和失败处理。把 YOUR_API_KEY 换成你的 Key 就能运行。
import time
import requests
BASE = "https://nezhagate.com/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
def run_job(kind, payload, every=5, give_up=5400):
"""kind is "images" or "videos". Returns the finished job (succeeded or failed)."""
job = requests.post(f"{BASE}/{kind}/generations", headers=HEADERS, json=payload, timeout=120)
job.raise_for_status() # 4xx here: fix the request; nothing was billed
job_id = job.json()["id"]
deadline = time.time() + give_up
while time.time() < deadline:
time.sleep(every)
r = requests.get(f"{BASE}/{kind}/jobs/{job_id}", headers=HEADERS, timeout=30).json()
if r["status"] in ("succeeded", "failed"):
return r
raise TimeoutError(f"{job_id} still running; poll it again later, it is not lost")
job = run_job("videos", {"model": "seedance-2.0", "prompt": "a paper boat drifting down a rainy street",
"duration": 5, "size": "16:9"})
if job["status"] == "succeeded":
url = job["data"][0]["url"]
with open("clip.mp4", "wb") as f: # keep your own copy: links expire after 60 days
f.write(requests.get(url, timeout=300).content)
else:
print(job["error"]["code"], job["error"]["message"])