---
title: "Submit a new job"
method: POST
path: "/api/v1/jobs"
tags: ["Job Management"]
---

# Submit a new job

`POST /api/v1/jobs`

## Request

- Base URL: `https://api.imagestep.dev`
- URL: `https://api.imagestep.dev/api/v1/jobs`
- Auth: API key in header `Authorization`

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

## Query parameters

- `dryRun` boolean
- `dry_run` boolean

## Headers

- `Idempotency-Key` string

## Request body

- 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`.
  - `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.
  - `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.
  - `type` 'ai-generate' | 'ai-edit' | 'parse' | 'process' | 'render' | 'chain': 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.
  - `templateId` string: render_template: the template to render — an id, or `id@version` to pin one version. May also be given as `parameters.templateId`.
  - `items` object[]: render_template: one object of template variables per output image (≤ 500). May also be given as `parameters.items`.
  - `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.
  - `imageCount` integer: 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`.
  - `wait` integer: 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.
  - `retentionDays` integer: 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.
  - `count` integer: A job with no `assetIds` that makes images from a prompt: how many to make, one item each. 1 when absent.
  - `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.
  - `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.
    - `op` string: An op from GET /api/v1/ops (kind ai or deterministic).
    - `model` string: AI steps: the model; the op's default when absent.
    - `prompt` string: AI steps: the prompt. {{subject.<name>}} expands to that subject's descriptor.
    - `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.
    - `operation` string: A processing-registry key (deterministic only), for steps no op covers.
    - `params` object: That registry step's arguments.
  - `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.
    - `name` string: Lowercase handle (`[a-z0-9][a-z0-9_-]{0,39}`, unique among the subjects), referenced in a prompt as {{subject.<name>}}.
    - `referenceAssetIds` string[]: Your own DONE asset ids showing this subject. They pin the geometry.
    - `descriptor` string: The locked description — colour, material, markings — at most 300 characters. Text pins what an image cannot show.
  - `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.
  - `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.
  - `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.
  - `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.
  - `preset` PipelineData: An inline processing pipeline: a name and the registry steps a process job runs.
    - `name` string
    - `pipeline` PipelineStep[]
      - `operation` string
      - `params` object: The registry step's arguments
      - `disabled` boolean

## Response `200`

`?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`).

## Other responses

- `201`: The job, created — `PENDING` or `PROCESSING`. Also the answer to a `wait` on a batch, which is ignored.
- `202`: `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=`.
- `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` - `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 - `preset_not_found` — no such preset of yours, or no such version of it - `not_found` — the `templateId` of a render, or an overlay's layer asset, does not exist
- `409`: Conflict — `error.code` is `idempotency_key_reuse` (the key already means a different request) or `request_in_progress` (the first request under this key has not settled yet; retryable).
- `422`: `asset_count_exceeded` — the items would take the account past its plan's stored-asset ceiling; the dry run reports `assetCountLeft`.
- `429`: Rate limited — `error.code` is `rate_limited` and `retryable` is true. `Retry-After` says how many seconds to wait.

---

[API](https://skmtc.dev/imagestep/apis/imagestep-api.md) · [All operations](https://skmtc.dev/imagestep/apis/imagestep-api/llms.txt) · [OpenAPI document](https://skmtc.dev/imagestep/apis/imagestep-api/revisions/5e8e922a3085?raw)
