Guides
Generations
How async generations work: queued, running, succeeded, failed and canceled statuses, polling best practices, outputs and cancellation.
Video and image models take from a few seconds to several minutes, so every request is asynchronous. Submitting returns a generation immediately; you then poll it or receive a webhook when it reaches a final state. One generation object describes the whole job — its input, status, output, error and cost.
Lifecycle#
queued if no capacity accepts it. Failed and canceled jobs are refunded in full.| Status | Final? | Meaning |
|---|---|---|
queued | No | Accepted and waiting for capacity. The price is held from your balance. You can still cancel. |
running | No | The model is generating. started_at is set. It can no longer be canceled. |
succeeded | Yes | Finished. output holds the media URLs and the held amount becomes the final charge. |
failed | Yes | Could not be completed. error explains why and the full hold has been refunded. |
canceled | Yes | You canceled it while it was queued. The full hold has been refunded. |
succeeded, failed and canceled are final: a generation never changes after reaching one of them. Treat any other value as "still in progress" so your code keeps working if we add intermediate states.
1. Submit#
POST /api/v1/run/{endpoint} with the model's inputs as the JSON body. The endpoint ID combines the model and the task, e.g. bytedance/seedance-2.0-fast/text-to-video. Find every endpoint and its fields on the Models page.
We validate the input, compute the price, hold it from your balance and queue the job. The response is 202 Accepted with the generation, plus urls.get and urls.cancel for convenience:
{
"id": "gen_8c2QmX1nq0k4Lw7Z",
"object": "generation",
"model": "bytedance/seedance-2.0-fast",
"endpoint": "bytedance/seedance-2.0-fast/text-to-video",
"mode": "text-to-video",
"status": "queued",
"input": {
"prompt": "A slow dolly-in on a ceramicist shaping a vase at golden hour, dust in the light, shallow depth of field",
"aspect_ratio": "16:9",
"resolution": "720p",
"duration": 5,
"generate_audio": true
},
"output": null,
"error": null,
"cost": {
"amount_usd": 0.4708,
"currency": "usd",
"estimated": true
},
"source": "api",
"created_at": "2026-10-15T09:21:04.512Z",
"started_at": null,
"completed_at": null,
"retention": "1d",
"expires_at": null,
"purged_at": null,
"urls": {
"get": "https://cheapergen.com/api/v1/generations/gen_8c2QmX1nq0k4Lw7Z",
"cancel": "https://cheapergen.com/api/v1/generations/gen_8c2QmX1nq0k4Lw7Z/cancel"
}
}Reference images, videos and audio#
reference-to-video modes take up to three kinds of references, each a list of HTTPS URLs (public links, or URLs from uploads): reference_image_urls for characters, products and scenes, reference_video_urls for motion, camera moves and effects, and reference_audio_urls for voice, music and sound.
Point at them in the prompt by position: @Image1 is the first URL in reference_image_urls, @Video1 the first in reference_video_urls, @Audio2 the second audio clip, and so on. Mentions are case-insensitive and normalized to @Image1. A mention with no matching reference (say @Video2 with one video attached) is rejected with 400 invalid_request (param: "prompt") before anything is charged.
{
"prompt": "@Image1 dances on a rooftop, copying the choreography and camera move of @Video1, in sync with @Audio1",
"reference_image_urls": [
"https://example.com/dancer.jpg"
],
"reference_video_urls": [
"https://example.com/choreo.mp4"
],
"reference_audio_urls": [
"https://example.com/beat.mp3"
],
"resolution": "720p",
"duration": 8
}We download the start of every reference video and audio file when you submit and check its real duration, size, resolution and frame rate against the model's limits. Anything out of range — a 20-second clip, a 1080p video for a model that takes 720p, an M4A where MP3 or WAV is required, a URL that doesn't answer — returns 400 invalid_request with param set to the field (for example reference_video_urls) and a message naming the clip, before anything is charged. Reference videos add billed seconds; see Billing.
| Model | Images | Videos | Audio | Rules |
|---|---|---|---|---|
| Seedance 2.0 | ≤ 9 | ≤ 3 · MP4/MOV · 2–15 s each, 15 s total · ≤ 50 MB total · 24–60 fps | ≤ 3 · MP3/WAV · 2–15 s each, 15 s total · ≤ 15 MB each | ≤ 12 files in total; needs an image or video |
| Seedance 2.5 | ≤ 30 | ≤ 10 · MP4/MOV · 2–30 s each, 30 s total · ≤ 200 MB each · 24–60 fps | ≤ 10 · MP3/WAV · 2–30 s each, 30 s total · ≤ 15 MB each | ≤ 50 files in total; needs at least one reference |
| Seedance 2.0 Fast | ≤ 9 | ≤ 3 · MP4/MOV · 2–15 s each, 15 s total · ≤ 50 MB total · 24–60 fps | ≤ 3 · MP3/WAV · 2–15 s each, 15 s total · ≤ 15 MB each | ≤ 12 files in total; needs an image or video |
| Seedance 2.0 Mini | ≤ 9 | ≤ 3 · MP4/MOV · 2–15 s each, 15 s total · ≤ 50 MB total · 480p–720p only · 24–60 fps | ≤ 3 · MP3/WAV · 2–15 s each, 15 s total · ≤ 15 MB each | ≤ 12 files in total; needs an image or video |
| Wan 3.0 | ≤ 4 | ≤ 5 · MP4/MOV · 1–15 s each, 15 s total · ≤ 100 MB each · 16+ fps · reference + output ≤ 30 s | ≤ 5 · MP3/WAV · 1–15 s each, 15 s total · ≤ 15 MB each | needs at least one reference |
| Wan 3.0 Prime | ≤ 4 | ≤ 5 · MP4/MOV · 1–15 s each, 15 s total · ≤ 100 MB each · 16+ fps · reference + output ≤ 30 s | ≤ 5 · MP3/WAV · 1–15 s each, 15 s total · ≤ 15 MB each | needs at least one reference |
| MiniMax H3 | ≤ 5 | ≤ 3 · MP4/MOV · 2–15 s each, 15 s total · ≤ 50 MB each · 24–60 fps | ≤ 3 · MP3/WAV · 2–15 s each, 15 s total · ≤ 15 MB each | ≤ 12 files in total; needs an image or video |
Videos must be H.264 or H.265 MP4/MOV. Limits are the strictest of the capacity each model runs on, so a request that passes here is accepted upstream. Reference images and audio don't change the price.
2. Poll for the result#
GET /api/v1/generations/{id} returns the current state. Reading a generation also nudges it forward: if a status check is due, we check before answering, so polling never returns data more than a few seconds old.
import os, time, requests
API = "https://cheapergen.com/api/v1"
headers = {"Authorization": f"Bearer {os.environ['CHEAPERGEN_API_KEY']}"}
def wait(gen_id, timeout=900):
delay, waited = 2, 0
while waited < timeout:
gen = requests.get(f"{API}/generations/{gen_id}", headers=headers).json()
if gen["status"] in ("succeeded", "failed", "canceled"):
return gen
time.sleep(delay)
waited += delay
delay = min(delay * 1.5, 10) # back off to every 10 s
raise TimeoutError(gen_id)Recommended polling intervals#
- Images: every 1–2 seconds. Most finish in under 30 seconds.
- Video: every 3–5 seconds at first, backing off to 10 seconds. A 5-second clip typically takes one to three minutes, longer at high resolutions.
- Polling counts toward your rate limit. If you run many jobs at once, prefer webhooks.
3. Use the output#
On success, output holds HTTPS URLs to the generated media. Video models return one file:
{
"output": {
"video": {
"url": "https://cdn.cheapergen.ai/outputs/gen_8c2QmX1nq0k4Lw7Z.mp4",
"content_type": "video/mp4"
}
}
}Image models return a list, one entry per generated image:
{
"output": {
"images": [
{
"url": "https://cdn.cheapergen.ai/outputs/gen_8c2QmX1nq0k4Lw7Z.png",
"content_type": "image/png"
}
]
}
}Store what you want to keep
Output URLs are served from our CDN until the generation's content expires — 1 day after it finishes for API generations by default (see Data retention). Copy outputs you want to keep into your own storage when the job succeeds.Failures and refunds#
When a generation fails, status is failed, error explains why and cost.amount_usd drops to 0 — the full hold has been returned to your balance. Transient capacity problems are retried automatically on healthy capacity before a job is marked failed, so a failure usually means the input itself was rejected (for example by content moderation).
{
"status": "failed",
"error": {
"code": "generation_failed",
"message": "The model could not generate an output for this input."
},
"cost": {
"amount_usd": 0,
"currency": "usd",
"estimated": false
}
}| error.code | What happened |
|---|---|
generation_failed | The model rejected or could not complete this input — often a content-policy refusal. Change the prompt or inputs before retrying. |
upstream_error | No capacity could complete the job after automatic retries. Safe to resubmit. |
timeout | The job did not finish within 40 minutes. Safe to resubmit. |
model_unavailable | The model was taken offline while the job was queued. Check the status page. |
Data retention#
API generations are kept for 1 day by default, counted from when they finish. After that we delete the prompt, input media, outputs and their files automatically. The generation itself stays, with its model, the parameters that set the price, cost and timestamps, so your usage history and invoices remain accurate: output becomes null, purged_at is set and content_expired explains why. Download outputs you want to keep before expires_at.
retention | Content is deleted |
|---|---|
immediate | 10 minutes after the result is delivered — your webhook accepted it, or you read the finished generation with GET — so you can still download the files. Never later than 1 hour after it finishes. |
1h | 1 hour after it finishes. |
1d | 1 day after it finishes. The default. |
7d, 30d | 7 or 30 days after it finishes. Enterprise accounts only. |
Pass retention in the request body to choose per generation. Any account can shorten it (or ask for up to 1d); 7d and 30d need an account-level retention setting — contact sales. A request for a longer period than your account allows returns 400 invalid_request with param: "retention".
{
"prompt": "A paper boat drifting down a rain-soaked street",
"resolution": "720p",
"duration": 5,
"retention": "immediate",
"webhook_url": "https://example.com/hooks/cheapergen"
}Studio and playground generations are kept until you delete them. Uploads live as long as a kept generation uses them; files that no generation uses are deleted after 1 day. Deleting your account deletes all of your generations and files. Where a model provider lets us choose, we also ask it to keep as little as possible on its side.
Deleting a generation#
Delete any finished generation with DELETE /api/v1/generations/{id}, from the generations list in the dashboard, or from the Studio viewer. Its files, prompt and inputs are removed right away and it disappears from lists; the charge stays in your billing history. See the API reference.
Canceling#
POST /api/v1/generations/{id}/cancel cancels a generation that is still queued and refunds the hold. Once a job is running the model is already working on it, so canceling returns 409 conflict and the job completes (and is billed) normally. Some models can't be canceled once queued; those also return 409.
curl -X POST https://cheapergen.com/api/v1/generations/gen_8c2QmX1nq0k4Lw7Z/cancel \
-H "Authorization: Bearer $CHEAPERGEN_API_KEY"Listing generations#
GET /api/v1/generations lists your generations, newest first, with cursor pagination. Filter by status or model; pass the returned next_cursor as cursor to get the next page. Details in the API reference.
curl "https://cheapergen.com/api/v1/generations?status=succeeded&limit=50" \
-H "Authorization: Bearer $CHEAPERGEN_API_KEY"Generations from the Studio and model-page playgrounds appear here too, with source: "playground"; API requests have source: "api".
Something unclear or missing? Email support@cheapergen.com — we read everything.