Jobs
A job is one op — or one preset — over one or more assets. It is the thing that owns the retry, the settlement, the cancellation and the webhook: every AI op is a job, because a provider call needs an owner, and a deterministic op is a job whenever you want the result kept as an asset. A job has items, one per input (times one per variant), and each item succeeds or fails on its own.
Submitting
POST /api/v1/jobs with an op from the catalogue, a presetId, or the steps a preset would store, inline — plus the inputs. The service derives the job type from what you sent, so you never send one.
JavaScript
import { ImageStep } from "imagestep";
const client = new ImageStep({ apiKey: process.env.IMAGESTEP_API_KEY });
const job = await client.ops.run("resize", { assetIds: ["ast_1a2b", "ast_3c4d"], parameters: { width: 1200 } });
console.log(job.id, job.status); // save the idPython
from imagestep import ImageStep
client = ImageStep() # reads IMAGESTEP_API_KEY
job = client.ops.run("resize", asset_ids=["ast_1a2b", "ast_3c4d"], parameters={"width": 1200})
print(job["id"], job["status"]) # save the idCLI
imagestep jobs submit --op resize --asset-ids ast_1a2b,ast_3c4d --params '{"width":1200}' -o jsoncurl
curl -s https://api.imagestep.dev/api/v1/jobs -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"op":"resize","assetIds":["ast_1a2b","ast_3c4d"],"parameters":{"width":1200}}'MCP
# tool call — "wait": false hands the job back at once; by default a write tool waits for it
transform {"op": "resize", "asset_ids": ["ast_1a2b", "ast_3c4d"], "parameters": {"width": 1200}, "wait": false}The answer is the job document — 201, and the handle for everything after. Save the id. A job that could start at once is already PROCESSING; one that has to wait behind your other running jobs of the same type answers PENDING and starts on its own (lifecycle).
{
"id": "job_18b6672add8a48758c4924a61c042414",
"type": "process",
"op": "resize",
"status": "PROCESSING",
"totalItems": 2,
"submittedItems": 0,
"settledItems": 0,
"completedItems": 0,
"failedItems": 0,
"cancelledItems": 0,
"pipelines": [
{ "name": "op:resize", "steps": [{ "operation": "resize", "params": { "width": 1200, "fit": "inside", "withoutEnlargement": true } }] }
],
"actualCredits": 0,
"presetName": "op:resize",
"attemptNumber": 1,
"rootJobId": "job_18b6672add8a48758c4924a61c042414",
"createdAt": "2026-09-17T15:27:01.365787172Z",
"submittedAt": "2026-09-17T15:27:01.371204518Z",
"expiresAt": "2026-10-17T15:27:01.366Z",
"items": [
{ "index": 0, "status": "PENDING", "sourceAssetId": "ast_1a2b" },
{ "index": 1, "status": "PENDING", "sourceAssetId": "ast_3c4d" }
]
}What the body looks like for one particular op is on the catalogue: every entry of the ops page carries an example the service has validated, in every surface. The shapes that are about the job rather than the op:
| to | the body |
|---|---|
| run a preset, pinned to version 3 | {"presetId": "product-cutout@3", "assetIds": ["ast_1a2b"]} |
| run a chain once, without saving a preset | {"steps": [{"op": "remove_bg"}, {"op": "resize", "parameters": {"width": 1200}}], "assetIds": ["ast_1a2b"]} |
| run an op on another model | {"op": "upscale", "assetIds": ["ast_1a2b"], "model": "replicate/nightmareai/real-esrgan"} |
| put the outputs in a collection | {"op": "remove_bg", "assetIds": ["ast_1a2b"], "collection": "spring-sale"} |
| render a template — rows in, no assets | {"op": "render_template", "templateId": "builtin-template-og-image", "items": [{"title": "Hello"}]} |
Every field the body may carry is in request fields, at the end of this page.
Price it first: the dry run
The same body to POST /api/v1/jobs?dryRun=true runs everything a real submit runs — preset lookup, model validation, asset resolution, item count, per-item price — and stops before the first write. It cannot drift from what you would be charged, and it doubles as validation: a bad parameter or a missing asset is refused here, without spending a job to find out.
JavaScript
const price = await client.ops.estimate("remove_bg", { assetIds: ["ast_1a2b", "ast_3c4d"] });Python
price = client.ops.estimate("remove_bg", asset_ids=["ast_1a2b", "ast_3c4d"])CLI
imagestep jobs estimate --op remove_bg --asset-ids ast_1a2b,ast_3c4d -o jsoncurl
curl -s "https://api.imagestep.dev/api/v1/jobs?dryRun=true" -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
-d '{"op":"remove_bg","assetIds":["ast_1a2b","ast_3c4d"]}'MCP
# tool call
transform {"op": "remove_bg", "asset_ids": ["ast_1a2b", "ast_3c4d"], "dry_run": true}Images you have not uploaded yet are priced by how many: imageCount on the dry run stands for that many more assetIds, because the price depends on the op, model and parameters, never on the pixels — so nothing is uploaded to ask. The CLI's jobs estimate --image-count, the SDKs' imageCount / image_count, MCP's dry_run over urls or file_paths and n8n's Dry Run over a Binary File all send it. A submit needs the images themselves, and answers the count with 400 invalid_param.
An AI op is priced in credits:
{
"type": "ai-edit",
"model": "fal-ai/bria/background/remove",
"presetName": "op:remove_bg",
"totalItems": 2,
"costPerItem": 227,
"estimatedCredits": 454,
"creditBalance": 0,
"sufficientCredit": false,
"assetCountLeft": 198,
"maxInputEdge": 2048
}A deterministic op costs none, and is counted against the plan's monthly allowance instead:
{
"type": "process",
"totalItems": 2,
"costPerItem": 0,
"estimatedCredits": 0,
"creditBalance": 0,
"sufficientCredit": true,
"assetCountLeft": 198,
"processCountLeft": 198,
"overageRuns": 0,
"overageCredits": 0
}The account's limits are reported, not enforced: a dry run answers 200 either way — as the first one above does, from an account with no credits — and says what the real submit would answer.
| the dry run says | the submit answers | what to do |
|---|---|---|
| sufficientCredit: false | 402 insufficient_credit | top up, or send fewer items; estimatedCredits is what it needs |
| assetCountLeft below totalItems | 422 asset_count_exceeded | every output is a new asset: delete some, or upgrade |
| overageRuns above 0 | the runs past the Free plan's allowance are paid from your balance — part of estimatedCredits, so sufficientCredit covers them | nothing, if you mean to pay them; otherwise wait for the next period, or upgrade |
Credits are the unit of AI work; the catalogue's creditsPerUsd turns them into dollars. Deterministic ops are unlimited on paid plans; on Free they count against the monthly allowance, and the runs past it are paid in credits too. Every field of the answer is in dry-run fields.
Waiting for it
A job is asynchronous; getting its result does not have to be a poll loop. The service waits for you: wait on the submit holds the response until the job is done — 200 with the finished job, or 202 with the same handle when the window closes first. Finished means COMPLETED, FAILED or CANCELLED, so read status. One call, one result, and still a job: charged, retried and replayable exactly as without it.
JavaScript
// resolves with the finished job; throws JobFailedError if it ended any other way than COMPLETED
const job = await client.ops.removeBg("ast_1a2b", { wait: true });Python
job = client.ops.remove_bg("ast_1a2b", wait=True) # raises JobFailedError unless it COMPLETEDCLI
# exit 0 COMPLETED · 1 FAILED or CANCELLED · 2 still running at --timeout (nothing is cancelled)
imagestep jobs submit --op remove_bg --asset-ids ast_1a2b --wait --timeout 120 -o jsoncurl
# 200 → the finished job · 202 → still running after 30 s, the same handle
curl -s https://api.imagestep.dev/api/v1/jobs -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"op":"remove_bg","assetIds":["ast_1a2b"],"wait":30}'MCP
# tool call — a write tool waits by default; when wait_seconds runs out the answer is the handle with timedOut: true
transform {"op": "remove_bg", "asset_ids": ["ast_1a2b"], "wait_seconds": 60}To wait on a job you already have — after a 202, after a batch, from another process:
JavaScript
const job = await client.jobs.wait("<job-id>", { timeoutMs: 120_000, onProgress: (j) => console.log(j.status) });Python
job = client.jobs.wait("<job-id>", timeout=120, on_progress=lambda j: print(j["status"]))CLI
imagestep jobs wait <job-id> --timeout 120 -o jsoncurl
# always 200: read "status". Repeat while it is not COMPLETED, FAILED or CANCELLED
curl -s "https://api.imagestep.dev/api/v1/jobs/<job-id>?wait=60" -H "Authorization: ApiKey $IMAGESTEP_API_KEY"MCP
# tool call
job_status {"job_id": "<job-id>"}- The service holds a request open for 60 s at most, and clamps a longer
waitrather than refusing it. The SDKs and the CLI ask again until their own timeout, sowait: truecan outlast a minute; bare REST repeats the read. - A submit waits only for a job of one item — a batch is what a webhook is for — and a batch simply answers
201at once. The read waits on any job. How long one item usually takes is each op'stypicalSecondsin the catalogue. - One account holds at most 8 waits open. A waited read past that is
429 rate_limited(details.reasonaccount_concurrency, withRetry-After), and a server holding all the waits it will is503 provider_unavailable— both retryable. A waited submit is never refused for either: the job exists, so it answers at once with the handle. - A wait that runs out is not a failure: the job keeps running and is charged for what completes. Wait again with the same id; never submit the same work twice because a client timed out — see submitting exactly once.
- In n8n it is Wait for Result on the node, or the ImageStep Trigger, which fires on the webhook and waits for nothing.
Outputs
Each completed item has a resultAssetId — a new asset with lineage pointing back at this job, its source asset and the preset version it ran; the asset a job read is never changed. Publish the ones you want a URL for. REST has no “outputs” endpoint: a run's products are GET /api/v1/assets?jobId=, one page at a time, and the SDKs, the CLI and MCP collect them for you.
JavaScript
const outputs = await client.jobs.outputs(job); // the result assets, in item order
const published = await client.assets.publish(outputs.map((a) => a.id)); // each now has a publicUrlPython
outputs = client.jobs.outputs(job) # the result assets, in item order
published = client.assets.publish([a["id"] for a in outputs]) # each now has a publicUrlCLI
imagestep jobs outputs <job-id> -o json # [{item, assetId, name, status, dimension, publicUrl}, …]
imagestep jobs outputs <job-id> --download ./out # …or the bytescurl
# the run's products are one listing, filtered by job — past 100, follow meta.nextCursor
curl -s "https://api.imagestep.dev/api/v1/assets?jobId=<job-id>" -H "Authorization: ApiKey $IMAGESTEP_API_KEY" | jq -r '.data[].id'
curl -s https://api.imagestep.dev/api/v1/assets/update -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
-d '{"ids":["<result-asset-id>"],"published":true}'MCP
# tool call
job_status {"job_id": "<job-id>", "publish": true}One completed item, as the job document carries it:
{
"index": 0,
"status": "COMPLETED",
"sourceAssetId": "ast_1a2b",
"resultAssetId": "ast_9x8y",
"startedAt": "2026-09-17T15:27:01.458315964Z",
"finishedAt": "2026-09-17T15:27:01.467171214Z",
"durationMs": 8,
"credits": 0
}A job carries its first 100 items and no more. A batch takes up to 10,000 inputs — more items still, with variants — so the document would otherwise be megabytes, re-sent on every wait. When there are more, the job says itemsTruncated: true and the rest are a page at a time from GET /api/v1/jobs/{id}/items (?status=FAILED for just the failures). Every item carries the index the rest of the API names it by, so a filtered page can still be acted on.
Two ops are different: analyze creates no asset — each item's answer is its output, and with the default schema its tags and objects also join the input's tags; render_template takes no input and makes one PNG asset per row.
When items fail, and resume
A job that was accepted and then failed carries no error envelope — the request was fine. The verdict is on the items: each failed one has an errorCode from the closed set and that code's own retryable, and the job repeats the verdict at the top — retryable: true with that item's code when any failed item is retryable, otherwise the first failed item's code with false. Branch on that, never on the message.
{
"id": "job_0db4028c1f6245d6aac4ea5ff3f6c0ae",
"type": "ai-edit",
"op": "remove_bg",
"model": "fal-ai/bria/background/remove",
"status": "FAILED",
"errorMessage": "2 of 3 items failed",
"errorCode": "provider_unavailable",
"retryable": true,
"totalItems": 3,
"completedItems": 1,
"failedItems": 2,
"actualCredits": 227,
"attemptNumber": 1,
"rootJobId": "job_0db4028c1f6245d6aac4ea5ff3f6c0ae",
"items": [
{ "status": "COMPLETED", "sourceAssetId": "ast_1a2b", "resultAssetId": "ast_9x8y", "credits": 227 },
{
"status": "FAILED",
"sourceAssetId": "ast_3c4d",
"error": "The provider timed out after the in-job retries",
"errorCode": "provider_unavailable",
"retryable": true,
"credits": 0
},
{
"status": "FAILED",
"sourceAssetId": "ast_5e6f",
"error": "The provider refused this image",
"errorCode": "provider_rejected",
"retryable": false,
"credits": 0
}
]
}| errorCode | retryable | on an item, it means |
|---|---|---|
| provider_unavailable | yes | the provider failed or timed out after the in-job retries; a resume may succeed |
| provider_rejected | no | the provider refused this input — a 4xx, a moderation refusal, an answer with no image; the same input fails again |
| asset_not_found | no | the source asset was deleted after the job was accepted |
| invalid_state | no | the source asset has no stored image (it never finished processing), or the preset or template the job runs was deleted while the job waited in the queue |
| invalid_param | no | a step the image cannot satisfy — a crop outside it — or a render_template row or template that cannot render |
| unsupported_format | no | the processing worker cannot decode the image: corrupt, or bigger than its decode limit |
| asset_count_exceeded | no | your plan's asset ceiling filled up while the job ran — other uploads or jobs used the room it was checked against — so this item's image was not kept |
| internal_error | yes | our side: a publish that failed, a stalled item the reaper ended, a render worker that gave up |
A deterministic or render item a worker failed without naming a cause carries error alone — no errorCode, no retryable. Treat it as not retryable. When no failed item is retryable and one of them is unclassified, the job carries no verdict either.
POST /api/v1/jobs/{id}/resume retries every incomplete item — FAILED or CANCELLED — of a FAILED or CANCELLED job as a new job: a new id, the same configuration and the same preset version, linked to the original by parentJobId, rootJobId and attemptNumber. The original is left as it was. Like cancel, it has no MCP tool.
JavaScript
const retry = await client.jobs.resume("<job-id>"); // a NEW job: wait on retry.id, not on the old onePython
retry = client.jobs.resume("<job-id>") # a NEW job: wait on retry["id"], not on the old oneCLI
imagestep jobs resume <job-id>curl
curl -s -X POST https://api.imagestep.dev/api/v1/jobs/<job-id>/resume -H "Authorization: ApiKey $IMAGESTEP_API_KEY"The new job, for the two items of the one above that did not complete:
{
"id": "job_5f3a9c1e7b2d4e109a6f2c8d1b0e4a77",
"status": "PROCESSING",
"attemptNumber": 2,
"parentJobId": "job_0db4028c1f6245d6aac4ea5ff3f6c0ae",
"rootJobId": "job_0db4028c1f6245d6aac4ea5ff3f6c0ae",
"totalItems": 2
}Items that already completed are not run — or charged — again, and a chain job's item restarts at the segment it failed on, from the image the segment before it left. Otherwise a resume is a submit: priced like the original for each item it runs, with every check a submit makes. There is no dry run of a resume; to price one first, dry-run the original body over the failed items' sourceAssetIds — an upper bound for a chain, whose items restart part-way.
| when | the resume answers |
|---|---|
| the job is FAILED or CANCELLED and has an incomplete item | 201 — the new job |
| the job is still running, or COMPLETED, or has nothing incomplete | 400 invalid_state |
| a later attempt of it exists — only the latest can be resumed | 422 job_not_resumable, details.reason NOT_LATEST_ATTEMPT |
| one job gets 3 resumes, counted across its attempts, and they are used up | 422 job_not_resumable, details.reason RESUME_LIMIT_EXCEEDED |
| the source asset of every incomplete item has been deleted since (items whose source is gone are otherwise left out) | 404 asset_not_found, details.missing |
| the new job does not fit the account | 402 insufficient_credit · 422 asset_count_exceeded, as a submit would |
Submitting exactly once
Send an Idempotency-Key with any submit you might send twice — after a timeout, a dropped connection, a crash between sending it and saving the id. The same key with the same body answers with the job the first call created instead of creating a second one; the same key with a different body is 409 idempotency_key_reuse; the same key while the first is still in flight is 409 request_in_progress, which is retryable. A submit that asked to wait replays with the job as it is now — and waits again while it runs — not as it was. The SDKs and the CLI send a key on every submit and reuse it across their own retries, so there is nothing to write; by hand, and from an agent, it looks like this:
curl
KEY=$(uuidgen) # one key per logical submit; send the SAME key on every retry of it
curl -s https://api.imagestep.dev/api/v1/jobs -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" -H "Idempotency-Key: $KEY" \
-d '{"op":"remove_bg","assetIds":["ast_1a2b"]}'MCP
# tool call — only the agent knows that two calls are the same attempt
transform {"op": "remove_bg", "asset_ids": ["ast_1a2b"], "idempotency_key": "cutout-ast_1a2b-1"}A dry run ignores the key, so its submit can reuse it. Keys last 24 hours, and “the same body” means the same bytes — the full rules are on errors & retries.
What you are charged
- AI work is charged per completed item, at the price the dry run quoted, and debited once, when the job settles: each item's
creditssum to the job'sactualCredits, so the parts add up to the invoice. A failed or cancelled item costs nothing — except a chain job's item, which pays for the AI segments it got through. - The dry run's
costPerItemis the charge, not an estimate of it — ananalyzeis priced by itsmaxOutputTokensup front, never by what the answer turned out to cost. - Deterministic and render items are free on a paid plan. On the Free plan each one handed to a worker counts once against the monthly allowance of 200, whether it then succeeds or fails, and the ones past it are paid from your balance at $0.002 each — held when the job is admitted, charged when it settles for the ones it dispatched. The dry run names them:
overageRunsandoverageCredits, already inestimatedCredits. - Usage adds up what was spent by op, key or day (
GET /api/v1/usage,imagestep usage,client.usage.get()). The credit ledger — every movement, top-ups included — is the console's Usage & billing page: it is the account's money, and no API key can read it. - A job record expires with the assets it produced (
expiresAt, stamped at submit from your plan's retention window, orretentionDaysfrom now when the submit asks for less) and cannot be deleted before then. The console's /jobs page is the same list, for a person checking what an agent ran.
Several sizes from one call
A deterministic job takes variants: up to 20 entries of {name, parameters}, each merged over the request's shared parameters. Two assets and two variants is four items, asset-major, each a new asset named after its variant — so the dry run, the quota, per-item failure and resume all behave as for any job. The synchronous endpoints have no form of this, and neither do AI ops, render_template or a preset — there it is 400 invalid_param.
JavaScript
const job = await client.ops.resize(["ast_1a2b", "ast_3c4d"], { fit: "cover", gravity: "attention" }, {
variants: [{ name: "ig", parameters: { width: 1080, height: 1350 } }, { name: "og", parameters: { width: 1200, height: 630 } }],
wait: true
});Python
job = client.ops.resize(["ast_1a2b", "ast_3c4d"], {"fit": "cover", "gravity": "attention"}, wait=True, variants=[
{"name": "ig", "parameters": {"width": 1080, "height": 1350}},
{"name": "og", "parameters": {"width": 1200, "height": 630}},
])CLI
imagestep jobs submit --op resize --asset-ids ast_1a2b,ast_3c4d --params '{"fit":"cover","gravity":"attention"}' \
--variants '[{"name":"ig","parameters":{"width":1080,"height":1350}},{"name":"og","parameters":{"width":1200,"height":630}}]' --waitcurl
curl -s https://api.imagestep.dev/api/v1/jobs -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
-d '{"op":"resize","assetIds":["ast_1a2b","ast_3c4d"],"parameters":{"fit":"cover","gravity":"attention"},
"variants":[{"name":"ig","parameters":{"width":1080,"height":1350}},{"name":"og","parameters":{"width":1200,"height":630}}]}'MCP
# tool call
transform {"op": "resize", "asset_ids": ["ast_1a2b", "ast_3c4d"], "parameters": {"fit": "cover", "gravity": "attention"}, "variants": [{"name": "ig", "parameters": {"width": 1080, "height": 1350}}, {"name": "og", "parameters": {"width": 1200, "height": 630}}]}A resume re-runs every variant that has an incomplete item, over every asset that has one — so when the failures are scattered, a few pairs that had already completed run again: free, and one more asset each.
Cancelling
POST /api/v1/jobs/{id}/cancel on a PENDING or PROCESSING job answers with the job — already CANCELLED when nothing was in flight, CANCELLING otherwise. A job already finished or already cancelling answers 400 invalid_state, with the state it is in under details.status. There is no MCP tool for it: an agent that wants out stops waiting, and the job is a person's to cancel.
JavaScript
const job = await client.jobs.cancel("<job-id>"); // CANCELLING until what is in flight settlesPython
job = client.jobs.cancel("<job-id>") # CANCELLING until what is in flight settlesCLI
imagestep jobs cancel <job-id>curl
curl -s -X POST https://api.imagestep.dev/api/v1/jobs/<job-id>/cancel -H "Authorization: ApiKey $IMAGESTEP_API_KEY"What stops depends on what is in flight. An AI item not yet sent to a provider is dropped; one in flight finishes and is charged, and one caught between its in-job retries ends FAILED. A deterministic or render item already handed to a worker always completes, because the worker has no notion of cancelling and a refused result would be an orphaned object. A chain job's item finishes the segment it is on and stops there, so a resume picks it up at the next one. When all of that has settled the job is CANCELLED, with the outputs — and the charge — of whatever completed.
Lifecycle
A job is in one of six statuses, and the last three are final: nothing moves a job out of them, and a resume is a new job.
PENDING— queued behind the 2 jobs of its type already runningPROCESSING— items handed out — in the submit's own answer when a slot is free- then, when its last item settles:
COMPLETED— no item failedFAILED— at least one item failed — resume runs those as a new jobCANCELLED— a cancel came first; it waited in CANCELLING for what was in flight
| status | meaning |
|---|---|
| PENDING | queued behind your other running jobs of the same type (2 run at a time per type); it starts when one of them settles, however long that takes, and is never failed for waiting |
| PROCESSING | running: its items are going out to workers and their results are coming back |
| CANCELLING | you cancelled it; what a provider or a worker already has finishes (and is charged), the rest is dropped |
| COMPLETED | every item settled and none failed |
| FAILED | every item settled and at least one failed — the others' outputs are there, and charged |
| CANCELLED | you cancelled it and what was in flight has settled: the items that finished keep their outputs (and their charge), the rest are CANCELLED |
A cancel moves a PENDING or PROCESSING job to CANCELLING, and it is CANCELLED once what was in flight has settled, whatever those items did.
Items go PENDING → PROCESSING → COMPLETED / FAILED / CANCELLED; a chain job's item goes back to PENDING between its segments. Each carries its own trace: who ran it (provider, model), when (startedAt, finishedAt, durationMs), what it cost (credits), on an AI item where the time went (timings), and on failure an error sentence, an errorCode and retryable.
A running job that makes no progress for 20 minutes — no item settling, nothing being handed out — is ended by a reaper: the items still waiting fail with internal_error, which is retryable, so a resume runs them again; an item a worker still holds is left to finish. The job then settles like any other, charged for what completed. A queued PENDING job is never failed for waiting.
Request fields
Everything a submit body may carry — and a dry run's, which is the same body:
| field | type | meaning |
|---|---|---|
| op | string | what to run: an op from the catalogue. The service derives type, presetId and preset from it |
| presetId | string | …or a saved preset instead: slug, id, or slug@version. model, prompt and parameters then override its one AI step — a preset with none, or several, refuses them |
| steps | PresetStep[] | …or the steps a preset would store, inline: the same array, checked the same way, run once without saving one — one job, priced per segment. Nothing rides beside it but its subjects: an op, a preset or a request-level model is refused by name |
| subjects | Subject[] | with steps only: the subjects a consistency preset stores, for its generate and edit steps — checked as a preset's are and recorded on the job. Beside an op or a presetId it is refused: a preset carries its own |
| assetIds | string[] ≤ 10000 | the inputs; resolved against your account before anything is created — a blank id is invalid_param, someone else's is asset_not_found with every missing id listed |
| imageCount | integer 1–10000 | dry run only: images you have not uploaded yet, priced as that many more assetIds — the price never depends on the pixels. A submit needs the images, so it refuses the count |
| parameters | object | the op's parameters object, validated against its contract |
| prompt · model | string | for AI ops; a request's own model and prompt override a preset's one AI step |
| count | integer 1–10 | how many images one generate makes |
| variants | object[] | deterministic ops only: several outputs per input, in one job — up to 20 entries (several sizes) |
| wait | integer | hold the response until a one-item job is done, 60 s at most (waiting for it) |
| collection | string | the collection the outputs go in; without one, an output made from an asset is in that asset's collection |
| retentionDays | integer ≥ 1 | keep the outputs (and this record) this many days instead of your plan's retention — shorter only; more is kept for the plan's time |
| templateId · items | string · object[] ≤ 500 | for render_template, which takes no assets — one object of variables per output image |
| type | ai-generate | ai-edit | parse | process | render | chain | the job type. Leave it out: op, presetId and steps decide it, and chain is what several segments run as — a preset's or inline steps — never something to ask for. Sent alone with a model, it runs that model with no preset |
| preset | PipelineData | an unsaved pipeline of engine steps, for a process job — the older, process-only form of steps |
Dry-run fields
Everything a dry run may answer with; a field that does not apply to the job type is absent:
| field | type | meaning |
|---|---|---|
| type · model | string | the job type the request resolves to, and — for an AI op — the model that would run |
| totalItems | integer | one per input, times one per variant; count for a generate; one per row for a render |
| costPerItem | integer | the charge per completed item, in credits — not an estimate of it |
| estimatedCredits | integer | totalItems × costPerItem |
| creditBalance · sufficientCredit | integer · boolean | your balance now, and whether it covers estimatedCredits; false means the submit is 402 insufficient_credit |
| assetCountLeft | integer | how many more assets your plan's ceiling leaves room for — every plan has one; fewer than totalItems means the submit is 422 asset_count_exceeded |
| processCountLeft · overageRuns · overageCredits | integer | deterministic and render jobs only: the monthly allowance left (-1 when unlimited), and of this job's runs how many fall past it and what they cost — paid from your balance, not refused, and already in estimatedCredits. An AI job does not carry them |
| maxInputEdge | integer | AI jobs: the longest edge, in px, an input is scaled down to before this model sees it |
| prompt | string | AI jobs: the prompt the model would receive, with every {{subject.<name>}} already expanded (presets → subjects) |
| presetId · presetName · presetVersion | string · integer | the preset the request resolved to and the version it would run; op:<name> for a bare op |
| templateId · templateName · templateVersion | string · integer | render only: the template the call named and the version it would render |
| steps · processPerItem | StepEstimate[] · integer | several segments only — a preset's or inline steps — the price broken down per segment (presets → chains) |
| warnings | StepWarning[] | what the steps say that is legal and probably not meant, with the step indexes — advisory, and never a reason a submit would fail (presets → warnings) |