Generations
A generation is one run of one effect on your inputs. You create it, it moves through a short status lifecycle, and it ends with output files or an error.
Create
Section titled “Create”curl https://api.moan.hot/v1/generations \ -H "Authorization: Bearer $MOANHOT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f1c1b8e-6a43-4c8e-9d61-2a1f7d1f9b20" \ -d '{ "effect": "EFFECT_SLUG", "variant": { "duration": "5s", "resolution": "640p", "audio": false, "prep": false }, "inputs": { "image": "file_3kq9c0m2x7z1b8v4n6p5r2t0" }, "metadata": { "user_id": "u_829" }, "client_reference_id": "order-1042", "webhook_url": "https://example.com/webhooks/moanhot" }'| Field | Required | Meaning |
|---|---|---|
effect |
yes | The effect slug |
variant |
yes | The axes of one listed variant: duration, resolution, audio, prep, and location for a storyboard |
inputs |
yes | One entry per input slot. The value is a file_ id or a public https:// image URL |
metadata |
no | Up to 20 string pairs, 500 characters each, returned on every read and webhook |
client_reference_id |
no | Your own id, up to 128 characters. Filter by it in GET /generations |
webhook_url |
no | An extra https destination for this generation’s events. See Webhooks |
Unknown fields are rejected with 400 invalid_request. The answer is 202 Accepted, a Location header and the generation in queued:
{ "id": "gen_7c2x9q0k3m1z8b4v6n5p2r0t", "object": "generation", "status": "queued", "mode": "live", "effect": "EFFECT_SLUG", "type": "video", "variant": { "duration": "5s", "resolution": "640p", "audio": false, "prep": false }, "variant_key": "5s.640p.noaudio.noprep", "price_micros": 100000, "charged_micros": 0, "price_list_version": 3, "metadata": { "user_id": "u_829" }, "client_reference_id": "order-1042", "outputs": [], "error": null, "api_key_prefix": "mh_live_4Hk2", "created_at": "2026-10-20T12:00:00Z", "started_at": null, "finished_at": null, "expires_at": null}At create we validate the request, check every input image, and hold the variant price on your balance. If your available balance is too low the call answers 402 insufficient_balance and nothing is created.
An image effect
Section titled “An image effect”An image effect (type: "image") takes one photo and returns one image. Its variants differ only by resolution: 1mp is the standard output, 2mp the upscaled one, each with its own price. Leave duration, audio and prep out; sending one of them answers 422 variant_unavailable.
curl https://api.moan.hot/v1/generations \ -H "Authorization: Bearer $MOANHOT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0b7c2f4e-1d9a-4e57-8c3b-5f6a7e8d9c01" \ -d '{ "effect": "IMAGE_EFFECT_SLUG", "variant": { "resolution": "2mp" }, "inputs": { "image": "file_3kq9c0m2x7z1b8v4n6p5r2t0" } }'The hold, capture, webhooks and retention are the same as for video. A succeeded image generation has one output of kind: "image".
Storyboards
Section titled “Storyboards”A storyboard takes one photo and a location. Send the location’s key and a resolution as the variant:
curl https://api.moan.hot/v1/generations \ -H "Authorization: Bearer $MOANHOT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0b5e2c1a-3f7d-4a8e-b2c9-71d4e6f8a903" \ -d '{ "effect": "STORYBOARD_SLUG", "variant": { "location": "beach", "resolution": "720p" }, "inputs": { "image": "file_3kq9c0m2x7z1b8v4n6p5r2t0" } }'The price of that location and resolution is held at create, like any other generation, and charged once when the film is delivered. A storyboard runs far longer than a video effect, often well over half an hour (see the variant’s typical_seconds); you get generation.started when work begins and generation.succeeded with one video output, the finished film. If the photo, or any frame we make from it, cannot be used, the generation ends failed with input_rejected and nothing is charged. The available locations are on the effect: see Storyboards.
Idempotency
Section titled “Idempotency”POST /generations requires an Idempotency-Key header: 1 to 255 printable ASCII characters. Use a fresh UUID for each logical create and reuse it for every retry of that create.
- Same key, same body: you get the first response again, with
Idempotent-Replayed: true. No second generation, no second hold. - Same key, different body:
409 idempotency_key_mismatch. - Same key while the first request is still running:
409 idempotency_in_progress. Retry after a second. - Keys are remembered for 24 hours per account and mode, across all your keys.
- Errors that may change on retry (
401,402,403,429,5xx) are not stored, so retrying them with the same key runs fresh. After a top-up, retry aninsufficient_balancecreate with the same key and body.
Missing the header answers 400 idempotency_key_required.
Status lifecycle
Section titled “Status lifecycle”| Status | Meaning | Money |
|---|---|---|
queued |
Accepted, waiting for a GPU worker | Price held |
processing |
A worker started the job | Price held |
succeeded |
Outputs are stored and ready | Price charged |
failed |
Ended with error. Never your fault, never charged |
Hold released |
canceled |
You canceled it | Released if it had not started; charged if it had |
queued ──> processing ──> succeeded │ ├──────> failed │ └──────> canceled (charged) └──────────────────> canceled (free)succeeded, failed and canceled are terminal. queue_seconds and execution_seconds tell you where the time went.
When a generation fails, error.code is one of:
error.code |
Meaning |
|---|---|
input_rejected |
The input could not be processed. No reason is given |
gpu_failure |
The GPU job failed (storyboards first retry a brief GPU failure up to twice) |
timeout |
The job did not finish in time |
upstream_unavailable |
Storage or the GPU control plane failed mid-run |
internal_error |
Something broke on our side |
effect_retired |
The effect retired before the job could start |
You are never charged for a failed generation.
Read and list
Section titled “Read and list”curl https://api.moan.hot/v1/generations/gen_7c2x9q0k3m1z8b4v6n5p2r0t \ -H "Authorization: Bearer $MOANHOT_API_KEY"
curl "https://api.moan.hot/v1/generations?status=succeeded&client_reference_id=order-1042" \ -H "Authorization: Bearer $MOANHOT_API_KEY"GET /generations lists newest first. Filters (status, effect, client_reference_id, created_gte, created_lt) combine with AND, and pagination uses limit, starting_after, ending_before and has_more.
Cancel
Section titled “Cancel”curl -X POST https://api.moan.hot/v1/generations/gen_7c2x9q0k3m1z8b4v6n5p2r0t/cancel \ -H "Authorization: Bearer $MOANHOT_API_KEY"- A
queuedgeneration is canceled and its hold released. You pay nothing. - A
processinggeneration is stopped as soon as possible. The GPU time is already spent, so the price is charged. - Canceling an already canceled generation answers
200with it. - Canceling a
succeededorfailedgeneration answers409 generation_not_cancelable.
Outputs
Section titled “Outputs”A succeeded generation has outputs[]:
{ "kind": "video", "url": "https://media.api.moan.hot/...", "expires_at": "2026-10-20T13:02:11Z", "mime_type": "video/mp4", "width": 640, "height": 960, "duration_seconds": 5.0, "bytes": 2483112 }kind is video, image or thumbnail. Output URLs are signed and valid for one hour. Every GET and every webhook carries freshly signed URLs, so do not store the URL: store the generation id and read it again when you need the file, or copy the file to your own storage.
Retention
Section titled “Retention”| What | Kept for |
|---|---|
Uploaded files (POST /files) |
24 hours from upload |
| Inputs used by a generation | 7 days after it finishes |
| Outputs | 30 days after it finishes |
| Generation records | Kept; outputs becomes [] once files are deleted |
The generation’s expires_at tells you when its outputs will be deleted. Copy anything you need to keep.