Generate
Run one model on your inputs in a single call, get a quote first, then poll for every output.
POST /api/v1/generate runs one catalog model on the inputs you send. No workflow to build or save: name the model, pass its inputs, get back a generation id, and poll it for the output URLs. Under the hood it runs on the same engine as Run, so credits, refunds, plan limits and content filters behave exactly as they do there.
Use a key with workflows:execute to start a generation and executions:read to read it.
The flow
- Find the model and its inputs with
GET /api/v1/generate/models. - Quote it with
quoteOnly: true. Nothing runs and nothing is charged. - Start it with the same body and an
Idempotency-Key. - Poll
GET /api/v1/generate/{id}untilstatusiscompletedorfailed, or receive a webhook.
# 1. The model and the input ids it accepts
curl "https://www.wireflow.ai/api/v1/generate/models?model=flux-schnell"
# 2. Quote: runs nothing
curl -X POST https://www.wireflow.ai/api/v1/generate \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{ "model": "flux-schnell", "input": { "prompt": "a red fox in fresh snow" }, "quoteOnly": true }'
# 200: {"data":{"quoteOnly":true,"model":{...},"quote":{"credits":1,"kind":"exact","label":"1 credit"}}}
# 3. Run it
curl -X POST https://www.wireflow.ai/api/v1/generate \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: job-8812" \
-d '{
"model": "flux-schnell",
"input": { "prompt": "a red fox in fresh snow" },
"metadata": { "endUserId": "user_1234" }
}'
# 202: {"data":{"id":"exec_abc123","status":"running","quote":{...},
# "poll":"/api/v1/generate/exec_abc123"}}
# 4. Poll
curl https://www.wireflow.ai/api/v1/generate/exec_abc123 \
-H "Authorization: Bearer sk-your-api-key"
Start a generation
POST /api/v1/generate
| Field | Type | Required | Description |
|---|---|---|---|
model |
string |
Yes | A model slug (flux-schnell) or nodeType (generate:flux_schnell) from GET /api/v1/generate/models. |
input |
object |
No | The model's inputs, keyed by input id, each in the type GET /api/v1/generate/models lists for it. Media inputs take https:// URLs. An id the model does not have is refused with the list of valid ids. |
quoteOnly |
boolean |
No | true returns the quote and runs nothing. |
metadata |
object |
No | A flat map stored on the generation and returned on every read and in the webhook. Up to 20 keys and 2048 bytes; values are strings (up to 512 characters), numbers, booleans or null. |
webhook |
object |
No | { url, secret? }. Wireflow POSTs the result there when the generation finishes. Same contract as the run webhook. |
Any other top-level key is refused with 400 unknown_parameter, so a typo never runs something you did not ask for. The whole body is capped at 256 KB; send media as URLs.
Each input is checked against the type the model list publishes for it: TEXT takes text (a list of texts when the input isArray), as long or as short as its own minLength and maxLength allow when the list publishes them, NUMBER a number inside its min/max, BOOLEAN true or false, an input with enum one of its options, IMAGE_SIZE one of its options or { "width": 1024, "height": 768 }, ARRAY a list, and IMAGE / VIDEO / AUDIO an https:// URL (a list when isArray). Every link must be written https:// with a named host (no IP address). That holds at any depth: inside media inputs and numbered media lists (element1), under link-named keys (elements[].frontal_image_url), and in text inputs named as links (*_url). Anywhere in a list or object, a value starting with // or carrying any scheme other than https is refused. Inputs are literal values: any text containing {{ (a template expression), at any depth, is refused with 400 invalid_input.
metadata.endUserId names the end user of your app this generation is for (an opaque id of up to 256 characters, never an email). It is stored on the generation so abuse can be traced to one of your users without you sharing who they are.
Response 202 Accepted
{
"data": {
"id": "exec_abc123",
"status": "running",
"model": {
"slug": "flux-schnell",
"nodeType": "generate:flux_schnell",
"name": "Flux Schnell"
},
"quote": { "credits": 1, "kind": "exact", "label": "1 credit" },
"poll": "/api/v1/generate/exec_abc123"
}
}
The quote
The quote is what you pay for this run: the price the run itself charges when it starts, for exactly these inputs, under your account's billing rules. A key with custom pricing is quoted at its own rate, and an account whose free first video covers this run is quoted 0.
kind |
Meaning |
|---|---|
exact |
The run is priced at credits. |
up_to |
credits is reserved as a ceiling and the run settles to what it really used, never more. |
The quote is the ceiling
Every generation runs with its quote as a hard credit limit. It is never charged more than the quote, even if its workflow is changed in the app after the quote (a pricier setting, an added model, a paid social post).
- A step that would take the run over the quote is stopped before it starts and takes no credits. The generation then reads
status: "failed"witherror.code: "over_quote"anderror.charged: false. - A generation has one model, so a stopped run has no outputs and
creditsreads{"charged": 0, "refunded": 0, "net": 0}. On a run that had already finished other steps, their outputs stay inoutputand their charges stay, inside the quote. - An
up_toquote is reserved at the start, then the unused part is refunded when the run settles. - A quote of
0(the free first video) is a limit of 0. If the free video can no longer be applied when the run starts (it was used in the meantime), the run stops withover_quoterather than charge you.
A start must pass two balance checks, both before anything runs: this route refuses when your balance is below the quote, and the run's own balance check (the one every run passes) refuses when it is below the standard price. Either way you get 402 insufficient_credits with the quote, and you are never charged for a generation that could not start. Keys with custom pricing must hold the standard price to start a run, even when they are billed less.
Idempotency
Send an Idempotency-Key header (up to 255 characters) on every start, one new key per generation you want. A key that started a generation stays bound to it; keys do not expire.
- The same key with the same body returns the original generation (
200, sameid,"idempotent": true). It never runs or charges a second time, so retrying after a timeout is safe. This holds even if the generation's workflow was later edited, published or deleted in the app, and even if its model has since been switched off. - The same key with a different body is refused with
409 idempotency_key_reused. - A retry while the first request is still starting gets
409 idempotency_in_progresswithRetry-After. Retry and you get the original generation. - If the run engine does not confirm a start (
503 engine_unavailable), the key stays held and is never used to start a second run. Retries readidempotency_in_progressfor 30 minutes. After that, the next retry returns the generation if it did start, and starts it if it did not and the workflow still exists. A run you start yourself on that workflow in the app is never taken for the generation. If the workflow was deleted in the meantime, the key reads409 idempotency_unresolvedfor good: use a new key. - A start the run refused outright (a
400,402or403) does not use up the key.
Read a generation
GET /api/v1/generate/{id}
Polling also advances the run, so poll every few seconds with a deadline.
Response 200 OK
{
"data": {
"id": "exec_abc123",
"status": "completed",
"model": {
"slug": "flux-schnell",
"nodeType": "generate:flux_schnell",
"name": "Flux Schnell"
},
"quote": { "credits": 1, "kind": "exact", "label": "1 credit" },
"outputs": ["https://cdn.wireflow.ai/..."],
"output": { "images": ["https://cdn.wireflow.ai/..."] },
"error": null,
"credits": { "charged": 1, "refunded": 0, "net": 1 },
"metadata": { "endUserId": "user_1234" },
"createdAt": "2026-10-25T10:00:00.000Z",
"completedAt": "2026-10-25T10:00:04.000Z"
}
}
statusispending,running,completed,failed,timeoutorcancelled.outputholds every output the model declares, keyed by its id from the model list: media as URLs, text as text, and structured values as they are.outputsis every media URL, for the common case. A text model also returnstext.creditscomes from the credit ledger:chargedis what was taken,refundedwhat was given back for a failure,netwhat it cost in the end. It can lag a few seconds behind the run.progress(0 to 100) appears while a long run reports it.
Errors
Every error is { "error": { "type", "code", "message" } }, and a refusal before anything ran adds "charged": false. Branch on code, never on message: messages are fixed sentences and never carry engine or provider detail.
Refused before anything runs (nothing is charged):
| Status | code |
When |
|---|---|---|
400 |
invalid_body |
The body is not a JSON object. |
400 |
unknown_parameter |
A top-level key this route does not take. |
400 |
invalid_model |
model is missing. |
400 |
unknown_input |
An input key the model does not have. The message lists the valid ones. |
400 |
invalid_input |
A value the model cannot take (wrong type, out of range, not an option, missing). |
400 |
invalid_metadata |
metadata is over the limits above. |
400 |
invalid_webhook |
webhook.url is not a valid https URL. |
400 |
unsupported_model |
A workflow building block, not a model. Run it inside a workflow. |
400 |
invalid_idempotency_key |
The Idempotency-Key header is longer than 255 characters. |
400 |
run_refused |
The run refused this start for another reason (the status may be any 4xx). |
402 |
insufficient_credits |
The run's balance check refused it. The body carries quote and, when known, requiredCredits and availableCredits. |
402 |
plan_required |
The model needs a paid plan. |
403 |
model_unavailable |
The model is not open yet, or is switched off. |
404 |
model_not_found |
No model by that slug or node type. |
409 |
idempotency_key_reused |
The key was used with a different body. |
409 |
idempotency_in_progress |
The first request with this key is still starting. |
409 |
idempotency_unresolved |
The first request with this key may have started a run that was never confirmed. Use a new key. |
413 |
body_too_large |
The body is over 256 KB. |
422 |
invalid_input |
Each value is fine but the model cannot run with them together (an image model sent no image, for example). |
429 |
rate_limit_exceeded |
Over your plan's request rate. Back off by Retry-After. |
500 |
internal_error |
Something failed on our side. Retry with the same Idempotency-Key. |
503 |
engine_unavailable |
The run engine did not confirm the start. Retry with the same Idempotency-Key; see Idempotency above. |
503 |
generation_unavailable |
Generations cannot start right now (the run engine or the run's credit limit is unavailable, or the generation could not be recorded). Nothing ran, and the same Idempotency-Key can be used again. Retry by Retry-After. |
A generation that started and then failed reads status: "failed" (or timeout, cancelled) with an error object that says what billing did:
{
"code": "content_filtered",
"message": "Blocked by the content filter. Try a different prompt or image.",
"charged": false,
"refunded": true
}
error.code |
When |
|---|---|
content_filtered |
The content filter blocked the prompt, an input or the output. |
generation_failed |
The model failed to produce a result. |
timed_out |
The run went past its time limit. |
cancelled |
The run was cancelled. |
over_quote |
The run needed more credits than its quote and was stopped before the step that would go over. That step took nothing. |
refunded is true when the credits for it were given back, and charged is true while any are still taken. Blocked runs are refunded, up to 2 a day on a free account.
The model list
GET /api/v1/generate/models
Public, no key needed. Every model this route runs, with what to send. A model is listed only when every output it declares can be returned by a read, and only when its quote is the price its run charges. A model the run has to measure or check before it charges (the length of an uploaded clip or audio file, a voice sample) is not offered here, and neither is one that cannot run today; run those in a workflow. For the same reason, a few models are listed without one input (a reference video they would measure first); the list shows exactly the inputs this route takes.
{
"data": {
"count": 205,
"models": [
{
"slug": "flux-schnell",
"nodeType": "generate:flux_schnell",
"name": "Flux Schnell",
"category": "Image",
"estimatedCredits": 1,
"price": "~1 credit for a default run",
"inputs": [
{ "id": "prompt", "label": "Prompt", "type": "TEXT", "required": true },
{
"id": "num_images",
"label": "Num Images",
"type": "NUMBER",
"default": 1,
"min": 1,
"max": 4,
"integer": true
},
{
"id": "output_format",
"label": "Output Format",
"type": "TEXT",
"default": "jpeg",
"enum": ["jpeg", "png"]
}
]
}
]
}
}
estimatedCredits is a default run; quote your real inputs with quoteOnly. requiresPaidPlan: true marks a model a free account cannot run. Add ?model=<slug or nodeType> for one model. Use it in CI to catch a model or input id you depend on changing.
Limits
- Rate limits are your plan's API tier, the same as Run. One generation counts as one request. See rate limits.
- Each generation is saved as a private workflow in your account, so you can open it on the canvas.
- Results are delivered by polling or by the webhook. There is no blocking wait on the start call.