---
title: Generate
description: Run one model on your inputs in a single call, get a quote first, then poll for every output.
updated: 2026-10-25
---

`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](/docs/api/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

1. Find the model and its inputs with `GET /api/v1/generate/models`.
2. Quote it with `quoteOnly: true`. Nothing runs and nothing is charged.
3. Start it with the same body and an `Idempotency-Key`.
4. Poll `GET /api/v1/generate/{id}` until `status` is `completed` or `failed`, or receive a webhook.

```bash
# 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](/docs/api/webhooks).                                                                   |

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`

```json
{
  "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"` with `error.code: "over_quote"` and `error.charged: false`.
- A generation has one model, so a stopped run has no outputs and `credits` reads `{"charged": 0, "refunded": 0, "net": 0}`. On a run that had already finished other steps, their outputs stay in `output` and their charges stay, inside the quote.
- An `up_to` quote 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 with `over_quote` rather 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`, same `id`, `"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_progress` with `Retry-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 read `idempotency_in_progress` for 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 reads `409 idempotency_unresolved` for good: use a new key.
- A start the run refused outright (a `400`, `402` or `403`) 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`

```json
{
  "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"
  }
}
```

- `status` is `pending`, `running`, `completed`, `failed`, `timeout` or `cancelled`.
- `output` holds **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.
- `outputs` is every media URL, for the common case. A text model also returns `text`.
- `credits` comes from the credit ledger: `charged` is what was taken, `refunded` what was given back for a failure, `net` what 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:

```json
{
  "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.

```json
{
  "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](/docs/api/run). One generation counts as one request. See [rate limits](/docs/api/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.

Last updated: 2026-10-25

---

Documentation index: fetch https://www.wireflow.ai/llms.txt for the full list of Wireflow docs.
