NezhaGateNezhaGate

图片和视频怎么提交、轮询、接收推送和下载,每个状态和字段代表什么,失败时怎么退款。

哪些接口是异步的

图片和视频生成都以异步任务运行:提交后立刻返回任务号,生成在后台进行。对话接口是同步的,不走任务。

方法路径说明
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}查询视频任务的状态与结果。

流程

  1. 提交任务:调用生成接口,立刻得到 HTTP 202 和任务号 id(形如 img_...,视频任务也用这个前缀)。
  2. 等待:用任务号轮询查询接口,或者在提交时带上 callback_url,等结果推送过来。
  3. 完成:status 变成 succeeded,结果链接在 data[0].url;变成 failed 时原因在 error,预扣的积分已经退回。
  4. 下载:结果链接保留 60 天,需要长期使用就下载到自己的存储。
curl · 提交任务
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 · 查询任务
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。
statusqueued / 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 判断原因。
完成后的返回 · video
{
  "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"}
}
failed
{
  "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」里配一个账户级地址。

JSON · callback_url
{"model": "seedance-2.0", "prompt": "...", "duration": 5, "callback_url": "https://example.com/webhooks/nezhagate"}

Webhook 文档 →

计费

  • 提交时按价格预扣积分;余额不够会直接返回 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 就能运行。

Python
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"])