指南视频生成 API 接入最佳实践:幂等、429 与轮询
MakerNice 团队 · ·
稳定接入 MakerNice 生成 API 靠五个习惯:每个任务一个 Idempotency-Key、创建前一刻再报价、按 Retry-After 轮询、只取消排队中的任务、在产物过期前转存。它们适用于所有视频和图片模型,包括 Seedance 2.5 和 Wan 3.0,因为这些模型共用同一套异步 REST 流程。
重试 POST /v1/generations 怎样才不会重复扣费?
- POST /v1/generations 必须带 Idempotency-Key 请求头:1 到 200 个可打印 ASCII 字符,不含空格,在你的账户内唯一。
- 每个业务任务生成一个键,发请求之前先存下来,之后每次重试都复用它。重试会返回 200 和原来的任务,不会再创建一个。
- 同一个键搭配不同的报价或不同的计价参数会返回 422 idempotency_conflict,所以不要在不同任务之间复用键。

报价多久有效?应该什么时候获取?
POST /v1/generations/quote 返回的报价有效期 12 分钟,且只能使用一次。请在创建之前立刻获取,而不是用户刚打开表单时就取。报价之后如果改了 model、mode、resolution 或 duration,创建会返回 422 quote_mismatch,所以要按最终输入报价。
多久轮询一次任务?遇到 429 怎么办?
每隔几秒请求一次 GET /v1/generations/{id} 就足够,生成本来就要一段时间。遇到终态就停:succeeded、failed 或 canceled。checking 表示我们正在核对结果、金额仍被冻结,继续轮询即可。所有接口都可能返回 429 rate_limited,并在 Retry-After 响应头里给出要等待的秒数,等够再发下一个请求。账户下(所有 Key 合计)进行中的任务数达到上限时,创建还会返回 429 concurrency_limit_reached,所以请在你这一侧排队,不要一次性全部发出。
// Poll one generation until it reaches a final status
const POLL_INTERVAL_MS = 5000;
const REQUEST_TIMEOUT_MS = 30000;
const FINAL = new Set(["succeeded", "failed", "canceled"]);
async function waitFor(id) {
for (;;) {
const res = await fetch(API_BASE + "/generations/" + id, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
headers: { Authorization: "Bearer " + API_KEY },
});
if (res.status === 429) {
// Honour the server's Retry-After (seconds) before trying again
const retry_after_s = Number(res.headers.get("Retry-After") ?? "5");
await new Promise((r) => setTimeout(r, retry_after_s * 1000));
continue;
}
if (!res.ok) throw new Error("poll failed: " + res.status);
const gen = await res.json();
if (FINAL.has(gen.status)) return gen;
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
}哪些 API 错误该重试,哪些该修正?
| 状态码 | 错误码 | 能否重试 | 怎么处理 |
|---|---|---|---|
| 429 | rate_limited、concurrency_limit_reached | 可以 | 等 Retry-After 秒后原样重发 |
| 410 | quote_expired | 换新报价后可以 | 重新报价再创建 |
| 409 | quote_already_used | 换新报价后可以 | 先确认任务复用了自己的键,再重新报价 |
| 422 | quote_mismatch | 换新报价后可以 | 按最终输入报价 |
| 422 | idempotency_conflict | 不可以 | 只有真正的新任务才换新键 |
| 400 | invalid_request | 不可以 | 修正输入;用 GET /v1/models 核对模式 |
| 402 | insufficient_balance | 不可以 | 到控制台充值 |
| 409 | not_cancelable | 不可以 | 用 GET 查看任务当前状态 |
| 410 | output_expired | 不可以 | 改用你自己存储里的副本 |
创建接口的其他错误码更适合提示给人处理,而不是自动重试:402 key_spend_limit_reached、402 daily_spend_limit_reached、403 org_frozen 和 404 quote_not_found。每个错误响应都带 error.code、error.message,通常还有 request_id;请按 code 而不是 message 做分支,并把 request_id 记进日志。
什么时候可以取消生成任务?
POST /v1/generations/{id}/cancel 只在状态为 queued 时有效,并会全额释放冻结金额。任务进入 generating 之后,取消会返回 409 not_cancelable。重复执行一次已经成功的取消同样返回 409,所以遇到 409 时,先用 GET 查看状态再判断发生了什么。
生成的视频能下载多久?
带上 Key 请求 GET /v1/generations/{id}/outputs/{n}:只有任务成功后才可用,返回指向短期签名地址的 302。每次需要文件时重新请求,不要保存跳转后的地址;想以附件形式下载可加 download=1。已转存的产物带 expiresAt(尚未转存时为 null),过期后接口返回 410 output_expired,所以任务一成功就把结果转存到你自己的存储里。
每个生成任务应该记录什么?
为每个任务记录报价 id、幂等键和任务 id。三者合起来就能还原整件事:GET /v1/generations/{id} 显示当前状态,GET /v1/generations/{id}/events 返回带时间戳的事件时间线,事件码包括 created、generating、delivered、failed_refunded 等。程序崩溃重启后,用 GET /v1/generations?status=queued,generating 列出仍在进行中的任务。这些标识也会让你向我们反馈问题时处理得快得多。
刚开始接入?先读「Seedance 2.5 API 教程」跑通四个基本调用,再把这些习惯加进去。实时价格可以调用 GET /v1/pricing,或用确切输入请求报价。

