Quickstart
From a key to a background removed and a public URL, in whichever of the SDKs, the CLI, curl or an MCP client you use. Then the same image through a saved preset of four steps, and the one-call road for when you want nothing stored.
1. Get a key
Sign in and create an API key — it is shown once. The Free plan needs no card and comes with a $3 welcome credit for AI ops. Every call sends the key as
Authorization: ApiKey is_sk_…; every sample below reads it fromIMAGESTEP_API_KEY. Install what you will call from:pnpm add imagestep # JavaScript / TypeScript pip install imagestep # Python pnpm add -g imagestep-cli # the CLI export IMAGESTEP_API_KEY=is_sk_…2. Upload, then run an op
Over REST an upload is one call —
POST /api/v1/assets/upload, the image as the body with its ownContent-Type, up to 25 MB — and the answer is the asset. The SDKs, the CLI and MCP take a file of any size in one line (assets has the three-step route they use underneath). An op on that asset is a job,{"op": "remove_bg", "assetIds": [...]}, and the same request with?dryRun=trueprices it first and creates nothing.JavaScript
import { ImageStep } from "imagestep"; const client = new ImageStep({ apiKey: process.env.IMAGESTEP_API_KEY }); const asset = await client.assets.upload("./product.jpg"); const price = await client.ops.estimate("remove_bg", { assetIds: asset.id }); // nothing is created const job = await client.ops.removeBg(asset.id, { wait: true }); const [cutout] = await client.jobs.outputs(job); const [published] = await client.assets.publish(cutout.id); console.log(published.publicUrl);Python
from imagestep import ImageStep client = ImageStep() # reads IMAGESTEP_API_KEY asset = client.assets.upload("./product.jpg") price = client.ops.estimate("remove_bg", asset_ids=asset["id"]) # nothing is created job = client.ops.remove_bg(asset["id"], wait=True) cutout = client.jobs.outputs(job)[0] published = client.assets.publish(cutout["id"])[0] print(published["publicUrl"])CLI
# pnpm add -g imagestep-cli, then imagestep login — or export IMAGESTEP_API_KEY imagestep asset upload ./product.jpg -o json # price it (nothing is created), then run it and block until it is done imagestep jobs estimate --op remove_bg --asset-ids <asset-id> imagestep jobs submit --op remove_bg --asset-ids <asset-id> --wait -o json # the bytes, and a permanent URL imagestep jobs outputs <job-id> --download ./out imagestep asset publish <result-asset-id>curl
# 0. upload — the bytes are the body, the answer is the asset id curl -s "https://api.imagestep.dev/api/v1/assets/upload?name=product.jpg" \ -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: image/jpeg" \ --data-binary @product.jpg # 1. price it (nothing is created) curl -s https://api.imagestep.dev/api/v1/jobs?dryRun=true \ -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \ -d '{"op":"remove_bg","assetIds":["<asset-id>"]}' # 2. run it, and wait for it in the same call: "wait" holds the response until the job is done # (60 s at most) — 200 with the finished job and its resultAssetId, or 202 if it is still running curl -s https://api.imagestep.dev/api/v1/jobs \ -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"op":"remove_bg","assetIds":["<asset-id>"],"wait":30}' # 3. only after a 202: wait again (or subscribe to the job.completed webhook and do not wait at all) curl -s "https://api.imagestep.dev/api/v1/jobs/<job-id>?wait=30" -H "Authorization: ApiKey $IMAGESTEP_API_KEY" # 4. a permanent URL: publish the result — its id is items[0].resultAssetId of the finished job curl -s https://api.imagestep.dev/api/v1/assets/update \ -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \ -d '{"ids":["<result-asset-id>"],"published":true}'MCP
# tool call — upload, run, wait and publish are one call; the answer carries publicUrl transform {"op": "remove_bg", "file_paths": ["./product.jpg"]} # the same call with "dry_run": true prices it and creates nothing # the hosted server cannot read your disk: give it "urls" or "asset_ids" instead of "file_paths"Uploading is for work you want kept. If you are holding an image and only want the result back, the shorter road below is one call.
3. Get the result
waitholds the response until a one-item job is done, 60 s at most:200with the finished job, or202while it is still running —GET /api/v1/jobs/{id}?wait=then waits again. A batch answers at once and ignoreswait: read it by id the same way, or register a webhook forjob.completedandjob.failedand never wait at all (webhooks).Each output is a new asset —
items[i].resultAssetIdof the finished job — and the input is left as it was. Publish an output and it gets apublicUrloncdn.imagestep.devthat does not change; or download its bytes. What each of these things is — assets, ops, jobs — has its own page; every error you can meet is on errors, retries & limits.
Next: several steps, one job
A preset is a list of steps you save once — versioned, under your account — and then run by name. This one is the preset the home page runs: cut the product out, trim it, put it on white, get it under 50 KB.
{
"name": "Marketplace cut-out",
"slug": "marketplace-cutout",
"description": "Background removed, trimmed to the product, on white, a WebP under 50 KB.",
"steps": [
{
"op": "remove_bg"
},
{
"op": "trim"
},
{
"op": "flatten",
"parameters": {
"background": "#ffffff"
}
},
{
"op": "compress",
"parameters": {
"format": "webp",
"maxBytes": 50000
}
}
]
}JavaScript
import { readFile } from "node:fs/promises";
const preset = await client.presets.create(JSON.parse(await readFile("marketplace-cutout.json", "utf8")));
console.log(preset.slug, preset.version); // "marketplace-cutout" 1Python
import json
preset = client.presets.create(json.load(open("marketplace-cutout.json")))
print(preset["slug"], preset["version"]) # "marketplace-cutout" 1CLI
imagestep preset create -f marketplace-cutout.json -o jsoncurl
curl -s https://api.imagestep.dev/api/v1/presets -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" -d @marketplace-cutout.jsonMCP
# tool call — an agent has no disk to read, so the document travels in the call
save_preset {"name":"Marketplace cut-out","slug":"marketplace-cutout","description":"Background removed, trimmed to the product, on white, a WebP under 50 KB.","steps":[{"op":"remove_bg"},{"op":"trim"},{"op":"flatten","parameters":{"background":"#ffffff"}},{"op":"compress","parameters":{"format":"webp","maxBytes":50000}}]}Then price it and run it over the asset you uploaded above. One of its steps is a model, so it runs as a single chain job: the intermediate images are not yours to manage, and the bill is per step.
JavaScript
const price = await client.presets.run("marketplace-cutout", ["<asset-id>"], { dryRun: true }); // nothing is created
console.log(price.estimatedCredits, price.steps);
const job = await client.presets.run("marketplace-cutout", ["<asset-id>"], { wait: true });
const outputs = await client.jobs.outputs(job);Python
price = client.presets.run("marketplace-cutout", ["<asset-id>"], dry_run=True) # nothing is created
print(price["estimatedCredits"], price.get("steps"))
job = client.presets.run("marketplace-cutout", ["<asset-id>"], wait=True)
outputs = client.jobs.outputs(job)CLI
imagestep jobs estimate --preset-id marketplace-cutout --asset-ids <asset-id>
imagestep jobs submit --preset-id marketplace-cutout --asset-ids <asset-id> --wait -o jsoncurl
# price it — the same body, nothing is created
curl -s "https://api.imagestep.dev/api/v1/jobs?dryRun=true" -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
-d '{"presetId":"marketplace-cutout","assetIds":["<asset-id>"]}'
# run it; "wait" holds the response until the job is done (60 s at most, then 202 and you wait again)
curl -s https://api.imagestep.dev/api/v1/jobs -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
-d '{"presetId":"marketplace-cutout","assetIds":["<asset-id>"],"wait":30}'MCP
# tool calls — the first prices it and creates nothing
run_preset {"preset":"marketplace-cutout","asset_ids":["<asset-id>"],"dry_run":true}
run_preset {"preset":"marketplace-cutout","asset_ids":["<asset-id>"]}The dry run lists the steps the way they are billed — the model at its price (227 credits is the $0.0227 the catalogue lists for remove_bg), the three deterministic steps as one process segment at 0:
{
"type": "chain",
"presetId": "pre_66a38dbbde6b43659060c8fa7c492173",
"presetName": "Marketplace cut-out",
"presetVersion": 1,
"totalItems": 1,
"costPerItem": 227,
"estimatedCredits": 227,
"creditBalance": 30000,
"sufficientCredit": true,
"assetCountLeft": 199,
"processCountLeft": 200,
"overageRuns": 0,
"overageCredits": 0,
"processPerItem": 1,
"steps": [
{ "index": 0, "op": "remove_bg", "model": "fal-ai/bria/background/remove", "costPerItem": 227, "maxInputEdge": 2048 },
{ "index": 1, "op": "process", "costPerItem": 0 }
]
}What happens when a step fails, what you pay for then, and how resume picks up from that step: chains. More presets to copy, this one included: recipes.
The shorter road: one call, nothing stored
Everything above is a job: this service has promised to finish the work, which is what the asset, the price and the webhook are for. When you are holding the image and only want the result back, a deterministic op — resize, convert, compress, crop and the rest of what GET /api/v1/ops marks with a syncEndpoint — runs while you wait: bytes in, bytes out, no upload, no asset, no publish. It is free on a paid plan; on Free it counts against the monthly allowance of deterministic ops and is paid from your balance past it. If it fails you send it again, so there is no Idempotency-Key either. AI ops never go this way; a provider call needs an owner for its retry and refund.
JavaScript
import { writeFile } from "node:fs/promises";
const small = await client.images.transform("resize", { file: "./product.jpg", parameters: { width: 1200 } });
await writeFile("out.jpg", small);Python
small = client.images.transform("resize", file="./product.jpg", parameters={"width": 1200})
open("out.jpg", "wb").write(small)CLI
imagestep image resize ./product.jpg --width 1200 --out out.jpgcurl
curl --data-binary @product.jpg -H "Content-Type: image/jpeg" \
-H "Authorization: ApiKey $IMAGESTEP_API_KEY" \
"https://api.imagestep.dev/api/v1/images/transform?op=resize&width=1200" -o out.jpgcurl -F
curl -F file=@product.jpg -H "Authorization: ApiKey $IMAGESTEP_API_KEY" \
"https://api.imagestep.dev/api/v1/images/transform?op=resize&width=1200" -o out.jpgA saved preset runs several steps in the same one call — if every step is deterministic: marketplace-cutout above starts with a model, so it is a job and only a job. A template renders to a PNG, and reading metadata is free — limits and the line between the two roads are on synchronous ops.
Not writing code?
The same two roads exist where there is no code to paste. Each page opens with its own install and first call.
- n8n — install the community node, add the ImageStep credential, pick an op from the Op dropdown. Store Result off is the one-call road; on is a job with an asset id and a permanent URL.
- Claude, Cursor or any MCP client — one config block, hosted or local; the tab above is what the agent then calls.
- A coding agent with a shell —
imagestep skill --install claude-codeteaches it to drive the CLI.
An agent choosing between these starts on the agents page.