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.

POST/v1/images/edits

Authorization

Authorizationstringheaderrequired

Bearer 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:

ModelMax source imagesMax size eachBills per source image
seedream-4.01422.5 MiB (23,592,960 bytes)—
seedream-4.51422.5 MiB (23,592,960 bytes)—
seedream-5.01422.5 MiB (23,592,960 bytes)—
seedream-5.0-pro1022.5 MiB (23,592,960 bytes)yes
qwen-image-2.037.5 MiB (7,864,320 bytes)—
qwen-image-3.037.5 MiB (7,864,320 bytes)yes
qwen-image-3.0-pro37.5 MiB (7,864,320 bytes)yes
qwen-image-edit-plus37.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

ModelEndpointsSize envelopeGridDefault sizeMax nReference imagesFormatsTransparent
seedream-4.0generate + edit921,600–16,777,216 px, up to 16:1—2048x20481014jpeg—
seedream-4.5generate + edit3,686,400–16,777,216 px, up to 16:1—2048x2048614jpeg—
seedream-5.0generate + edit3,686,400–16,777,216 px, up to 16:1—2048x2048614jpeg, png—
seedream-5.0-progenerate + edit921,600–4,624,220 px, up to 16:1—2048x2048410jpeg, pngyes
qwen-image-2.0generate + edit262,144–4,194,304 px, up to 4:1—2048x204863png—
qwen-image-3.0generate + edit262,144–6,553,600 px, up to 8:1—model decides13png—
qwen-image-3.0-progenerate + edit262,144–6,553,600 px, up to 8:1—model decides13png—
qwen-image-edit-plusediteach side 512–2,04816 pxmodel decides63png—
z-image-turbogenerate262,144–4,194,304 px, up to 4:116 px1536x10241—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.5 and seedream-5.0, whose pixel floor sits above all three: ask for 1024x1024 and the delivered image is 2048x2048; 720x1280 delivers 1440x2560.
  • qwen-image-edit-plus and z-image-turbo render on a 16-pixel grid. Each side rounds to the nearest multiple of 16, ties to the even multiple — 1000x700 arrives 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":

ParameterWhy
styleNo model here exposes a style control
qualityOnly "auto" is legal; no model here exposes a quality control
output_compressionThis surface never transcodes a delivered image
input_fidelityNot supported by any model here
moderationContent policy is always applied and is not configurable
maskNo 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.

StatustypeWhat it means
400invalid_request_errorA 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
400image_generation_user_errorThe prompt or a source image was refused by the model's content policy. code is moderation_blocked
401authentication_errorMissing, malformed or unrecognised credential
402insufficient_creditYour organization is out of credit
403permission_errorThe key is inactive or expired, or the model is not enabled in your region
413invalid_request_errorThe request body is over the endpoint's size limit
429rate_limit_errorOver your request rate, or the model is not priced on your tier or in your billing currency
429quota_exceededOver a spend budget: the API key's, its budget for this model, your member budget or your organization's. code names which
500api_errorSomething on our side failed, including a gate that could not reach its own dependency and refused rather than guess
502api_errorThe model produced a response we could not interpret
503api_errorThe model is temporarily unavailable or over capacity — retry with backoff
504api_errorThe 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-data
modelstringrequired

The image model to use (e.g. "qwen-image-edit-plus"). Only models whose Endpoints column includes "edit" serve this endpoint.

imagestring | anyrequired

The 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`.

promptstringrequired

Text description of the edit to make (1–32,000 characters). Used verbatim; some models expand it before rendering — see Prompt handling below.

ninteger

How 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.

sizestring

Output 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_jsonurl
output_formatstring

Delivered 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.

transparentopaqueauto
quality"auto"

Accepted for OpenAI compatibility, and only as `"auto"`.

userstring

An opaque identifier for your end user. Recorded in your logs and never passed to the model.

Response

createdintegerrequired

Unix timestamp of when the images were returned.

dataobject[]required

One 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);

Response

{
  "created": 1755820800,
  "data": [
    {
      "b64_json": "iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAYAAAB/HSuDAAA...(truncated)"
    }
  ]
}