GuidesVideo Generation API Best Practices: Idempotency, 429s and Polling
MakerNice Team · ·
A reliable integration with the MakerNice generation API comes down to five habits: one Idempotency-Key per job, a fresh quote right before you create, polling that honours Retry-After, cancelling only queued jobs, and copying outputs before they expire. They apply to every video and image model, including Seedance 2.5 and Wan 3.0, because all of them share the same async REST flow.
How do you retry POST /v1/generations without paying twice?
- POST /v1/generations requires an Idempotency-Key header of 1 to 200 printable ASCII characters, no spaces, unique within your account.
- Generate one key per logical job, store it before you send the request, and reuse it on every retry. A retry returns 200 with the original generation instead of creating a second one.
- Reusing a key with a different quote or different price-affecting parameters returns 422 idempotency_conflict, so never recycle keys across jobs.

How long is a quote valid, and when should you request it?
A quote from POST /v1/generations/quote is valid for 12 minutes and can be used once. Request it immediately before creating, not when a user first opens a form. If you change the model, mode, resolution or duration after quoting, create returns 422 quote_mismatch, so quote the final input.
How often should you poll a generation, and how do you handle 429?
Polling GET /v1/generations/{id} every few seconds is plenty, since generations take a while. Stop on a final status: succeeded, failed or canceled. The checking status means we are confirming the result and the amount stays held, so keep polling. Every endpoint can answer 429 rate_limited with a Retry-After header in seconds; wait that long before the next call. Create can also return 429 concurrency_limit_reached when your account already has its maximum number of generations in progress, across all of its keys, so queue work on your side instead of firing everything at once.
// 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));
}
}Which API errors should you retry, and which should you fix?
| Status | Error code | Retry? | What to do |
|---|---|---|---|
| 429 | rate_limited, concurrency_limit_reached | Yes | Wait Retry-After seconds, then repeat the same call |
| 410 | quote_expired | With a new quote | Quote again, then create |
| 409 | quote_already_used | With a new quote | Check the job reused its key, then quote again |
| 422 | quote_mismatch | With a new quote | Quote the final input |
| 422 | idempotency_conflict | No | Use a new key only for a genuinely new job |
| 400 | invalid_request | No | Fix the input; check the mode in GET /v1/models |
| 402 | insufficient_balance | No | Top up in the console |
| 409 | not_cancelable | No | GET the generation to see its status |
| 410 | output_expired | No | Serve the copy in your own storage |
Other codes on create are worth surfacing to a person rather than retrying: 402 key_spend_limit_reached and 402 daily_spend_limit_reached, 403 org_frozen and 404 quote_not_found. Every error body carries error.code, error.message and usually a request_id; branch on the code, not the message, and log the request_id.
When can you cancel a generation?
POST /v1/generations/{id}/cancel works only while the status is queued, and it releases the hold in full. Once a generation is generating, cancel returns 409 not_cancelable. Repeating a cancel that already succeeded also returns 409, so check the status with a GET before deciding what went wrong.
How long can you download a generated video?
Request GET /v1/generations/{id}/outputs/{n} with your key; it only works once the generation has succeeded and answers with a 302 to a short-lived signed URL. Ask again whenever you need the file rather than storing the redirect target, and add download=1 if you want an attachment. Stored outputs carry an expiresAt (it is null while a file has not been stored yet), and after that the endpoint returns 410 output_expired, so copy results into your own storage as soon as a generation succeeds.
What should you log for every generation?
Record the quote id, the idempotency key and the generation id for every job. Together they let you reconstruct what happened: GET /v1/generations/{id} shows the current state, and GET /v1/generations/{id}/events returns a timeline of codes such as created, generating, delivered and failed_refunded, each with a timestamp. After a crash, GET /v1/generations?status=queued,generating lists the jobs still in flight. The same identifiers also make support requests much faster to resolve.
New to the API? Start with the Seedance 2.5 API tutorial for the four basic calls, then add these habits. For live prices, call GET /v1/pricing or request a quote with your exact input.
Frequently asked questions
What does the checking status mean in the MakerNice API?
Can I reuse an Idempotency-Key for a different generation?
Should I store the URL that outputs/0 redirects to?
Is the Retry-After header in seconds or milliseconds?
Models in this post
VideoSeedance 2.5
ByteDance's flagship for long takes: up to 30 seconds, 480p to 4K, and up to 16 reference images to keep every shot on-model.
VideoWan 3.0
-21%The widest duration range on MakerNice — 2 to 30 seconds — at 480p, 720p or 1080p, with every input mode and up to 10 references.