SDKs
imagestep on npm and imagestep on PyPI are the REST API with the plumbing written once: the response envelope unwrapped, an idempotency key on every write, retryable failures sent again, waiting and paging done for you. Both have the same resources with the same methods — camelCase in JavaScript, snake_case in Python. The top of this page is how a client behaves; below it, every method of every resource. The same API is also a CLI for the terminal and an MCP server for agents.
Install and make a first call
JavaScript runs on Node 20 or newer, Deno, Bun and edge runtimes, ships ESM and CommonJS with its types, and has no runtime dependencies: it uses the runtime's own fetch and the global crypto. Python needs 3.10 or newer and one runtime dependency, httpx.
pnpm add imagestep # JavaScript — or: npm install imagestep
pip install imagestep # PythonUpload a photo, cut out its background as a job, and publish the result — four calls, each of which waits for what the next one needs:
JavaScript
import { ImageStep } from "imagestep";
const client = new ImageStep({ apiKey: process.env.IMAGESTEP_API_KEY });
const asset = await client.assets.upload("./product.jpg"); // stage, PUT, finish, wait for ingest
const job = await client.ops.removeBg(asset.id, { wait: true }); // a job; wait holds on until it settles
const [cutout] = await client.jobs.outputs(job); // what it made: new assets, in item order
const [published] = await client.assets.publish(cutout.id);
console.log(published.publicUrl);Python
from imagestep import ImageStep
client = ImageStep() # api_key from IMAGESTEP_API_KEY
asset = client.assets.upload("./product.jpg")
job = client.ops.remove_bg(asset["id"], wait=True)
[cutout] = client.jobs.outputs(job)
[published] = client.assets.publish(cutout["id"])
print(published["publicUrl"])The client
| JavaScript | Python | Default and meaning |
|---|---|---|
apiKey | api_key | A key from /keys. JavaScript reads no environment variable, and without a key sends no Authorization: the public reads (ops.list, ops.get, agent.guidelines) work and every other call is a 401. Python falls back to IMAGESTEP_API_KEY and does not construct without a key (TypeError). |
baseUrl | base_url | default https://api.imagestep.dev; Python falls back to IMAGESTEP_BASE_URL before the default. |
timeoutMs | timeout | default 60 s, per attempt — milliseconds in JavaScript, seconds in Python. |
maxRetries | max_retries | default 2 — how many times a retryable failure is sent again (below). |
userAgent | user_agent | replaces imagestep-js/<version> · imagestep-python/<version>. |
fetch | transport · http_client | Your own fetch; in Python an httpx transport, or a whole httpx.Client. For proxies and tests. |
Methods return what the API returns, unwrapped from its envelope: plain objects in JavaScript, typed by the package's index.d.ts; plain dicts in Python, typed as TypedDicts generated from the OpenAPI document, with the keys as they are on the wire (asset["publicUrl"]). Python's keyword arguments are snake_case and go out under their wire names; a dict you pass as a body (a preset, a template, a webhook patch) is sent as written, in the wire's camelCase. In JavaScript the methods whose signature lists a signal take an AbortSignal; Python has no equivalent.
Everything a program imports: ImageStep (also the default export), ImageStepError, JobFailedError, verifyWebhookSignature and constructWebhookEvent from imagestep on npm; ImageStep, AsyncImageStep, ImageStepError, JobFailedError, WebhookSignatureError, verify_webhook_signature, construct_webhook_event, Page, RequestResult and BinaryResult from imagestep on PyPI.
Python: a sync client and an async one
ImageStep and AsyncImageStep take the same arguments and have the same resources with the same method names. On the async client every resource method is awaited, images included, and the iterate… methods are async iterators (async for). Close them with close() or with, and aclose() or async with. The differences inside a resource: the async jobs.wait also accepts a coroutine as on_progress, webhooks.verify and webhooks.construct_event are plain functions on both clients — no network, nothing to await — and the async assets.upload reads a local file synchronously before it sends it.
from imagestep import AsyncImageStep
async with AsyncImageStep() as client:
job = await client.ops.remove_bg(asset_ids, wait=True)
outputs = await client.jobs.outputs(job)Retries, timeouts and idempotency
Every call sends the key as Authorization: ApiKey … and unwraps the envelope, so a method returns the data. What happens when a call fails:
| Behaviour | What the SDK does |
|---|---|
| Retries | A failure whose retryable is true, and a network failure (a timed-out attempt is one), is sent again up to maxRetries / max_retries times. A failure that is not retryable is raised on the first attempt, with the service's details on the error — for insufficient_credit, what was needed and topUpUrl, the page that clears it. |
| Backoff | The Retry-After header, in seconds, when the response carries one; otherwise 0.5 s, 1 s, 2 s … after an API error and 0.25 s, 0.5 s, 1 s … after a network failure. |
| Timeouts | timeoutMs / timeout bounds each attempt, not the whole call; a request that asks the service to hold it open for a job gets that window plus 15 s. In JavaScript, aborting the signal stops the request and any backoff wait, and is never retried. |
| Idempotency-Key | On every write: a fresh UUID per call, or the idempotencyKey / idempotency_key you pass. The SDK's own retries resend the same key, so a retried submission cannot create a second job. Pass your own when your code retries: the same key with the same body replays the first response instead of running again. The methods that take one are ops.run and every per-op helper, ops.estimate, presets.run, presets.create, jobs.submit, jobs.estimate, assets.renameCollection, agent.feedback and the raw requests. |
The exception: images | The synchronous endpoints get no key, because they create nothing and there is nothing to replay. They still retry a retryable failure, since you are still holding the input — except a JavaScript body that can be read only once (a stream), which gets a single attempt. |
The contract behind these — the envelope, the error codes, what a replay answers, rate limits — is Errors, idempotency and limits.
Waiting and paging
Two things the SDKs do in a loop so that your code does not: hold on until work is finished, and walk a listing to its end.
| When | What the SDK does |
|---|---|
wait on a run | ops.run, every per-op helper, presets.run and, in JavaScript, jobs.submit take wait: true, or the options of jobs.wait. The submit itself asks the service to hold the answer (up to 60 s), so a one-item job that settles in that window comes back finished in one round trip; whatever is left is jobs.wait. |
jobs.wait | Each read is GET /api/v1/jobs/{id}?wait=, which the service answers the moment the job settles (or after 60 s), so a five-second job costs one request. The interval is only a floor between reads. It gives up after 10 minutes unless you pass a timeout, raising JobFailedError with the job as it last stood — nothing is cancelled. A job that ends FAILED or CANCELLED raises too, unless throwOnFailure / throw_on_failure is false. A read turned away for now — 429 rate_limited when the account already holds its share of open waits, or a 503 — is asked again after its Retry-After until the time runs out. For anything long-running, a webhook beats waiting. |
| Uploads | assets.upload, uploadMany and fromUrl wait for ingest by default: one status call per tick for every asset still PROCESSING, every 1.5 s for up to 120 s, then JobFailedError. With wait: false they return at once, and assets.waitReady waits later with your own interval and timeout. |
| Listings | A list method (list, collections, items, deliveries, reports) reads one page — at most 100 rows — and returns it with its meta; presets.list and webhooks.list answer everything at once. Its iterate… twin yields every row, one at a time, reading each page after the first by the meta.nextCursor the last one carried, and throws rather than stop halfway when a page says hasMore without a cursor. Pass cursor to resume a walk. The paging rules themselves are Pagination. |
Errors
Every failure the API answers is raised as ImageStepError, after the retries the SDK is allowed have run out. Branch on retryable, never on the status: the status cannot tell a rate limit from a quota. The closed set of codes and what each one means are Errors, idempotency and limits.
| JavaScript | Python | Meaning |
|---|---|---|
status | status | the HTTP status; 0 for an error the SDK raises itself (an op with no synchronous form, a parameter named op, preset or response) |
code | code | from the closed set. When the response carried no error envelope (a proxy's 502), null in JavaScript; Python fills in internal_error for a 5xx and unknown_error otherwise |
message | message | for a person to read, not to parse |
retryable | retryable | whether sending the same request again can succeed — the field to branch on. Without an error envelope to say so, a 429 or a 5xx is retryable and anything else is not |
param | param | the parameter to fix, when the error is about one; else null |
details | details | machine-readable context, when there is some; else null |
retryAfter | retry_after | seconds the service asked you to wait (the Retry-After header); else null |
requestUrl | request_url | the URL that failed |
requestId | request_id | the service's id for the request (X-Request-Id) — quote it when you report a failure; null before a response |
| JavaScript | Python | Raised when |
|---|---|---|
JobFailedError | JobFailedError | A waited-for job ended other than COMPLETED, or a wait ran out of time (jobs.wait, a wait option, an upload's wait, assets.waitReady). job is the last state polled; retryable is always false. |
Error | WebhookSignatureError | A webhook signature did not verify in constructWebhookEvent / construct_webhook_event. The Python class is a ValueError. |
| the runtime's fetch error | httpx.TransportError | The network failed on every attempt, or a timeout outlived the retries. |
JavaScript
import { ImageStepError, JobFailedError } from "imagestep";
try {
await client.ops.upscale(ids, { wait: true });
} catch (err) {
// retryable: the SDK already retried, so come back later — after what the service asked for
if (err instanceof ImageStepError && err.retryable) queueForLater(ids, err.retryAfter);
else if (err instanceof JobFailedError) console.error(err.job.status, err.job.id);
else throw err; // not retryable: fix what err.param names
}Python
from imagestep import ImageStepError, JobFailedError
try:
client.ops.upscale(ids, wait=True)
except JobFailedError as err:
print(err.job["status"], err.job["id"])
except ImageStepError as err:
if not err.retryable:
raise # fix what err.param names
# retryable: the SDK already retried, so come back later — after what the service asked for
queue_for_later(ids, err.retry_after)ops — the catalogue, and running an op as a job
An op is one atomic capability, and ops.list() is the catalogue: read it instead of hard-coding names, since a new op appears there without an SDK release. Running an op submits a job, which is what owns progress, cancellation, settlement and an asset id for the result. What each op does and takes is Ops.
| JavaScript | Python | Returns |
|---|---|---|
client.ops.list() | client.ops.list() | every op definition, with its parameter contract and syncEndpoint |
client.ops.get(op) | client.ops.get(op) | one op definition |
client.ops.run(op, { assetIds, prompt, count, model, parameters, variants, templateId, items, collection, retentionDays, dryRun, imageCount, wait, idempotencyKey, signal }) | client.ops.run(op, asset_ids=, prompt=, count=, model=, parameters=, variants=, template_id=, items=, collection=, retention_days=, dry_run=False, image_count=, wait=False, idempotency_key=) | The created job. With wait (true, or an object of jobs.wait options) the finished job, or a JobFailedError (waiting); with dryRun the estimate. assetIds is one id or many; collection is where the outputs go, each of which is a new asset — a job never overwrites its input. |
client.ops.estimate(op, { assetIds, prompt, count, model, parameters, variants, templateId, items, collection, retentionDays, dryRun, imageCount, wait, idempotencyKey, signal }) | client.ops.estimate(op, **opts) | The cost dry run — estimatedCredits, creditBalance, sufficientCredit — with nothing created |
One helper per op
Each helper is ops.run with the op name filled in, and returns what run returns; the options are the same. A deterministic op's helper takes the op's own parameters — the keys shown — before them. Every name below is on client.
| JavaScript | Python | Takes and returns |
|---|---|---|
ops.removeBg · ops.upscale · ops.restoreFace · ops.colorize · ops.analyze · ops.grayscale · ops.flip · ops.flop (assetIds, opts) | ops.remove_bg · ops.upscale · ops.restore_face · ops.colorize · ops.analyze · ops.grayscale · ops.flip · ops.flop (asset_ids, **opts) | what run returns. analyze answers with structured JSON per image, as each job item's output; the default answer's tags and objects also join the asset's tags. |
ops.generate (prompt, opts) | ops.generate (prompt, **opts) | what run returns |
ops.edit (assetIds, prompt, opts) | ops.edit (asset_ids, prompt, **opts) | what run returns |
ops.resize (assetIds, { width, height, fit, gravity, background, withoutEnlargement, matchOrientation }, opts) | ops.resize (asset_ids, parameters, **opts) | what run returns |
ops.convert (assetIds, { format, quality }, opts) | ops.convert (asset_ids, parameters, **opts) | what run returns |
ops.compress (assetIds, { quality, format }, opts) | ops.compress (asset_ids, parameters, **opts) | what run returns |
ops.crop (assetIds, { left, top, width, height }, opts) | ops.crop (asset_ids, parameters, **opts) | what run returns |
ops.pad (assetIds, { top, bottom, left, right, background }, opts) | ops.pad (asset_ids, parameters, **opts) | what run returns |
ops.rotate (assetIds, { angle, background }, opts) | ops.rotate (asset_ids, parameters, **opts) | what run returns |
ops.trim (assetIds, { threshold, background }, opts) | ops.trim (asset_ids, parameters=, **opts) | what run returns |
ops.flatten (assetIds, { background }, opts) | ops.flatten (asset_ids, parameters=, **opts) | what run returns |
ops.adjust (assetIds, { brightness, saturation, hue, lightness, contrast }, opts) | ops.adjust (asset_ids, parameters, **opts) | what run returns |
ops.mask (assetIds, { shape, radius }, opts) | ops.mask (asset_ids, parameters=, **opts) | what run returns |
ops.blurRegion (assetIds, { width, height, left, top, sigma, pixelate }, opts) | ops.blur_region (asset_ids, parameters, **opts) | what run returns |
ops.overlay (assetIds, { layerAssetId, gravity, scale, opacity, margin, tile }, opts) | ops.overlay (asset_ids, parameters, **opts) | what run returns |
ops.caption (assetIds, { text, font, size, color, background, gravity, margin }, opts) | ops.caption (asset_ids, parameters, **opts) | what run returns |
ops.readMetadata (assetId) | ops.read_metadata (asset_id) | not a job: { id, name, image, metadata, expiresAt } read from the asset |
JavaScript
const { items } = await client.assets.list({ collection: "shoot-01" });
const ids = items.map((a) => a.id);
const estimate = await client.ops.estimate("convert", { assetIds: ids, parameters: { format: "webp", quality: 80 } });
if (estimate.sufficientCredit) {
const job = await client.ops.convert(ids, { format: "webp", quality: 80 }, { wait: { onProgress: (j) => console.log(j.status) } });
}Python
page = client.assets.list(collection="shoot-01")
ids = [a["id"] for a in page.items]
estimate = client.ops.estimate("convert", asset_ids=ids, parameters={"format": "webp", "quality": 80})
if estimate["sufficientCredit"]:
job = client.ops.convert(ids, {"format": "webp", "quality": 80}, wait={"on_progress": print})images — bytes in, bytes out, nothing stored
The synchronous face: for when you are holding an image and only want the result back. Deterministic ops and deterministic presets only — AI ops, batches and anything you want an asset id for are jobs. Which ops qualify is read from ops.list() once per client, never from a list in the SDK. The endpoints and their limits are Synchronous ops.
| JavaScript | Python | Returns |
|---|---|---|
client.images.syncEndpoints() | client.images.sync_endpoints() | { op: syncEndpoint or null } for every op, fetched once and cached on the client |
client.images.supports(op) | client.images.supports(op) | whether the op has a synchronous form |
client.images.transform(op, { file, url, assetId, preset, response, signal, parameters }) | client.images.transform(op=, file=, url=, asset_id=, preset=, response=, parameters=) | The result bytes (Uint8Array / bytes); with response: "url" a JSON object with a signed URL instead. Pass null / None as the op to run a preset. An op with no synchronous form fails with invalid_param (status 0) before the image is sent — the SDK reads that from the catalogue. The input is one of file — a path or bytes, and in JavaScript a Blob or a stream, in Python a binary file object — or url, or assetId / asset_id, which send a reference instead of bytes. The op's own parameters go in parameters, never beside file: a parameter can name nothing but a parameter, so a bag of them that came from somewhere else cannot point this process at a local file. |
client.images.transformResult(op, { file, url, assetId, preset, response, signal, parameters }) | client.images.transform_result(op=, file=, url=, asset_id=, preset=, response=, parameters=) | the same, plus what the service measured: { bytes, json, contentType, width, height } / BinaryResult(content, json, content_type, width, height); a size that was not measured is null |
client.images.render(templateId, data, { signal }) | client.images.render(template_id, data=) | one PNG from one row of data; many rows are a render_template job |
client.images.metadata(file, { signal }) | client.images.metadata(file) | EXIF, GPS, dimensions, format and SHA-1 as JSON — free, and it stores nothing |
JavaScript
import { writeFile } from "node:fs/promises";
const small = await client.images.transform("resize", { file: "./photo.jpg", parameters: { width: 1200 } });
await writeFile("small.jpg", small);
const { bytes, contentType, width } = await client.images.transformResult(null, { file: "./photo.jpg", preset: "builtin-util-web-optimize" });Python
small = client.images.transform("resize", file="./photo.jpg", parameters={"width": 1200})
open("small.jpg", "wb").write(small)
result = client.images.transform_result(None, file="./photo.jpg", preset="builtin-util-web-optimize")
print(result.content_type, result.width, result.height)assets — upload, find, publish
An asset is an image stored in the account with a stable id. Upload bytes or hand the service URLs to fetch, put them in a collection, and publish the ones that need a permanent URL. The asset document and its filters are Assets.
| JavaScript | Python | Returns |
|---|---|---|
client.assets.upload(input, { name, mimeType, collection, tags, retentionDays, wait, reuseExisting, signal }) | client.assets.upload(source, name=, mime_type=, collection=, tags=, retention_days=, wait=True, reuse_existing=True, timeout=) | The asset once ingest has written its dimensions and metadata — at most 120 s, then JobFailedError (waiting); with wait: false the new asset straight away, still PROCESSING. Bytes ingested before come back as that asset unless reuseExisting is false. The input is a path (Node), a Blob or bytes; in Python a path, bytes or a binary file object. Python's timeout bounds the PUT to storage, not the wait. |
client.assets.fromUrl(urls, { collection, tags, retentionDays, wait, signal }) | client.assets.from_url(urls, collection=, tags=, retention_days=, wait=True) | { url, asset } or { url, error } per URL, in order, so one bad link costs only itself. The service fetches them; the SDK sends any number, 20 to a request. wait waits for all of them, one status call per tick. |
client.assets.uploadMany(inputs, { concurrency, collection, tags, retentionDays, wait, reuseExisting, signal }) | client.assets.upload_many(sources, concurrency=4, collection=, tags=, retention_days=, wait=True, reuse_existing=True, timeout=) | { name, asset } or { name, error } per input, in order: one stage and one finish call per batch, concurrency uploads at a time (default 4) and one status call per tick while they ingest — for many files, not upload in a loop. Same inputs as upload. |
client.assets.waitReady(id, { intervalMs = 1500, timeoutMs = 120_000, signal }) | client.assets.wait_ready(asset_id, interval=1.5, timeout=120.0) | the asset once it leaves PROCESSING, polled at the interval above; JobFailedError when the time runs out |
client.assets.status(ids, { signal }) | client.assets.status(ids) | [{ id, status, width?, height? }] for up to 100 ids, in request order; ids that are not yours are absent |
client.assets.download(id, { variant = "readable", signal }) | client.assets.download(asset_id, variant="readable") | { bytes, contentType } / bytes. variant is readable (full size, in a type a browser shows), original or preview (a 400 px wide WebP). Follows the signed redirect without sending the API key; both requests are timed and retried like any other. In JavaScript it needs a runtime whose fetch exposes a manual redirect (Node does, a browser does not). |
client.assets.get(id, { signal }) | client.assets.get(asset_id) | the asset |
client.assets.list({ page, perPage, cursor, collection, tag, mime, q, view, minWidth, maxWidth, minHeight, maxHeight, takenFrom, takenTo, createdFrom, createdTo, source, status, includeIntermediate, hasCollection, jobId, op }) | client.assets.list(**params) | { items, meta } / Page, filtered by collection, tag, type, origin, size, capture date, free text and publish state |
client.assets.iterate({ page, perPage, cursor, collection, tag, mime, q, view, minWidth, maxWidth, minHeight, maxHeight, takenFrom, takenTo, createdFrom, createdTo, source, status, includeIntermediate, hasCollection, jobId, op }) | client.assets.iterate(**params) | every matching asset, a row at a time, paging as it goes — for await (const a of …) in JavaScript, for a in … (or async for) in Python. Same filters as list; each page after the first is the meta.nextCursor the last one carried, and it stops when the answer says there is no more. |
client.assets.collections({ q, page, perPage, cursor }) | client.assets.collections(**params) | { items, meta } / Page of your collections — collection, count, lastCreatedAt — most recently added to first; q narrows to names containing it |
client.assets.iterateCollections({ q, page, perPage, cursor }) | client.assets.iterate_collections(**params) | every collection, one at a time, paging as it goes |
client.assets.renameCollection(from, to, { idempotencyKey, signal }) | client.assets.rename_collection(from_, to, idempotency_key=) | { from, to, updated } — every asset in from moved to to; null / None takes them out of any collection. Python writes from_ because from is a keyword. |
client.assets.publish(ids, published = true) | client.assets.publish(ids, published=True) | the updated assets, each with a stable publicUrl on the CDN — the image at full size, not a thumbnail |
client.assets.unpublish(ids) | client.assets.unpublish(ids) | the updated assets |
client.assets.setCollection(ids, collection) | client.assets.set_collection(ids, collection) | the updated assets, each now in that collection |
client.assets.tag(ids, tags) | client.assets.tag(ids, tags) | the updated assets, their tags replaced by tags ([] clears them); list with tag matches one exactly |
client.assets.delete(ids) | client.assets.delete(ids) | { deleted: 1 } for one id, the batch result for several. Permanent. |
jobs — follow, collect, cancel
A job is the handle for work the service promises to finish. ops.run and presets.run create them; this is where you follow one. The job document and its states are Jobs.
| JavaScript | Python | Returns |
|---|---|---|
client.jobs.submit(request, { body, headers, idempotencyKey, signal, retries, timeoutMs, wait }) | client.jobs.submit(request, **opts) | the created job, from a raw job request — ops.run is the usual entry point. In JavaScript wait holds on for the finished job the way ops.run's does |
client.jobs.estimate(request, { body, headers, idempotencyKey, signal, retries, timeoutMs }) | client.jobs.estimate(request, **opts) | the dry-run estimate of a raw job request |
client.jobs.get(id, { signal, wait }) | client.jobs.get(job_id, wait=) | the job, with its first 100 items; wait (seconds, at most 60) holds the call until the job settles or the window closes, and answers with the job as it stands either way |
client.jobs.list({ page, perPage, cursor, status, type, op, preset, rootJobId, createdFrom, createdTo }) | client.jobs.list(**params) | { items, meta } / Page of job summaries — the counts, without items |
client.jobs.iterate({ page, perPage, cursor, status, type, op, preset, rootJobId, createdFrom, createdTo }) | client.jobs.iterate(**params) | every matching job, a row at a time, paging as it goes — the same filters as list |
client.jobs.items(id, { page, perPage, cursor, status }) | client.jobs.items(job_id, **params) | { items, meta } / Page of the job's items past the first 100 — each with the index the API names it by; status narrows to one state |
client.jobs.iterateItems(id, { page, perPage, cursor, status }) | client.jobs.iterate_items(job_id, **params) | every item of a job, one at a time, paging as it goes |
client.jobs.cancel(id) | client.jobs.cancel(job_id) | the job |
client.jobs.resume(id) | client.jobs.resume(job_id) | the new attempt: a job of its own, linked by rootJobId and attemptNumber |
client.jobs.wait(id, { intervalMs = 1000, timeoutMs = 10 * 60_000, onProgress, signal, throwOnFailure = true }) | client.jobs.wait(job_id, interval=1.0, timeout=600.0, on_progress=, throw_on_failure=True) | The job once it is COMPLETED, FAILED or CANCELLED. The service holds each read open until the job settles, so the interval is a floor between reads, not a polling rate. Raises JobFailedError when it ends other than completed (unless throwOnFailure is false) or the time runs out — waiting. |
client.jobs.outputs(job) | client.jobs.outputs(job) | the run's products as list rows, in item order — one paged listing, not one read per item. JavaScript also takes the job's id; Python takes the job |
One read has no method: the totals by status and type, GET /api/v1/jobs/counts — the numbers beside the console's job filters, a table for a person to look at more than a step a program takes. Send it as a raw request, client.get("/api/v1/jobs/counts") in either SDK. A program that watches one status already has its total: meta.total of jobs.list with that status.
presets — saved steps, run by slug
A preset is a versioned list of steps saved once and run over assets as one job — including a chain with a model in it, where the image each step makes for the next is handed on and cleaned up for you, and the dry run prices every step before the first one runs. The same steps run once without a preset as jobs.submit({steps, assetIds}). Built-ins are read-only; start from one by reading it and creating your own. The step format is Presets.
| JavaScript | Python | Returns |
|---|---|---|
client.presets.list(filter, { includeVersions }) | client.presets.list(filter=, include_versions=False) | the presets; filter is builtin or user |
client.presets.get(slug) | client.presets.get(slug) | one preset — steps, subjects, version, versions |
client.presets.create(preset, { idempotencyKey, signal }) | client.presets.create(preset, idempotency_key=) | the saved preset, version 1. An idempotencyKey / idempotency_key makes a retried call return the preset the first one saved, instead of a second preset. |
client.presets.update(slug, preset) | client.presets.update(slug, preset) | the preset as saved — send only what changes; what you leave out is carried over, and new steps are a new version |
client.presets.delete(slug) | client.presets.delete(slug) | nothing |
client.presets.deleteVersion(slug, version) | client.presets.delete_version(slug, version) | nothing. slug@version answers 404 preset_not_found from then on and the number is never reissued, so this is for a version nothing pins — and it is what makes room when a preset is at its version ceiling and update is refused with 422 resource_limit_exceeded. The current version cannot be deleted. |
client.presets.import(presets) | client.presets.import_(presets) | the import result, entry by entry. Python spells it import_ because import is a keyword. |
client.presets.run(presetId, assetIds, { wait, collection, retentionDays, prompt, count, dryRun, imageCount, signal, idempotencyKey }) | client.presets.run(preset_id, asset_ids, collection=, retention_days=, prompt=, count=, dry_run=False, image_count=, wait=False, idempotency_key=) | The job, the finished job with wait, or the estimate with dryRun. presetId is a slug, an id, or slug@version to pin one version; the preset decides the job type. |
templates — HTML and CSS to PNG
A render template is HTML and CSS with {{ var }} placeholders; images.render and the render_template op turn rows of data into PNGs. Templates are versioned: an update saves a new version and the previous one stays readable and renderable as id@version. What a template may contain is Templates.
| JavaScript | Python | Returns |
|---|---|---|
client.templates.list(filter, { page, perPage, cursor }) | client.templates.list(filter=, **params) | one page, built-ins first, then yours; filter is builtin or user. Rows carry no html or css — get returns the whole document |
client.templates.iterate(filter) | client.templates.iterate(filter=) | every template, a row at a time, paging as it goes |
client.templates.get(id) | client.templates.get(template_id) | one template — by id, by your slug, or id@version for one frozen version |
client.templates.versions(id) | client.templates.versions(template_id) | every version ever saved, newest first |
client.templates.create(template) | client.templates.create(template) | version 1, from { name, html, css?, width, height, variables? } |
client.templates.update(id, template) | client.templates.update(template_id, template) | the new version, from only what changes; the previous one is kept |
client.templates.delete(id) | client.templates.delete(template_id) | nothing — the template and every version are gone |
client.templates.import(templates) | client.templates.import_(templates) | each entry created as version 1 from what list("user") returns; an entry that fails is reported, not raised |
webhooks — endpoints and signatures
Register an https:// endpoint and the service POSTs signed events to it when a job finishes. The events, the retry schedule and auto-disable are Webhooks.
| JavaScript | Python | Returns |
|---|---|---|
client.webhooks.list() | client.webhooks.list() | your endpoints, with the secret masked |
client.webhooks.get(id) | client.webhooks.get(endpoint_id) | one endpoint |
client.webhooks.create({ url, events, description, enabled }) | client.webhooks.create(url, events=, description=, enabled=) | the endpoint with its secret — shown this once |
client.webhooks.update(id, patch) | client.webhooks.update(endpoint_id, patch) | the endpoint |
client.webhooks.delete(id) | client.webhooks.delete(endpoint_id) | nothing |
client.webhooks.rotateSecret(id) | client.webhooks.rotate_secret(endpoint_id) | the endpoint with a new secret; the old one stops verifying |
client.webhooks.test(id) | client.webhooks.test(endpoint_id) | the delivery of a test event, sent now |
client.webhooks.deliveries(id, { page, perPage, cursor }) | client.webhooks.deliveries(endpoint_id, **params) | { items, meta } / Page of delivery attempts |
client.webhooks.iterateDeliveries(id, { page, perPage, cursor }) | client.webhooks.iterate_deliveries(endpoint_id, **params) | every delivery to one endpoint, newest first, one at a time, paging as it goes |
client.webhooks.verify(rawBody, header, secret, { toleranceSeconds, now }) | client.webhooks.verify(raw_body, header, secret, tolerance_seconds=300, now=) | Whether the ImageStep-Signature header signs this body and its timestamp is within the tolerance (300 s unless you pass one). A promise in JavaScript (Web Crypto) — await it; a plain bool in Python. No network call. An empty or missing secret throws (TypeError / ValueError) rather than answering: a key nobody set is a signature anybody can make. |
client.webhooks.constructEvent(rawBody, header, secret, { toleranceSeconds, now }) | client.webhooks.construct_event(raw_body, header, secret, tolerance_seconds=300, now=) | Verify, then parse: { id, type, createdAt, data }. Throws an Error in JavaScript and raises WebhookSignatureError in Python when the signature does not check out. |
The same two functions are exported at the top level, so a webhook handler needs no client: verifyWebhookSignature and constructWebhookEvent in JavaScript, verify_webhook_signature and construct_webhook_event in Python. Verify the body exactly as it arrived; parsed and re-serialised JSON has different bytes and never verifies.
agent — the operating contract, and reporting a gap
The rules an automation is expected to follow, and somewhere to say what ImageStep could not do instead of routing around it. More on For agents.
| JavaScript | Python | Returns |
|---|---|---|
client.agent.guidelines() | client.agent.guidelines() | { version, updated, markdown } — how to discover ops, price a batch, decide on a retry; a public endpoint |
client.agent.feedback({ kind, message, op, context, idempotencyKey, signal }) | client.agent.feedback(kind, message, op=, context=, idempotency_key=) | the stored report; kind is capability_gap, bug or other. Free: no credit, no job. |
client.agent.reports({ page, perPage, cursor }) | client.agent.reports(**params) | what this account has reported, newest first: { items, meta } in JavaScript, a Page in Python — the same shape every other list method answers |
client.agent.iterateReports({ page, perPage, cursor }) | client.agent.iterate_reports(**params) | every report this account has filed, one at a time, paging as it goes |
models — the model catalogue
| JavaScript | Python | Returns |
|---|---|---|
client.models.list(mode = "ai_image") | client.models.list(mode="ai_image") | the models with their prices; mode is ai_image or analyze |
usage — what a key has spent
| JavaScript | Python | Returns |
|---|---|---|
client.usage.get({ from, to, groupBy }) | client.usage.get(from_=, to=, group_by="op") | Credits charged, jobs created and items settled over a window, grouped by op (the default), key or day. from and to are YYYY-MM-DD or ISO-8601 instants; the window defaults to the last 30 days. Python writes from_ because from is a keyword. |
Raw requests
For an endpoint you want to call directly, with the same headers, envelope handling, idempotency key and retries as every method above.
| JavaScript | Python | Returns |
|---|---|---|
client.request(method, path, { body, headers = {}, idempotencyKey, signal, retries, timeoutMs = this.timeoutMs }) | client.request(method, path, body=, headers=, idempotency_key=, retries=, timeout=) | { data, meta, replayed, headers } / RequestResult — replayed is true when the service answered with a stored response for that key. |
client.requestBinary(path, { body, contentType, accept = "*/*", signal, retries, timeoutMs = this.timeoutMs }) | client.request_binary(path, content=, json_body=, content_type=, accept="*/*", timeout=, retries=) | A bytes call, with no key: { bytes | json, contentType, headers } in JavaScript; the bytes or the parsed JSON in Python. |
client.get(path, { body, headers, idempotencyKey, signal, retries, timeoutMs }) | client.get(path, **opts) | request() with GET |
client.post(path, body, { body, headers, idempotencyKey, signal, retries, timeoutMs }) | client.post(path, body=, **opts) | request() with POST |
client.put(path, body, { body, headers, idempotencyKey, signal, retries, timeoutMs }) | client.put(path, body=, **opts) | request() with PUT |
client.del(path, { body, headers, idempotencyKey, signal, retries, timeoutMs }) | client.delete(path, **opts) | request() with DELETE |
| — | client.request_binary_result(path, content=, json_body=, content_type=, accept="*/*", timeout=, retries=) | request_binary as a BinaryResult — the bytes, the parsed JSON, the content type and the measured size |
| — | client.put_object(url, data, content_type, timeout=) | PUT bytes to a presigned storage URL, with none of the API's headers — what assets.upload does between its two calls |