Skip to content

Generations

View .md

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.

Terminal window
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 (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.

Terminal window
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".

A storyboard takes one photo and a location. Send the location’s key and a resolution as the variant:

Terminal window
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.

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 an insufficient_balance create with the same key and body.

Missing the header answers 400 idempotency_key_required.

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.

Terminal window
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.

Terminal window
curl -X POST https://api.moan.hot/v1/generations/gen_7c2x9q0k3m1z8b4v6n5p2r0t/cancel \
-H "Authorization: Bearer $MOANHOT_API_KEY"
  • A queued generation is canceled and its hold released. You pay nothing.
  • A processing generation is stopped as soon as possible. The GPU time is already spent, so the price is charged.
  • Canceling an already canceled generation answers 200 with it.
  • Canceling a succeeded or failed generation answers 409 generation_not_cancelable.

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.

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.