IMAGES
Edit an Image
Edit or recombine one or more source images from a text instruction. Synchronous — the edited images come back in the response.
Authorization
AuthorizationstringheaderrequiredBearer token — your API key. Example: Bearer sk-...
Editing is synchronous, like
generation: the request stays open until
the images are ready. Send multipart/form-data — the source images are file
parts, and every other parameter rides alongside them as a text field.
Source images are read only to identify and measure them, and are never
persisted — no stored object, no database row, nothing retained once the
response is sent. Only the model's output is persisted, and only when you ask
for response_format: "url".
The whole request body is capped at 40 MiB, source images and form framing
together; over that is a 413. A model that
only generates — the Endpoints column says which — is a 400 here naming
model, not a silent fallback.
Source images
Repeat the image field, or name it image[], to pass several source
images in one request; the model composes from all of them. The two forms are
equivalent.
Both the number of source images and the size of each are capped per model, and both are enforced before any rendering begins:
| Model | Max source images | Max size each | Bills per source image |
|---|---|---|---|
seedream-4.0 | 14 | 22.5 MiB (23,592,960 bytes) | — |
seedream-4.5 | 14 | 22.5 MiB (23,592,960 bytes) | — |
seedream-5.0 | 14 | 22.5 MiB (23,592,960 bytes) | — |
seedream-5.0-pro | 10 | 22.5 MiB (23,592,960 bytes) | yes |
qwen-image-2.0 | 3 | 7.5 MiB (7,864,320 bytes) | — |
qwen-image-3.0 | 3 | 7.5 MiB (7,864,320 bytes) | yes |
qwen-image-3.0-pro | 3 | 7.5 MiB (7,864,320 bytes) | yes |
qwen-image-edit-plus | 3 | 7.5 MiB (7,864,320 bytes) | — |
Accepted types are PNG, WEBP, JPEG — identified from the bytes, not from
the content type your form declares. HEIC is not accepted, because this surface
never transcodes. An oversize file is a 400 naming which one it was
(image[1]) and the limit it broke, so a rejection is actionable without
guessing.
Which cap you meet first depends on the model. On 4 of
them — seedream-4.0, seedream-4.5, seedream-5.0, seedream-5.0-pro — a full set
of source images at the per-file maximum would far exceed the 40 MiB body cap,
so the body is what binds and those files share the 40 MiB between them. On
the rest, a full set fits inside the body cap with room to spare, so the
per-file limit is the only one you can hit.
Note what the last column costs you: on those models the per-source fee is
charged for each delivered image, not once per request — so where the model
also accepts an n above 1, source images and n multiply. See Billing
below.
Masks are not supported
mask is refused by name rather than ignored: no model on this surface
offers masked editing, so a mask would silently have no effect. Describe
the region you want changed in the prompt instead.
Models
| Model | Endpoints | Size envelope | Grid | Default size | Max n | Reference images | Formats | Transparent |
|---|---|---|---|---|---|---|---|---|
seedream-4.0 | generate + edit | 921,600–16,777,216 px, up to 16:1 | — | 2048x2048 | 10 | 14 | jpeg | — |
seedream-4.5 | generate + edit | 3,686,400–16,777,216 px, up to 16:1 | — | 2048x2048 | 6 | 14 | jpeg | — |
seedream-5.0 | generate + edit | 3,686,400–16,777,216 px, up to 16:1 | — | 2048x2048 | 6 | 14 | jpeg, png | — |
seedream-5.0-pro | generate + edit | 921,600–4,624,220 px, up to 16:1 | — | 2048x2048 | 4 | 10 | jpeg, png | yes |
qwen-image-2.0 | generate + edit | 262,144–4,194,304 px, up to 4:1 | — | 2048x2048 | 6 | 3 | png | — |
qwen-image-3.0 | generate + edit | 262,144–6,553,600 px, up to 8:1 | — | model decides | 1 | 3 | png | — |
qwen-image-3.0-pro | generate + edit | 262,144–6,553,600 px, up to 8:1 | — | model decides | 1 | 3 | png | — |
qwen-image-edit-plus | edit | each side 512–2,048 | 16 px | model decides | 6 | 3 | png | — |
z-image-turbo | generate | 262,144–4,194,304 px, up to 4:1 | 16 px | 1536x1024 | 1 | — | png | — |
size takes any WIDTHxHEIGHT inside the model's envelope — the
envelope column is the whole surface, not a list of presets, and within it the
image arrives at exactly the size you name. Two carve-outs:
- A size below the model's minimum scales up. You get the smallest size
the model renders at your exact aspect ratio — never fewer pixels than you
asked for, and never a changed shape. This is how the OpenAI canonical sizes
work on
seedream-4.5andseedream-5.0, whose pixel floor sits above all three: ask for1024x1024and the delivered image is 2048x2048;720x1280delivers 1440x2560. qwen-image-edit-plusandz-image-turborender on a 16-pixel grid. Each side rounds to the nearest multiple of 16, ties to the even multiple —1000x700arrives as 992x704. Sides already divisible by 16 arrive exactly.
A size outside the envelope — over its pixel ceiling, or at an aspect ratio beyond its limit — is a 400 naming the envelope. A below-minimum size is never an error.
Every model additionally accepts size: "auto", and a model whose default
size reads model decides has no fixed output shape: on an edit it follows your
source image, and on a generation the model picks for itself.
The Transparent column marks the models that can render an alpha channel —
seedream-5.0-pro — and it is an edits-only capability. Transparency
needs something to be transparent around, so on this endpoint
background: "transparent" fails for every model without exception: we refuse
it outright on the models with no alpha support, and on the one that has it the
request is refused for having no source image. Either way you get a 400. On
edits, where it does work, it carries further
conditions worth reading before you rely on it.
The response carries the image, not a size field — measure the bytes if the exact shape matters to you.
Prompt handling
Your prompt is used exactly as you wrote it — no rewriting, no prefix, not even a trim, because leading and trailing whitespace is yours and models do respond to it. Prompts run to 32,000 characters, counted as JavaScript string length, so anything outside the basic multilingual plane — most emoji — counts as two.
seedream-4.0, seedream-4.5, seedream-5.0 and seedream-5.0-pro expand the prompt before rendering it, and there is
no switch to turn that off, so a terse prompt can produce a more elaborate image
than you specified. The
remaining models render what you wrote. Nothing in the response reports the
expanded text, so if reproducibility matters, prefer a model that does not
expand and write the detail yourself.
Delivery
response_format decides where the pixels go, and the two options have
different privacy properties:
"b64_json"(default) inlines the bytes in the JSON response. The image is not stored — no object, no row, no URL, and nothing to expire. Your request is still recorded in your usage history like any other, to the depth your organization's logging level sets."url"stores each image for 24 hours and returns an authenticated URL. The URL is not public: fetching it needs your API key, the same as any other endpoint. After 24 hours it returns 404. There is no delete endpoint — expiry is the only removal path.
Batching
n is bounded per model rather than at OpenAI's flat 10 — see the Max n
column. The ceilings are not one rule: some models have no documented n at
all and are pinned to 1, and on the slowest models a full fan-out would not
finish inside the request wall.
However it is served, n means n separate answers to your whole
prompt. A prompt asking for "six variations" still returns exactly the n
you requested, never a set the model decided on for itself, and never one
picture divided across several files.
Underneath, qwen-image-2.0 and qwen-image-edit-plus render the whole batch in one pass, while
the remaining models render n single images concurrently. Nothing in the
response distinguishes them, and the only place it surfaces is the bill: see
Billing for what n does to a per-source charge.
Unknown parameters are refused
We do not silently drop parameters we do not recognise. Any key outside the documented set is a 400 naming it —
unknown parameter 'foo' — so a typo fails loudly instead of appearing to
work while changing nothing.
Several parameters from OpenAI's images API are recognised and then refused by
name, because no model here implements them. The refusal says
which one and why, so you can tell whether to drop the parameter or change
model. quality is the one partial case — it is accepted, but only as
"auto":
| Parameter | Why |
|---|---|
style | No model here exposes a style control |
quality | Only "auto" is legal; no model here exposes a quality control |
output_compression | This surface never transcodes a delivered image |
input_fidelity | Not supported by any model here |
moderation | Content policy is always applied and is not configurable |
mask | No model here offers masked editing |
If you are porting OpenAI code, strip these before your first call.
Errors
Every failure is the same { "error": { "message", "type", … } } envelope.
Read the status and type together — the 5xx family all report api_error,
so the status is what separates a transient failure worth retrying from one
that is not.
| Status | type | What it means |
|---|---|---|
| 400 | invalid_request_error | A parameter is missing, unknown, or not valid for this model. param names it when one parameter is at fault; a refusal from the model itself carries a message but no param |
| 400 | image_generation_user_error | The prompt or a source image was refused by the model's content policy. code is moderation_blocked |
| 401 | authentication_error | Missing, malformed or unrecognised credential |
| 402 | insufficient_credit | Your organization is out of credit |
| 403 | permission_error | The key is inactive or expired, or the model is not enabled in your region |
| 413 | invalid_request_error | The request body is over the endpoint's size limit |
| 429 | rate_limit_error | Over your request rate, or the model is not priced on your tier or in your billing currency |
| 429 | quota_exceeded | Over a spend budget: the API key's, its budget for this model, your member budget or your organization's. code names which |
| 500 | api_error | Something on our side failed, including a gate that could not reach its own dependency and refused rather than guess |
| 502 | api_error | The model produced a response we could not interpret |
| 503 | api_error | The model is temporarily unavailable or over capacity — retry with backoff |
| 504 | api_error | The model did not finish in time |
Every response carries X-RateLimit-Limit-RPM, -TPM and -TPD with
their -Remaining counterparts, which is where to look when a 429 is not
obviously yours.
Two of these are worth planning for specifically. Capacity pressure reaches you as a 503, never a 429 — a retry policy keyed only on 429 will not fire for it, and it is the most common transient failure on this surface. And a 429 is not always about rate: an unpriced model on your tier, or one with no price in your billing currency, is refused the same way.
Billing
Images are billed per delivered image. Most models carry a single rate for
every size they render; on seedream-5.0-pro and qwen-image-3.0-pro the rate steps up once the
delivered image's pixel count crosses the model's large-size threshold —
it is the delivered pixels that pick the rate, so naming a size pins it.
A model whose default reads model decides can come back at a shape you did
not pick, and you are billed for the shape that arrived. Your usage history
itemizes each request into per-meter line items, one per rate applied, so what
you were charged for is always visible there. Your tier's price list is the
authority on the rates themselves and on where each model's threshold sits.
seedream-5.0-pro, qwen-image-3.0 and qwen-image-3.0-pro additionally bill per source image on an edit, at a
flat rate that does not vary with size. That line counts source images per
delivered image, not once per request — so on a model that accepts an n
above 1, three source images with n: 2 bills six rather than three. Where
the ceiling is 1 the two numbers are the same and the distinction never
appears.
A request that returns no images is not billed at all, and a partly successful batch is billed only for the images it delivered — source images included.
Images carry no token meter. They report zero tokens and add nothing to your tokens-per-minute or tokens-per-day counters. They are still checked against those limits, though: an image request refused with a token-limit 429 is being turned away on allowance your other traffic has already spent, not on anything the image itself consumed.
Request body
multipart/form-datamodelstringrequiredThe image model to use (e.g. "qwen-image-edit-plus"). Only models whose Endpoints column includes "edit" serve this endpoint.
imagestring | anyrequiredThe source image(s) to edit, as multipart file parts; at least one is required, and sending none is a 400. Repeat the field, or name it `image[]`, to send several — see Source images below for the per-model caps on count and size. We identify each file from its own bytes and ignore the content type your form declares, so a mislabelled file is fine; one we cannot read, or whose bytes disagree with themselves about their format, is a 400 naming `image`.
promptstringrequiredText description of the edit to make (1–32,000 characters). Used verbatim; some models expand it before rendering — see Prompt handling below.
nintegerHow many edited images to produce (defaults to 1), bounded per model — see the `Max n` column in the model table. Sent as a form field; a numeric string is read as the number it stands for. On the models that bill per source image, `n` multiplies that charge — see Billing.
sizestringOutput size as WIDTHxHEIGHT — any size inside the model's envelope (see the model table), not an enum; an explicit size overrides the shape of your source image. Omitting it, or sending `"auto"`, falls back to the model's `Default size`; where that column reads *model decides*, the edit follows the shape of your source image rather than any fixed size.
response_format"b64_json" | "url"How to deliver the images. `"b64_json"` (the default) inlines the bytes and stores nothing; `"url"` stores each image for 24 hours and returns an authenticated URL.
b64_jsonurloutput_formatstringDelivered image format, from the model's supported list. Omit to let the model use its native format. One override exists: `background: "transparent"` forces PNG, because an alpha channel cannot survive a JPEG — an explicit `output_format: "jpeg"` alongside it is silently upgraded rather than refused.
background"transparent" | "opaque" | "auto"Background handling. `"transparent"` renders an alpha channel and forces the delivered format to PNG. It is the narrowest option on this surface, and it is refused in two places: we reject it immediately on any model not marked `Transparent` in the model table, and on the one that is, the model additionally requires a source image that is a PNG already containing transparent pixels — a photograph or a flat PNG comes back as a 400 after the full render latency. `"opaque"` and `"auto"` (the default) are accepted everywhere and are both no-ops; no model here exposes a background control beyond transparency.
transparentopaqueautoquality"auto"Accepted for OpenAI compatibility, and only as `"auto"`.
userstringAn opaque identifier for your end user. Recorded in your logs and never passed to the model.
Response
createdintegerrequiredUnix timestamp of when the images were returned.
dataobject[]requiredOne entry per image the model actually delivered, in delivery order. A partly-successful batch returns only its survivors, with a `200` and no error field — compare `data.length` against the `n` you asked for. You are billed for only the images you received.
Request
import OpenAI from "openai";
import { createReadStream } from "node:fs";
const client = new OpenAI({
apiKey: "your-scx-api-key",
baseURL: "https://api.scx.ai/v1",
});
const result = await client.images.edit({
model: "qwen-image-edit-plus",
image: createReadStream("kite.jpg"),
prompt: "Make it a stormy afternoon, keep the kite exactly as it is",
});
const bytes = Buffer.from(result.data[0].b64_json, "base64");
await Bun.write("kite-stormy.png", bytes);import OpenAI from "openai";
import { createReadStream } from "node:fs";
const client = new OpenAI({
apiKey: "your-scx-api-key",
baseURL: "https://api.scx.ai/v1",
});
const result = await client.images.edit({
model: "qwen-image-edit-plus",
image: createReadStream("kite.jpg"),
prompt: "Make it a stormy afternoon, keep the kite exactly as it is",
});
const bytes = Buffer.from(result.data[0].b64_json, "base64");
await Bun.write("kite-stormy.png", bytes);Response
{
"created": 1755820800,
"data": [
{
"b64_json": "iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAYAAAB/HSuDAAA...(truncated)"
}
]
}{
"created": 1755820800,
"data": [
{
"b64_json": "iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAYAAAB/HSuDAAA...(truncated)"
}
]
}