How do I run one AI model through the API without building a workflow?
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.
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:
- 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, without
quoteOnly, and anIdempotency-Keyheader. - 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.
That key and one credit balance cover every model your plan includes, as one API key for every model explains.
The route runs on the same engine as workflow runs, 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 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.
curl "https://www.wireflow.ai/api/v1/generate/models?model=flux-schnell"
const res = await fetch('https://www.wireflow.ai/api/v1/generate/models?model=flux-schnell');
console.log(await res.json());
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:
{
"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:
slugornodeTypeis the value you send asmodel.- Each
idunderinputsis a key you can send ininput. Fields such astype,required,minandmaxshow what value it takes. estimatedCreditscovers a default run, so quote your real inputs in the next step.requiresPaidPlan: truemarks a model that needs a paid plan. 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.
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 }'
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);
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:
{ "credits": 1, "kind": "exact", "label": "1 credit" }
The kind field tells you how to read credits:
exact: the run is priced atcredits.up_to:creditsis 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.
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" } }'
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);
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:
{
"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. 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.
curl https://www.wireflow.ai/api/v1/generate/exec_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
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);
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:
{
"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.
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
/runcall makes a clip with each model. - A model you can swap without changing your code.
/runloads 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.
Next steps
- Polling or webhooks
Know when an AI image or video job is done: poll the run until a final status, or add a webhook to /run and get a signed POST when it ends.
- Run a workflow from your app
To run an AI workflow from your app, send its inputs to POST /api/v1/workflows/{id}/run with your API key, poll the run, then download each output file.
Related
- Generate endpoint
Run one model on your inputs in a single call, get a quote first, then poll for every output.
- One key for every model
One Wireflow API key runs every image and video model your plan includes: add the model to a workflow, call /run, and pay from one credit balance.