---
title: Generate images from text
url: https://base_url.placeholder/docs/ops/generate
group: Concepts
---

# Generate images from text

`generate` turns a prompt into one to ten images, each stored as an [asset](https://base_url.placeholder/docs/assets) with its own id and, if you publish it, a stable URL. Of the AI ops in the [catalogue](https://base_url.placeholder/docs/ops) it is the one that takes no input image.

## What it does

A model reads `prompt` and writes `count` images. Each becomes an asset, so the result is an id you can hand to the next op, publish, download or delete — not a data URL you have to find somewhere to put. The model's own knobs go in `parameters`: on the default model, `aspectRatio` and `imageSize` — and `imageSize` is also what it is priced by. The low end of its price range is its smallest size; a call that names none is priced at 1K, which the [dry run](https://base_url.placeholder/docs/jobs#price) shows before you spend.

Reach for it when the picture does not exist yet: illustrations for generated articles, product mock-ups, thumbnails per row of a spreadsheet. When you already have the image and want it changed, that is [edit](https://base_url.placeholder/docs/ops/edit). When you want the same layout filled with different data — a price card, an OG image per post — do not generate it: render it from a [template](https://base_url.placeholder/docs/templates), which is deterministic, exact and free on a paid plan.

## Call it

One op, six surfaces, one vocabulary: the `op` name below is the same string everywhere — the SDKs' named helpers only spell it in their language's case, `ops.generate` in JavaScript — because every surface enumerates `GET /api/v1/ops` instead of carrying its own list. The request is the catalogue's own `example` for this op, so it is the one the service has checked.

#### JavaScript

```js
// pnpm add imagestep
const job = await client.ops.generate("a red bicycle on a white background", { count: 1, wait: true });
const [out] = await client.jobs.outputs(job);
```

#### Python

```python
# pip install imagestep
job = client.ops.generate("a red bicycle on a white background", count=1, wait=True)
[out] = client.jobs.outputs(job)
```

#### CLI

```sh
imagestep jobs submit --op generate -p "a red bicycle on a white background" --count 1 --wait
```

#### curl

```sh
# "wait" holds the response until the job is done (60 s at most): 200 with the finished job,
# or 202 with the handle — then GET /api/v1/jobs/<id>?wait=30 waits again
curl -sS -X POST https://api.imagestep.dev/api/v1/jobs \
  -H "Authorization: ApiKey $IMAGESTEP_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"op":"generate","prompt":"a red bicycle on a white background","count":1,"wait":30}'
```

#### MCP

```text
# tool call
generate  {"prompt":"a red bicycle on a white background","count":1}
```

#### n8n

```json
{
  "nodes": [
    {
      "parameters": {
        "resource": "op",
        "operation": "run",
        "op": "generate",
        "inputMode": "none",
        "prompt": "a red bicycle on a white background",
        "count": 1
      },
      "name": "ImageStep",
      "type": "n8n-nodes-imagestep.imageStep",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    }
  ],
  "connections": {}
}
```

The [n8n](https://base_url.placeholder/docs/n8n) tab is a workflow to paste onto the canvas: the **generate** entry of the node's Op dropdown, which is filled from the same catalogue. Full references: [SDKs](https://base_url.placeholder/docs/sdk), [CLI](https://base_url.placeholder/docs/cli), [MCP](https://base_url.placeholder/docs/mcp), [REST](https://base_url.placeholder/docs/api).

## Parameters, model and price

The entry as the catalogue stood when this page was built; `GET https://api.imagestep.dev/api/v1/ops/generate` has it live — the same entry the SDKs, the MCP server and the n8n node enumerate. The rows are fields of the request body. `parameters` goes to the model: its keys are the model's own, listed under `parameters` for each model in `GET /api/v1/ai-models`, and a key the model does not declare, or a value outside its options or bounds, is `400 invalid_param` naming it.

### generate

ai job type ai-generate · typically ~10 s per item · input scaled to ≤ 1536 px on the long edge

Text → image. Returns one asset per generated image.

| Parameter | Type | What it does |
| --- | --- | --- |
| prompt | string | What to draw. |
| count | integer | Images to generate (1–10). default 1 |
| model | string | A model whose categories include 'image_generate' — GET /api/v1/ai-models lists them. Any other model is 400 invalid_param, with the ones that fit in details.allowed. default google/gemini-3.1-flash-image-preview |
| parameters | object | Passed to the model; GET /api/v1/ai-models lists each model's own, and a key or value it does not declare is 400 invalid_param. The default model reads aspectRatio and imageSize — the output size, whatever the input's, and what its price depends on. |

AI op: per-item USD from the model's price; credits are charged per item. Price a batch with POST /api/v1/jobs?dryRun=true before spending. Default model google/gemini-3.1-flash-image-preview: $0.0567 - $0.1903 per item.

That is how it is priced. What one request costs — another model, a bigger scale factor, forty images — is the [dry run](https://base_url.placeholder/docs/jobs#price), which prices the exact body you are about to send without creating anything.

## In a workflow

The hard part of generating at scale is not the call, it is keeping a batch on-brand. A [preset with subjects](https://base_url.placeholder/docs/presets#subjects) is the answer: the reference images pin the geometry, the descriptor pins the words, and every run expands `{{subject.hero}}` into the same description.

Save the subject once:

```json
{
  "name": "Bottle shots",
  "slug": "bottle-shots",
  "subjects": [
    {
      "name": "hero",
      "referenceAssetIds": [
        "<product-asset-id>"
      ],
      "descriptor": "a matte black water bottle"
    }
  ],
  "steps": [
    {
      "op": "generate",
      "prompt": "{{subject.hero}} on a rooftop at golden hour"
    }
  ]
}
```

#### JavaScript

```js
import { readFile } from "node:fs/promises";

const preset = await client.presets.create(JSON.parse(await readFile("bottle-shots.json", "utf8")));
console.log(preset.slug, preset.version); // "bottle-shots" 1
```

#### Python

```python
import json

preset = client.presets.create(json.load(open("bottle-shots.json")))
print(preset["slug"], preset["version"])  # "bottle-shots" 1
```

#### CLI

```sh
imagestep preset create -f bottle-shots.json -o json
```

#### curl

```sh
curl -s https://api.imagestep.dev/api/v1/presets -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" -d @bottle-shots.json
```

#### MCP

```text
# tool call — an agent has no disk to read, so the document travels in the call
save_preset  {"name":"Bottle shots","slug":"bottle-shots","steps":[{"op":"generate","prompt":"{{subject.hero}} on a rooftop at golden hour"}],"subjects":[{"name":"hero","referenceAssetIds":["<product-asset-id>"],"descriptor":"a matte black water bottle"}]}
```

Then every run is the same bottle. The preset starts from a prompt, so a run takes no assets — on every surface, the preset alone:

#### JavaScript

```js
const price = await client.presets.run("bottle-shots", [], { dryRun: true }); // nothing is created
console.log(price.estimatedCredits, price.steps);

const job = await client.presets.run("bottle-shots", [], { wait: true });
const outputs = await client.jobs.outputs(job);
```

#### Python

```python
price = client.presets.run("bottle-shots", [], dry_run=True)  # nothing is created
print(price["estimatedCredits"], price.get("steps"))

job = client.presets.run("bottle-shots", [], wait=True)
outputs = client.jobs.outputs(job)
```

#### CLI

```sh
imagestep jobs estimate --preset-id bottle-shots
imagestep jobs submit --preset-id bottle-shots --wait -o json
```

#### curl

```sh
# price it — the same body, nothing is created
curl -s "https://api.imagestep.dev/api/v1/jobs?dryRun=true" -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
  -d '{"presetId":"bottle-shots","assetIds":[]}'

# run it; "wait" holds the response until the job is done (60 s at most, then 202 and you wait again)
curl -s https://api.imagestep.dev/api/v1/jobs -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
  -d '{"presetId":"bottle-shots","assetIds":[],"wait":30}'
```

#### MCP

```text
# tool calls — the first prices it and creates nothing
run_preset  {"preset":"bottle-shots","dry_run":true}
run_preset  {"preset":"bottle-shots"}
```

Two more things worth wiring once: an [idempotency key](https://base_url.placeholder/docs/jobs#idempotency), so a retried request returns the first submission instead of paying twice, and a [dry run](https://base_url.placeholder/docs/jobs#price) in front of anything a user triggers, so you can refuse before you spend.

## Limits

One to ten images per call, and the prompt is the model's contract, not ours — what it refuses to draw and how literally it reads you belong to the model you chose. Generated images carry no EXIF, so the [capture-date filters](https://base_url.placeholder/docs/assets#record) mean nothing for them. When a provider is busy or rate-limits, the job retries the item itself; one that still fails settles as `provider_unavailable` — retryable, not charged — and is the one to [resume](https://base_url.placeholder/docs/jobs#failure), not a reason to change the prompt.

## FAQ

**How many images can one call make?**

Up to ten, through count. Each one is a separate item of the job: it settles on its own, and an item that fails is not charged and does not cost you the rest.

**Can I keep a character or a product consistent across generations?**

Yes — that is what preset subjects are for. Save reference images and a written descriptor once, then write {{subject.name}} in the prompt; the images pin the geometry and the words pin the description.

**Which models can I use?**

Any model whose categories include image_generate — GET /api/v1/ai-models lists them with their categories; another is 400 invalid_param, with the ones that fit in details.allowed. Every model is priced per image in USD; /pricing lists them by op.

**How do I control the aspect ratio?**

Through parameters, which go to the model. The default model reads aspectRatio and imageSize; GET /api/v1/ai-models lists every model's own parameters with their allowed values. A key the model does not declare, or a value it does not take, is 400 invalid_param naming it — before anything is spent.

**Where does the image end up?**

As an asset in your account, with metadata and a thumbnail once ingest finishes. Publish it and it gets a stable CDN URL at full size; leave it unpublished and only your key can read it.
