---
title: "How do I run one AI model through the API without building a workflow?"
description: "Run one AI model through the API without building a workflow: find it, quote it with quoteOnly, POST to /api/v1/generate, then poll for the output files."
updated: 2026-11-02
---

To run one AI model through the API without building a workflow, send the model and its inputs to `POST /api/v1/generate`, then poll the generation it returns until the output URLs are ready. There is nothing to build or save first.

It takes four steps:

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, without `quoteOnly`, and an `Idempotency-Key` header.
4. Poll `GET /api/v1/generate/{id}` until the generation finishes, then read its output URLs.

Steps 2 to 4 need an API key. Use a key with `workflows:execute` to start a generation and `executions:read` to read it. Create it in **Settings > API Keys**. If this is your first call, begin with the [API overview](/docs/api/overview).

That key and one credit balance cover every model your plan includes, as [one API key for every model](/docs/one-api-key-for-every-model) explains.

The route runs on the same engine as [workflow runs](/docs/api/run), so credits, refunds, plan limits and content filters behave exactly as they do there. This page walks through one run from start to finish, and the [Generate reference](/docs/api/generate) lists every field and error.

## How do I find a model and the inputs it takes?

Call `GET /api/v1/generate/models`: it is public, needs no key, and lists every model this route runs with what to send. Add `?model=` with a slug or node type to get one model.

```bash
curl "https://www.wireflow.ai/api/v1/generate/models?model=flux-schnell"
```

```javascript
const res = await fetch('https://www.wireflow.ai/api/v1/generate/models?model=flux-schnell');
console.log(await res.json());
```

```python
import requests

res = requests.get(
    "https://www.wireflow.ai/api/v1/generate/models",
    params={"model": "flux-schnell"},
)
print(res.json())
```

One model in the list looks like this, shortened to two inputs:

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

Read it like this:

- `slug` or `nodeType` is the value you send as `model`.
- Each `id` under `inputs` is a key you can send in `input`. Fields such as `type`, `required`, `min` and `max` show what value it takes.
- `estimatedCredits` covers a default run, so quote your real inputs in the next step.
- `requiresPaidPlan: true` marks a model that needs a paid plan. [Pricing](https://www.wireflow.ai/pricing) lists each plan.

## How do I see what a run will cost before it starts?

Send the generate request with `quoteOnly: true`, and the response gives the price for exactly those inputs while nothing runs and nothing is charged.

```bash
curl -X POST https://www.wireflow.ai/api/v1/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "flux-schnell", "input": { "prompt": "a red fox in fresh snow" }, "quoteOnly": true }'
```

```javascript
const res = await fetch('https://www.wireflow.ai/api/v1/generate', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'flux-schnell',
    input: { prompt: 'a red fox in fresh snow' },
    quoteOnly: true,
  }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
console.log(data.quote);
```

```python
import requests

res = requests.post(
    "https://www.wireflow.ai/api/v1/generate",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "model": "flux-schnell",
        "input": {"prompt": "a red fox in fresh snow"},
        "quoteOnly": True,
    },
)
res.raise_for_status()
print(res.json()["data"]["quote"])
```

The `200` response holds a `quote`, which is what you pay for this run under your account's billing rules:

```json
{ "credits": 1, "kind": "exact", "label": "1 credit" }
```

The `kind` field tells you how to read `credits`:

- `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 unused part is refunded when the run settles.

The quote is also a hard limit, so a generation is never charged more than its quote. A step that would take the run over the quote is stopped before it starts and takes no credits, and the generation reads `status: "failed"` with `error.code: "over_quote"`.

A start is refused with `402 insufficient_credits` when your balance is below the quote or below the standard price. You are never charged for a generation that could not start.

## How do I start a generation?

Send `POST /api/v1/generate` with the body you quoted, minus `quoteOnly`, and add an `Idempotency-Key` header with a new key for this generation.

```bash
curl -X POST https://www.wireflow.ai/api/v1/generate \
  -H "Authorization: Bearer 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" } }'
```

```javascript
const res = await fetch('https://www.wireflow.ai/api/v1/generate', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'job-8812',
  },
  body: JSON.stringify({
    model: 'flux-schnell',
    input: { prompt: 'a red fox in fresh snow' },
  }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
console.log(data.id, data.poll);
```

```python
import requests

res = requests.post(
    "https://www.wireflow.ai/api/v1/generate",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Idempotency-Key": "job-8812",
    },
    json={
        "model": "flux-schnell",
        "input": {"prompt": "a red fox in fresh snow"},
    },
)
res.raise_for_status()
data = res.json()["data"]
print(data["id"], data["poll"])
```

The response is `202 Accepted`, with the generation `id` and the path to poll:

```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 start call does not wait for the result, so keep the `id` for the last step.

In the body, `model` takes a `slug` or a `nodeType`, and `input` holds the model's inputs keyed by input id. Media inputs take `https://` URLs, and the whole body is capped at 256 KB. The only other top-level keys are `quoteOnly`, `metadata` and `webhook`. Anything else is refused with `400 unknown_parameter`.

These refusals happen before anything runs, so nothing is charged:

| Refusal | What it means |
| --- | --- |
| `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 or missing. |
| `402 insufficient_credits` | The balance check refused the start. The body carries `quote` and, when known, `requiredCredits` and `availableCredits`. |
| `402 plan_required` | The model needs a paid plan. |
| `404 model_not_found` | No model by that slug or node type. |
| `422 invalid_input` | Each value is fine, but the model cannot run with them together, such as an image model sent no image. |

One generation counts as one request against your plan's [rate limits](/docs/api/rate-limits). Over that rate you get `429 rate_limit_exceeded`, so back off by `Retry-After`.

## How do I retry without paying twice?

Send the same request again with the same `Idempotency-Key`: you get the original generation back, and it never runs or charges a second time.

That makes it safe to retry after a timeout. Send a key of up to 255 characters on every start, with one new key per generation you want. A key that started a generation stays bound to it and does not expire, so store it with your own job.

| What happened | What you get | What to do |
| --- | --- | --- |
| You sent the same key and the same body | `200`, the same `id` and `"idempotent": true` | Poll that `id` |
| You retried while the first request was still starting | `409 idempotency_in_progress` with `Retry-After` | Retry, and you get the original generation |
| You sent the same key with a different body | `409 idempotency_key_reused` | Use a new key for a new generation |
| Something failed on our side | `500 internal_error` | Retry with the same key |
| The run engine did not confirm the start | `503 engine_unavailable` | Retry with the same key. Retries read `idempotency_in_progress` for 30 minutes, then the next retry returns the generation if it did start, and starts it if it did not and the workflow still exists |
| Generations cannot start right now | `503 generation_unavailable` | Nothing ran. Retry by `Retry-After` with the same key |
| The first request may have started a run that was never confirmed | `409 idempotency_unresolved` | Use a new key |

A start refused outright with a `400`, `402` or `403` does not use up the key.

## How do I get the output files?

Poll `GET /api/v1/generate/{id}` every few seconds until the generation finishes, then read the file URLs from `outputs`.

Polling also advances the run, so keep polling and set a deadline. `status` is `pending`, `running`, `completed`, `failed`, `timeout` or `cancelled`, and your loop stops on the last four. The 5-second gap and 10-minute deadline below are example values, so set your own.

```bash
curl https://www.wireflow.ai/api/v1/generate/exec_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```javascript
const id = 'exec_abc123';
const finished = ['completed', 'failed', 'timeout', 'cancelled'];
const deadline = Date.now() + 10 * 60 * 1000;

let generation;
while (Date.now() < deadline) {
  const res = await fetch(`https://www.wireflow.ai/api/v1/generate/${id}`, {
    headers: { Authorization: 'Bearer YOUR_API_KEY' },
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  generation = (await res.json()).data;
  if (finished.includes(generation.status)) break;
  await new Promise((resolve) => setTimeout(resolve, 5000));
}
console.log(generation.status, generation.outputs, generation.error);
```

```python
import time
import requests

generation_id = "exec_abc123"
finished = {"completed", "failed", "timeout", "cancelled"}
deadline = time.time() + 600

while time.time() < deadline:
    res = requests.get(
        f"https://www.wireflow.ai/api/v1/generate/{generation_id}",
        headers={"Authorization": "Bearer YOUR_API_KEY"},
    )
    res.raise_for_status()
    generation = res.json()["data"]
    if generation["status"] in finished:
        break
    time.sleep(5)

print(generation["status"], generation.get("outputs"), generation.get("error"))
```

A finished read looks like this, shortened:

```json
{
  "data": {
    "id": "exec_abc123",
    "status": "completed",
    "outputs": ["https://cdn.wireflow.ai/..."],
    "output": { "images": ["https://cdn.wireflow.ai/..."] },
    "error": null,
    "credits": { "charged": 1, "refunded": 0, "net": 1 }
  }
}
```

| Field | What it holds |
| --- | --- |
| `outputs` | Every media URL, for the common case. |
| `output` | 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. |
| `text` | Also returned by a text model. |
| `credits` | `charged` is what was taken, `refunded` what was given back for a failure, and `net` what it cost in the end. |
| `progress` | A number from 0 to 100, which appears while a long run reports it. |

If you would rather not poll, add `webhook` with `{ url, secret? }` to the start body. Wireflow POSTs the result there when the generation finishes, on the same contract as the [run webhook](/docs/api/webhooks).

If `status` is `failed`, `timeout` or `cancelled`, read `error` instead. The `error.code` values are `content_filtered`, `generation_failed`, `timed_out`, `cancelled` and `over_quote`, so branch on `code`, never on `message`. `refunded` is `true` when the credits were given back, and `charged` is `true` while any are still taken.

**Check that it worked:** `status` reads `completed`, `error` is `null`, and `outputs` holds the URL of your file (a text model also returns `text`). Open the URL to see the file. `credits.net` then shows what the run cost, no more than the quote, though it can lag a few seconds behind the run.

## When should I build a workflow instead?

Build a workflow when you need something this route does not run: it takes one model per generation and only the inputs its model list shows.

- **A model that is not listed.** 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, so run it in a workflow. A few models are also listed without one input, a reference video they would measure first.
- **A workflow building block.** The route refuses one with `400 unsupported_model`, so run it inside a workflow.
- **Several models from one call.** Wire one Text Input into the **Prompt** of each video node in the same workflow, and one `/run` call makes a clip with each model.
- **A model you can swap without changing your code.** `/run` loads the saved workflow on every call, so when you swap the model node on the canvas, the next run uses the new model with the same inputs.

Each generation is saved as a private workflow in your account, so you can open it on the canvas.

To call a saved workflow from code, follow [trigger a workflow from your app](/docs/trigger-a-workflow-from-your-app).

Last updated: 2026-11-02

---

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