MakerNice
← All posts
Macro close-up of precision brass gears on a watchmaker's workbench under a warm desk lampGuides

Video 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.
3D clay illustration of a key standing inside a ring of circular arrows, with a clock and a shield bearing a check mark
One idempotency key per job turns every retry into a safe replay instead of a second charge.

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?

StatusError codeRetry?What to do
429rate_limited, concurrency_limit_reachedYesWait Retry-After seconds, then repeat the same call
410quote_expiredWith a new quoteQuote again, then create
409quote_already_usedWith a new quoteCheck the job reused its key, then quote again
422quote_mismatchWith a new quoteQuote the final input
422idempotency_conflictNoUse a new key only for a genuinely new job
400invalid_requestNoFix the input; check the mode in GET /v1/models
402insufficient_balanceNoTop up in the console
409not_cancelableNoGET the generation to see its status
410output_expiredNoServe 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?
checking means we are confirming a result before it is final. The amount stays held, so keep polling until you see succeeded, failed or canceled.
Can I reuse an Idempotency-Key for a different generation?
No. A key must be unique within your account. Sending it again with a different quote or different price-affecting parameters returns 422 idempotency_conflict.
Should I store the URL that outputs/0 redirects to?
No. The redirect points to a short-lived signed URL. Request /v1/generations/{id}/outputs/0 again each time you need the file, or keep your own copy.
Is the Retry-After header in seconds or milliseconds?
Seconds. Every 429 response carries Retry-After as the number of seconds to wait before retrying.

Models in this post