---
title: Jobs
url: https://base_url.placeholder/docs/api/jobs
group: Surfaces
---

# Jobs

Asynchronous work: an op, a saved preset or inline steps over a batch of assets, run as one job with a handle. Price it with a dry run, wait on it in the same call or follow it by id or webhook, cancel it, resume what failed. Credits are charged when the job settles, for what completed.

7 endpoints under `/api/v1/jobs`. What every call shares is on [the REST overview](https://base_url.placeholder/docs/api#conventions), and what they are for — with the calls for every SDK, the CLI and MCP — on [Jobs](https://base_url.placeholder/docs/jobs). `*` marks a required field.

- GET /api/v1/jobs — List jobs
- POST /api/v1/jobs — Submit a new job
- GET /api/v1/jobs/counts — Count jobs by status and type
- GET /api/v1/jobs/{id} — Get a job
- POST /api/v1/jobs/{id}/cancel — Cancel a job
- GET /api/v1/jobs/{id}/items — List a job's items
- POST /api/v1/jobs/{id}/resume — Resume incomplete items in a job

## List jobs

GET /api/v1/jobs

[Paged](https://base_url.placeholder/docs/errors#pagination) — `page` and `perPage`, or `cursor`, in; `meta` out

Your jobs, newest first, one page at a time. Every filter is optional and they combine; a missing one filters nothing. A row carries the item counts and what ran, not `items` — read one job for those.

To follow a job you just submitted, read it by id — `GET /api/v1/jobs/{id}?wait=` holds the answer until it settles — or register a webhook. Polling this list is the slow way to watch one job.

USE THIS WHEN:

- Finding jobs by status, type, op, preset or date — last week's failures, say
- Reading every attempt of one job (`rootJobId`)

DO NOT USE WHEN:

- You have the job's id → `GET /api/v1/jobs/{id}`
- You only need how many → `GET /api/v1/jobs/counts`

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| status | query | string | Only jobs in this status<br>one of PENDING · PROCESSING · COMPLETED · FAILED · CANCELLED · CANCELLING |
| type | query | string | Only jobs of this type<br>one of ai-generate · ai-edit · parse · process · render · chain |
| preset | query | string | Only jobs that ran this preset: a slug or id for every version of it, or slug@version for the one that was pinned. A preset that is not yours, or a version that does not exist, is 404 preset_not_found. Jobs that ran inline steps or a bare op carry no preset and never match. |
| op | query | string | Only jobs submitted as this op — one entry of `GET /api/v1/ops`. A job submitted as a raw type or a preset carries no op and never matches. |
| rootJobId | query | string | Every attempt of one job: the ones whose `rootJobId` is this — the first attempt's id, which is its own `rootJobId` too, so it is in the page. Ordered newest first like any other page, so the latest attempt is first. |
| createdFrom | query | string | Submitted at or after this. Epoch millis, or an ISO-8601 date or instant read as UTC: 2026-09-14, 2026-09-14T08:30:00Z |
| createdTo | query | string | Submitted at or before this. Same forms as createdFrom; a bare date includes the whole of that day — which is also how to freeze the window a walk by page number pages through, since jobs you submit meanwhile would shift its pages. A walk by cursor is not shifted. |

### Returns `200` — `data` is `JobSummary[]`

One page of rows, with `meta`

### Errors

| Status | Code and when |
| --- | --- |
| 400 | `invalid_param` on `createdFrom` / `createdTo` — neither epoch milliseconds nor an ISO-8601 date or instant |
| 404 | `preset_not_found` — the `preset` filter names no preset of yours, or no such version of it |

## Submit a new job

POST /api/v1/jobs

Accepts an [`Idempotency-Key`](https://base_url.placeholder/docs/errors#idempotency)

Submits an async job. Name the work with `op` — one entry of `GET /api/v1/ops` — and the service derives the rest. Price it first: the same request with `?dryRun=true` creates nothing.

**op** is the vocabulary:

- `{"op": "remove_bg", "assetIds": ["ast_…"]}`
- `{"op": "resize", "assetIds": ["ast_…"], "parameters": {"width": 1200}}`
- `{"op": "generate", "prompt": "a red bicycle on white", "count": 2}`
- `{"op": "render_template", "templateId": "builtin-template-og-image", "items": [{"title": "Hello"}]}`

A parameter outside the op's contract is `400 invalid_param` naming it, before any credit is touched.

**presetId** runs a saved preset (`GET /api/v1/presets`) by slug or id, or `slug@version` to pin one version. The preset decides the job type; `model` / `prompt` / `parameters` override its ONE AI step — a preset with no AI step, or with several, refuses them (`400 invalid_param` naming the field). **steps** runs the same list of steps inline, once, without saving a preset (`op`, `presetId` and the overrides are refused beside it — put them on the step). **assetIds** batches: one item per asset. Several segments — an AI step beside other steps, or two of them — run as one `chain` job, each item walking the segments in order; the dry run breaks its price down per segment (`steps[]`). `type` (its values are the enum on the request schema) is what an op or a preset translates into; send it only without `op`.

The response is the job: follow it with `GET /api/v1/jobs/{id}` or a `job.*` webhook. Nothing is charged at submit — the credits of an AI job are taken when it settles, for the items that completed. A deterministic job counts its items against the period's processing allowance instead.

**wait** (seconds, at most 60; a larger value is clamped) holds the response until a ONE-item job is terminal: `200` with the finished job and its outputs, or `202` with the same job when the window closes first — then `GET /api/v1/jobs/{id}?wait=` waits again. One call, one result, and still a job: it is charged for what completes, retried and replayable exactly as without it. On a batch `wait` is ignored and the answer is the `201` at once.

**The dry run is a validation pass.** It answers every `400` and `404` a submit would, but never refuses for money or quota (`402 insufficient_credit`, `422 asset_count_exceeded`): what the account cannot afford comes back as data instead — `sufficientCredit`, `assetCountLeft`, `processCountLeft`, and what runs past the allowance would cost (`overageRuns`, `overageCredits`). **Images not stored yet** are priced by how many: `imageCount` on a dry run stands for that many more `assetIds` — the price depends on the op, model, parameters and count, never on the pixels — and the submit after the upload names them.

**Past the processing allowance** a deterministic job (a pass over assets, a render, a chain's passes) is not refused: each run past it is paid from your balance at the plan's `overageCredits` (`GET /api/v1/ops`), held with the job and charged for the runs it hands out. Only a balance that cannot cover them refuses the submit — `402 insufficient_credit`, like an AI job.

USE THIS WHEN:

- Running an AI op, a batch, or anything you want an `asset_id` back for
- Pricing a run before paying for it (`?dryRun=true`)

DO NOT USE WHEN:

- One image you already hold and a deterministic op → `POST /api/v1/images/transform` (bytes in, bytes out)
- Reading an image's metadata → `POST /api/v1/images/metadata`, or `GET /api/v1/assets/{id}` for a stored one

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| dryRun | query | boolean | Price the job and return the estimate without creating it or spending anything. Accepts `dryRun` and `dry_run`. A dry run ignores `Idempotency-Key`, so the submit after it can carry the same one. |
| dry_run | query | boolean | `dryRun` spelled snake_case; either one set to `true` is a dry run. |

### Request body — `JobRequest`

### Returns

| Status | Body | When |
| --- | --- | --- |
| 200 | `data` is `JobEstimate` or `Job` | `?dryRun=true`: what the job would cost, nothing was created — or, with `wait`, the job, terminal inside the window (a FAILED job is terminal too: read `status`). |
| 201 | `data` is `Job` | The job, created — `PENDING` or `PROCESSING`. Also the answer to a `wait` on a batch, which is ignored. |
| 202 | `data` is `Job` | `wait` was given and the job is still running when the window closed: the same job document, not yet terminal. Wait again with `GET /api/v1/jobs/{id}?wait=`. |

### Errors

| Status | Code and when |
| --- | --- |
| 400 | - `invalid_param` on the field at fault, before any credit is touched: an unknown `op` or one that is not a job, a parameter outside the op's contract (`parameters.<name>`), a `model` that does not run the op (`details.allowed` lists those that do), a step that cannot run (`steps[i]…`), something sent beside `steps` (or `subjects` without it), an override a preset has no one step for, a blank id in `assetIds` (`details.index`), a `collection` name that breaks its rules, `count` outside 1–10, more `assetIds` / `variants` / `items` than a job takes, `imageCount` without `dryRun` or on a render, or the removed `mode`<br>- `invalid_state` — an overlay's layer asset has not finished uploading |
| 402 | `insufficient_credit` — the balance does not cover the estimate. Only an AI job spends credits; the dry run says `sufficientCredit: false` instead. |
| 404 | - `asset_not_found` on `assetIds` — an id that is not one of your assets; `details.missing` lists every one<br>- `preset_not_found` — no such preset of yours, or no such version of it<br>- `not_found` — the `templateId` of a render, or an overlay's layer asset, does not exist |
| 422 | `asset_count_exceeded` — the items would take the account past its plan's stored-asset ceiling; the dry run reports `assetCountLeft`. |

## Count jobs by status and type

GET /api/v1/jobs/counts

How many of your jobs are in each status and of each type, as one table: a row per status and type that has any, with its count. Takes the filters GET /api/v1/jobs takes, except status and type — those are the table's two dimensions — and every cell is the `meta.total` that listing would report for that status and type.

To count by status alone, sum the rows for each status; to count the statuses within one type, keep that type's rows first.

USE THIS WHEN:

- Checking how many jobs failed, or are still running, without paging through them
- Showing a count beside each status or type filter

DO NOT USE WHEN:

- You need the jobs themselves → use GET /api/v1/jobs
- You need credits spent → use GET /api/v1/usage

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| preset | query | string | Only jobs that ran this preset, as on GET /api/v1/jobs: a slug or id for every version of it, or slug@version. A preset that is not yours is 404 preset_not_found. |
| op | query | string | Only jobs submitted as this op — one entry of `GET /api/v1/ops`. |
| rootJobId | query | string | Only the attempts of one job: the ones whose `rootJobId` is this — the first attempt's id, the first attempt included. |
| createdFrom | query | string | Submitted at or after this. Epoch millis, or an ISO-8601 date or instant read as UTC. |
| createdTo | query | string | Submitted at or before this. Same forms as createdFrom; a bare date includes the whole of that day. |

### Returns `200` — `data` is `JobCount[]`

The non-zero cells; an empty list when nothing matches

### Errors

| Status | Code and when |
| --- | --- |
| 400 | `invalid_param` on `createdFrom` / `createdTo` — neither epoch milliseconds nor an ISO-8601 date or instant |
| 404 | `preset_not_found` — the `preset` filter names no preset of yours, or no such version of it |

## Get a job

GET /api/v1/jobs/{id}

One job as it stands: its status, the item counts, what ran (op, preset and version, model, steps) and, per item, the output — `resultAssetId`, or `output` for an `analyze` — or, for a failed item, `errorCode` and `retryable`. The first 100 items ride inline and `itemsTruncated` says when there are more (`GET /api/v1/jobs/{id}/items` pages the rest). `actualCredits` is what the job was charged; it is set when the job settles.

**wait** turns the read into a long-poll: the answer is held until the job is terminal or the window closes, and the job as it stands comes back either way — read `status`. It is how a `202` from a waited submit is picked up.

A job record lives as long as the assets it made: at `expiresAt` it is swept, and a read after that is `404`.

USE THIS WHEN:

- Reading what a job produced, or why an item failed
- Waiting for a job to finish without writing a poll loop (`?wait=`)

DO NOT USE WHEN:

- Listing jobs → use GET /api/v1/jobs
- You only want the assets it made → `GET /api/v1/assets?jobId={id}`

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The job's id, `job_…` |
| wait | query | integer (int32) | Long-poll: hold the response until the job is terminal, for at most this many seconds (at most 60; a larger value is clamped). The job as it stands comes back either way — read `status`. Any job, batches included. More than 8 waits open on one account is `429 rate_limited` (`details.reason` `account_concurrency`) with `Retry-After`; a full node is `503 provider_unavailable`. Both are retryable. |

### Returns `200` — `data` is `Job`

The job — terminal or not; read `status`

### Errors

| Status | Code and when |
| --- | --- |
| 404 | `job_not_found` — no such job, or it is not yours, or its record has expired |
| 503 | `provider_unavailable`, `details.reason` `capacity` — this node holds as many waits as it will; retry after `Retry-After`, or read without `wait` |

## Cancel a job

POST /api/v1/jobs/{id}/cancel

Accepts an [`Idempotency-Key`](https://base_url.placeholder/docs/errors#idempotency)

Stops a `PENDING` or `PROCESSING` job. Items not yet handed to a provider or a worker are cancelled and never charged; items already running finish, and are charged for what they complete — nothing is taken at submit, so there is nothing to refund. The answer is the job: `CANCELLED` when nothing was in flight, else `CANCELLING` until the running items settle, and then `CANCELLED`. A `chain` item lets the segment in flight finish and starts no further one.

The job settles as `CANCELLED` and a `job.failed` event goes out. What it did make is kept; a resume retries the cancelled items.

USE THIS WHEN:

- Stopping a running or queued job you no longer want

DO NOT USE WHEN:

- The job is already terminal (`COMPLETED`, `FAILED`, `CANCELLED`) or `CANCELLING` → `400 invalid_state`

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The job's id, `job_…` |

### Returns `200` — `data` is `Job`

The job — `CANCELLING` or `CANCELLED`

### Errors

| Status | Code and when |
| --- | --- |
| 400 | `invalid_state` — there is nothing left to cancel: the job is already terminal or `CANCELLING`; `details.status` says which |
| 404 | `job_not_found` — no such job, or it is not yours |

## List a job's items

GET /api/v1/jobs/{id}/items

[Paged](https://base_url.placeholder/docs/errors#pagination) — `page` and `perPage`, or `cursor`, in; `meta` out

One page of a job's items, in item order (`index`, from 0) — the number the rest of the API names an item by, in events and in a resume.

`GET /api/v1/jobs/{id}` carries the first 100 items inline and sets `itemsTruncated` when there are more; this is how to read the rest. A batch takes up to 10000 assets, each times its variants, so the whole list is not something to hold in a context or re-read on every poll.

USE THIS WHEN:

- A job says `itemsTruncated` and you need an item past the first page
- You want only the failed ones, to decide whether a resume is worth its price (`status=FAILED`)

DO NOT USE WHEN:

- You want the assets the job produced → `GET /api/v1/assets?jobId={id}`, which is the run's products without the items around them

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The job's id, `job_…` |
| status | query | string | Only items in this status<br>one of PENDING · PROCESSING · COMPLETED · FAILED · CANCELLED |

### Returns `200` — `data` is `JobItem[]`

One page of items, with `meta`

### Errors

| Status | Code and when |
| --- | --- |
| 404 | `job_not_found` — no such job, or it is not yours |

## Resume incomplete items in a job

POST /api/v1/jobs/{id}/resume

Accepts an [`Idempotency-Key`](https://base_url.placeholder/docs/errors#idempotency)

Retries what did not finish: a new job over the FAILED and CANCELLED items of a `FAILED` or `CANCELLED` job, with the original's configuration — the same op, model, prompt and parameters, the same preset **at the version the original ran**, or the same inline steps and subjects. Only the incomplete items are retried (a variant set may repeat a few that had completed — deterministic work, priced like any other run).

- The new job is an ordinary submit: it is priced, checked and charged like one, so it can be refused like one (`402`, `422`).
- A `chain` item restarts on the segment it failed on (`failedStep`), from the image the last segment it finished made; the segments it already paid for are not paid again.
- A source asset deleted since the original ran drops its item from the retry; when none is left the answer is `404 asset_not_found`.
- The new job points back with `parentJobId`, carries `rootJobId` — the first attempt's id, the same on every resume and on the first attempt itself — and advances `attemptNumber` by 1; `GET /api/v1/jobs?rootJobId=` lists every attempt. Only the latest attempt can be resumed, and only a few times (`details.job.maxResumes`).

Whether a resume can help at all is on the job: `retryable: true` means at least one failed item failed for a reason that may pass.

USE THIS WHEN:

- Retrying only the failed items of a partially-successful job without resubmitting the whole batch

DO NOT USE WHEN:

- Submitting a fresh job → use POST /api/v1/jobs
- Every failed item says `retryable: false` — the same input fails the same way

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The id of the job to retry, `job_…` |

### Returns `201` — `data` is `Job`

The new attempt, created

### Errors

| Status | Code and when |
| --- | --- |
| 400 | `invalid_state` — the job is not `FAILED` or `CANCELLED`, or it has no FAILED or CANCELLED item to retry |
| 402 | `insufficient_credit` — the balance does not cover the retried AI items |
| 404 | - `job_not_found` — no such job, or it is not yours<br>- `asset_not_found` on `assetIds` — none of the incomplete items still has its source asset (or, for a chain, the image it would restart from); `details.missing` lists them |
| 422 | - `job_not_resumable` — `details.reason` is `NOT_LATEST_ATTEMPT` (resume the newest attempt of this `rootJobId` instead) or `RESUME_LIMIT_EXCEEDED` (`details.job.maxResumes` reached)<br>- `asset_count_exceeded` — the retry would pass the plan's stored-asset ceiling |

## Objects

Each described once; a linked type is another object on this page.

### Job

| Field | Type | Description |
| --- | --- | --- |
| actualCredits | integer (int64) | Credits charged: set when the job settles, for the work that completed; 0 until then, and for deterministic work, which spends none |
| attemptNumber | integer (int32) | 1 for a fresh job, one more for each resume |
| cancelledItems | integer (int32) | Items cancelled before they ran |
| collection | string | The collection the outputs go in, as the request named it; absent when it named none (an output made from an asset then lands in that asset's collection) |
| completedAt | string (date-time) | When it reached a terminal status |
| completedItems | integer (int32) | Items that completed |
| createdAt | string (date-time) | When it was submitted |
| errorCode | string | The verdict on the failed items, from the closed set of error codes: a retryable item's code when any is retryable, else the first failed item's. Absent when nothing failed, or no failure was classified |
| errorMessage | string | On a job with failed items, one line for a person ("2 of 3 items failed"); each item carries its own `error` |
| expiresAt | string (date-time) | When the record is swept — the plan's retention window at submit, the same as the assets it made. Never shortened by a downgrade |
| failedItems | integer (int32) | Items that failed — each says why, and whether a resume could help |
| id | string | The job's id, `job_…` |
| items | JobItem[] | The items, in order: the first 100 of them. `itemsTruncated` says when there are more — GET /api/v1/jobs/{id}/items pages the rest |
| itemsTruncated | boolean | True when `items` is only the first page of them; absent otherwise |
| model | string | The model the job calls; absent for deterministic work, and on a chain, whose segments name their own |
| op | string | The op it was submitted as, when it was submitted as one — what usage groups by |
| parameters | object | What the job runs with: an AI op's model parameters, `analyze`'s `schema` and `maxOutputTokens`, or a render's data rows under `items` |
| parentJobId | string | The attempt this one resumed; absent on a first attempt |
| pipelines | JobPipeline[] | A deterministic job's pipeline as stored at submit — one per variant, named after it — with each layer named by the asset id you gave |
| presetId | string | The preset it ran, by id; absent for an op, inline steps or a render |
| presetName | string | The preset's name; `op:<name>` for an op, `Ad-hoc` for a bare type and model; absent for a render |
| presetVersion | integer (int32) | The version of the preset it ran — a later edit of the preset never changes it |
| prompt | string | The prompt the model receives, every `{{subject.<name>}}` already expanded |
| references | string[] | The reference images sent with every model call — the subjects' images, resolved at submit |
| retryable | boolean | Whether a resume could help: true when at least one failed item failed for a reason that may pass. Absent with `errorCode` |
| rootJobId | string | The first attempt's id — the job's own on a first attempt, and the same on every resume of it, so `GET /api/v1/jobs?rootJobId=` lists every attempt |
| segments | JobSegment[] | `chain` only: the segments each item walks, in order, frozen at submit with what each costs per item |
| settledItems | integer (int32) | Items that reached an end — completed, failed or cancelled |
| status | string | `PENDING` waits for a slot (an account runs a few jobs of one type at a time; the rest queue), `PROCESSING` is running and `CANCELLING` is a cancel waiting for items already running. Then one of the terminal three: `COMPLETED` (no item failed), `FAILED` (at least one did — read the items) or `CANCELLED`.<br>one of PENDING · PROCESSING · COMPLETED · FAILED · CANCELLED · CANCELLING |
| stepCount | integer (int32) | How many inline `steps` the job ran; absent for an op or a preset job |
| steps | PresetStep[] | The inline `steps` it was submitted with, as written; absent for an op or a preset job |
| subjects | Subject[] | The subjects sent with inline `steps`, as checked |
| submittedAt | string (date-time) | When it left the queue and started running |
| submittedItems | integer (int32) | Items handed to a provider or worker so far |
| templateId | string | `render` only: the template, by id |
| templateName | string | `render` only: the template's name |
| templateVersion | integer (int32) | `render` only: the version of the template it rendered |
| totalItems | integer (int32) | How many items the job has — one per asset (times the variants), per image to generate, or per render row |
| type | string | What the job runs as — decided by the op, preset or steps it was submitted with<br>one of ai-generate · ai-edit · parse · process · render · chain |

### JobCount

How many of your jobs are in one status and of one type. Only non-zero cells are listed.

| Field | Type | Description |
| --- | --- | --- |
| count | integer (int64) | Jobs in this status and of this type, under the request's filters |
| status | string | The cell's status<br>one of PENDING · PROCESSING · COMPLETED · FAILED · CANCELLED · CANCELLING |
| type | string | The cell's job type<br>one of ai-generate · ai-edit · parse · process · render · chain |

### JobEstimate

Cost estimate for a job that has not been created

| Field | Type | Description |
| --- | --- | --- |
| assetCountLeft | integer (int64) | How many more assets the account can hold: the plan's stored-asset ceiling minus the assets it stores now (a chain's intermediates and failed uploads do not count). Deleting assets frees room; below zero after a downgrade. More items than this and a submit is 422 asset_count_exceeded.<br>e.g. 188 |
| costPerItem | integer (int64) | Credits per item (1 credit = $0.0001)<br>e.g. 390 |
| creditBalance | integer (int64) | The account's credit balance right now<br>e.g. 50000 |
| estimatedCredits | integer (int64) | totalItems × costPerItem<br>e.g. 4680 |
| maxInputEdge | integer (int32) | The longest edge, in pixels, of the image the model will be handed: a larger input is turned upright, scaled down to it and re-encoded as WebP with no metadata first. AI jobs that send an image only; a chain states it per step.<br>e.g. 2048 |
| model | string | Model that would run<br>e.g. "google/gemini-3.1-flash-image-preview" |
| overageCredits | integer (int64) | What `overageRuns` cost, in credits — already in `estimatedCredits`. Absent for AI jobs.<br>e.g. 0 |
| overageRuns | integer (int32) | Of this job's deterministic runs, how many fall past the plan's allowance and are paid from your balance instead of refused. 0 when they all fit; absent for AI jobs.<br>e.g. 0 |
| presetId | string | Preset the request resolved to, when it named one |
| presetName | string | Preset name; `op:<name>` for an op with no preset, 'Ad-hoc' for a bare type + model |
| presetVersion | integer (int32) | The version of that preset the job would run — the one `slug@version` pinned, or the current one<br>e.g. 3 |
| processCountLeft | integer (int64) | Deterministic-op quota left this period (process / render items), never below zero; -1 when the plan sets no ceiling. Runs your running jobs have claimed are already taken out. Runs past it are paid from your balance — `overageRuns` · `overageCredits`. Absent for AI jobs, which are priced in credits.<br>e.g. 190 |
| processPerItem | integer (int32) | `chain` only: how many process passes each item runs — one per deterministic segment. `totalItems × processPerItem` is what the job takes out of the period's deterministic-op quota. Every other type runs one pass per item.<br>e.g. 2 |
| prompt | string | The prompt the model would receive, with every {{subject.<name>}} already expanded to that subject's locked descriptor. Check it here rather than discovering a mis-typed placeholder in the pictures. AI jobs only. |
| steps | StepEstimate[] | `chain` only: what each segment costs, in order, so the total can be added up rather than trusted. Their `costPerItem` sums to the job's. |
| sufficientCredit | boolean | Whether a submit would be admitted: the balance, plus the one top-up an enabled auto top-up may overdraw, less what your running jobs already hold, covers the estimate. False does NOT mean the request is invalid — it means submitting it now would return insufficient_credit. |
| templateId | string | `render` only: the template the call named, by id<br>e.g. "builtin-template-og-image" |
| templateName | string | `render` only: that template's name<br>e.g. "OG image" |
| templateVersion | integer (int32) | `render` only: the version of the template the job would render — the one `id@version` pinned, or the current one<br>e.g. 1 |
| totalItems | integer (int32) | Number of items the job would have<br>e.g. 12 |
| type | string | Job type the request resolves to<br>e.g. "ai-generate" |
| warnings | StepWarning[] | What the steps say that is legal, will run, and is probably not what was meant — a shadowed output format, a step the one before it already did. Advisory: a warning never stops a submit, and an empty list is left out. What cannot run at all is a 400 invalid_param here instead, naming the step. Present when the request named a preset or sent inline steps. |

### JobItem

One unit of a job — an asset, an image to generate or a render row — and its whole trace: what it made, what it cost, how long it took, and why it failed

| Field | Type | Description |
| --- | --- | --- |
| credits | integer (int64) | What this item cost, in credits; 0 for deterministic work. The items' credits add up to the job's `actualCredits` |
| currentAssetId | string | `chain` only: the image its next segment reads — the source, then each segment's product |
| durationMs | integer (int64) | `finishedAt` − `startedAt`, in milliseconds |
| error | string | A failed item: why, in one sentence for a person |
| errorCode | string | A failed item: the code to branch on, from the closed set of error codes. Absent when the failure was not classified — treat that as not retryable |
| failedStep | integer (int32) | `chain` only: the segment a failed item failed on, from 0 — where a resume restarts it |
| finishedAt | string (date-time) | When it settled |
| index | integer (int32) | The item's position in the job, from 0 — how events, the item pages and a resume name it |
| intermediateAssetIds | string[] | `chain` only: the images earlier segments made to feed the next; deleted when the item settles, except the last one a failed item restarts from |
| model | string | AI items: the model it ran on |
| output | object | `analyze` only: the answer, in the default schema's shape or your `parameters.schema` |
| presetVersion | integer (int32) | The preset version it ran — the job's, repeated so one item explains itself |
| provider | string | AI items: the upstream that ran it<br>e.g. "fal" |
| resultAssetId | string | The asset it made, once `COMPLETED`; absent for `analyze`, whose product is `output` |
| retryable | boolean | A failed item: whether a resume could succeed — the code's own flag |
| sourceAssetId | string | The asset it runs on; absent for an image generated from a prompt or a render row |
| startedAt | string (date-time) | When a worker picked it up |
| status | string | Where the item is<br>one of PENDING · PROCESSING · COMPLETED · FAILED · CANCELLED |
| step | integer (int32) | `chain` only: the segment the item is on, from 0 |
| templateVersion | integer (int32) | A render item's template version — the job's, repeated |
| timings | object | AI items: where the time went, in milliseconds — `inputMs` (making the image the model is handed), `providerMs` (the provider call), `storeMs` (fetching and storing the result). Exclusive of one another, summed over retries |
| variant | string | The variant that made it; absent outside a variant set |

### JobPipeline

One pass a deterministic job runs: a name — the variant's, when there is a set of them — and its registry steps

| Field | Type | Description |
| --- | --- | --- |
| name | string | The variant's name, or `op:<name>` for a single op |
| steps | PipelineStep[] | The registry steps, in order |

### JobRequest

What to run and on what: one of `op`, `presetId` or `steps` (or a raw `type`), the inputs, and where the outputs go. The same body prices it with `?dryRun=true`.

| Field | Type | Description |
| --- | --- | --- |
| assetIds | string[] | Your assets to run on — one item per id, times the `variants`. Required unless the op reads no image (`requiresAssets: false` in GET /api/v1/ops) or the job is a render. Every id must be yours: one that is not is `404 asset_not_found`, before anything is created. |
| collection | string | Collection to put the output assets in (optional; at most 200 characters, and names starting with job: are reserved). Without one, an output made from an asset is in that asset's collection.<br>e.g. "shoot-01" |
| count | integer (int32) | A job with no `assetIds` that makes images from a prompt: how many to make, one item each. 1 when absent.<br>e.g. 1 |
| imageCount | integer (int32) | Dry run only (`?dryRun=true`): images you have not stored yet, priced as that many more `assetIds` — one item each, times the `variants` — so the price can be asked before anything is uploaded. Beside `assetIds` it adds to them, and those are still checked. A submit needs the images themselves as `assetIds`: without `dryRun`, or on a job that reads no image (a render), it is `400 invalid_param`. At most 10000 together with `assetIds`.<br>e.g. 20 |
| items | object[] | render_template: one object of template variables per output image (≤ 500). May also be given as `parameters.items`.<br>e.g. [{"site":"imagestep.dev","title":"Hello"}] |
| model | string | The model an AI op runs on — an id from GET /api/v1/ai-models whose `categories` include the op's `modelCategory`; any other is `400 invalid_param` with the ones that fit in `details.allowed`. Absent, the op's `defaultModel`. With a preset it overrides the preset's one AI step. |
| op | string | An op name — one entry of GET /api/v1/ops, the catalogue every client enumerates. When set, `type` / `presetId` / `preset` are derived from it and `parameters` carries the op's parameters; the dry run prices it like any job.<br>e.g. "remove_bg" |
| parameters | object | The op's parameters. A deterministic op's are checked against its `params` in GET /api/v1/ops, and one outside them is `400 invalid_param` naming it; an AI op's go to its model (`parameters` in GET /api/v1/ai-models). With a preset they replace its one AI step's parameters. |
| preset | PipelineData | A raw processing pipeline — registry steps, run as one pass. What an op translates into; prefer `op`, or `steps` for several. |
| presetId | string | A saved preset to run: its slug or id, or `slug@version` / `id@version` to pin one version (GET /api/v1/presets). The preset decides the job type; `model` / `prompt` / `parameters` override its ONE AI step, and are refused by a preset that has none or several.<br>e.g. "builtin-util-web-optimize" |
| prompt | string | The prompt of an AI op that takes one — required where the op's entry says `requiresPrompt`. With a preset it overrides the one AI step's prompt, and `{{subject.<name>}}` in it expands to that subject's descriptor. |
| retentionDays | integer (int32) | Keep the assets this job makes — and its own record — for this many days instead of your plan's retention: shorter only. More than the plan keeps is kept for the plan's time, not refused; the assets' `expiresAt` says what was stamped. Absent: the plan's retention.<br>e.g. 7 |
| steps | PresetStep[] | An inline chain: the same steps a preset stores (each `{op, model?, prompt?, parameters?}` or `{operation, params}`), checked as a preset's are and run once without saving one. Sent instead of `op` or `presetId`; `model`, `prompt` and `parameters` go on the step. The job records them. |
| subjects | Subject[] | With `steps` only: the subjects a preset stores — reference images plus the locked words, applied to every `generate` / `edit` step, with `{{subject.<name>}}` in a prompt expanding to the descriptor. Checked as a preset's are (your own DONE assets, at most 4 subjects, and a generate or edit step to read them). The job records them. |
| templateId | string | render_template: the template to render — an id, or `id@version` to pin one version. May also be given as `parameters.templateId`.<br>e.g. "builtin-template-og-image" |
| type | string | Job type. `chain` is what several segments run as — a preset's or inline `steps` — and is derived from them; asked for on its own it is refused.<br>one of ai-generate · ai-edit · parse · process · render · chain |
| variants | object[] | Run the op once per entry, producing one asset each — the whole set of social sizes from one call. Each entry is {name, parameters}; the parameters are merged over the top-level ones, and each name must be distinct (it is added to the output's name). Deterministic ops only — beside an AI op, `render_template` or a preset it is `400 invalid_param`; at most 20. |
| wait | integer (int32) | Hold the response until the job is terminal, for at most this many seconds — capped at 60 (a larger value is clamped, not refused). A job of ONE item only; on a batch it is ignored and the handle comes back at once. Terminal inside the window → 200 with the finished job; still running → 202 with the same handle, and GET /api/v1/jobs/{id}?wait= waits again. Ignored on a dry run.<br>e.g. 30 |

### JobSegment

One segment of a `chain`: a deterministic pass or one AI step, and what it costs per item

| Field | Type | Description |
| --- | --- | --- |
| bound | boolean | True when the model bills by the size of the image it is sent: `costPerItem` was priced at the largest image it is ever sent, and it is the charge whatever the size of an item's own |
| costPerItem | integer (int64) | Credits one item costs on this segment, frozen at submit; 0 for a deterministic one |
| model | string | An AI segment's model |
| op | string | The op the segment runs; absent for a pass built from registry steps only |
| parameters | object | An AI segment's parameters |
| pipeline | PipelineStep[] | A deterministic segment's steps, as one pass |
| prompt | string | An AI segment's prompt, every `{{subject.<name>}}` expanded |
| type | string | What the segment runs as: a deterministic pass, or an AI op's job type<br>one of ai-generate · ai-edit · parse · process |

### JobSummary

One row of `GET /api/v1/jobs`: the job's own fields, as on `Job` — what ran, how far it got, what it cost — without `items`, `steps`, `subjects`, `pipelines`, `segments`, `parameters`, `prompt`, `references` or `collection`. Read the job by id for those.

| Field | Type | Description |
| --- | --- | --- |
| actualCredits | integer (int64) |   |
| apiKeyId | string | The API key that submitted it; absent for a signed-in console session |
| attemptNumber | integer (int32) |   |
| cancelledItems | integer (int32) |   |
| completedAt | string (date-time) |   |
| completedItems | integer (int32) |   |
| costPerItem | integer (int64) | Credits one item is priced at — what the job was quoted; 0 for deterministic work, and on a chain the sum of its segments |
| createdAt | string (date-time) |   |
| errorCode | string |   |
| errorMessage | string |   |
| expiresAt | string (date-time) |   |
| failedItems | integer (int32) |   |
| id | string |   |
| model | string |   |
| op | string |   |
| parentJobId | string |   |
| presetId | string |   |
| presetName | string |   |
| presetVersion | integer (int32) |   |
| retryable | boolean |   |
| rootJobId | string |   |
| settledItems | integer (int32) |   |
| status | string | one of PENDING · PROCESSING · COMPLETED · FAILED · CANCELLED · CANCELLING |
| stepCount | integer (int32) |   |
| submittedAt | string (date-time) |   |
| submittedItems | integer (int32) |   |
| templateId | string |   |
| templateName | string |   |
| templateVersion | integer (int32) |   |
| totalItems | integer (int32) |   |
| type | string | one of ai-generate · ai-edit · parse · process · render · chain |

### PipelineData

An inline processing pipeline: a name and the registry steps a process job runs.

| Field | Type | Description |
| --- | --- | --- |
| name | string |   |
| pipeline | PipelineStep[] |   |

### PipelineStep

| Field | Type | Description |
| --- | --- | --- |
| disabled | boolean |   |
| operation | string |   |
| params | any |   |

### PresetStep

A preset step: either an L1 op from GET /api/v1/ops — {op, model?, prompt?, parameters?} — or a processing-registry step — {operation, params} — for what no op covers. Never both.

| Field | Type | Description |
| --- | --- | --- |
| model | string | AI steps: the model; the op's default when absent. |
| op | string | An op from GET /api/v1/ops (kind ai or deterministic).<br>e.g. "resize" |
| operation | string | A processing-registry key (deterministic only), for steps no op covers.<br>e.g. "sharpen" |
| parameters | object | The op's parameters. A deterministic op's are checked against its catalogue contract when the steps are written, and so are `analyze`'s bounds; an AI image op's go to the model as sent. |
| params | any | That registry step's arguments. |
| prompt | string | AI steps: the prompt. {{subject.<name>}} expands to that subject's descriptor. |

### StepEstimate

One segment of a chain job's cost

| Field | Type | Description |
| --- | --- | --- |
| bound | boolean | True when this segment's model is priced by the size of the image it is sent: `costPerItem` is set at the largest image that model is ever sent (`maxInputEdge`), and it is what every item is charged, whatever the size of its own image. Absent means the price does not depend on the image. |
| costPerItem | integer (int64) | Credits one item costs on this segment; 0 for a process segment<br>e.g. 390 |
| index | integer (int32) | Segment index, 0-based<br>e.g. 0 |
| maxInputEdge | integer (int32) | The longest edge, in pixels, of the image this segment's model is handed; absent for a process segment<br>e.g. 2048 |
| model | string | The model this segment would call; absent for a process segment |
| op | string | What the segment runs: `process` for a pipeline pass, else the AI op's name<br>e.g. "remove_bg" |

### StepWarning

An advisory finding about a list of steps: legal, and probably not what was meant

| Field | Type | Description |
| --- | --- | --- |
| code | string | Machine-readable finding code from a closed set; the table is /docs/presets#warnings<br>e.g. "format_shadowed" |
| message | string | The finding in one sentence, naming the steps by index<br>e.g. "steps[1] writes jpeg and steps[3] writes webp; a process segment writes one image, in the format of its last format step, so steps[1]'s format is not written" |
| steps | integer (int32)[] | The step indexes the finding is about, in order — the redundant one first<br>e.g. [1,3] |

### Subject

A recurring character, product or location: reference images plus the locked words for it.

| Field | Type | Description |
| --- | --- | --- |
| descriptor | string | The locked description — colour, material, markings — at most 300 characters. Text pins what an image cannot show. |
| name | string | Lowercase handle (`[a-z0-9][a-z0-9_-]{0,39}`, unique among the subjects), referenced in a prompt as {{subject.<name>}}.<br>e.g. "hero" |
| referenceAssetIds | string[] | Your own DONE asset ids showing this subject. They pin the geometry. |
