Skip to main content

Qwen Image 2.1 image editing revises one to ten reference images from a written instruction and returns one frame at 1k, 1.5k or 2k. It also does local edits with a black-and-white mask, where white is repainted.

From $0.036/image

Workbench

Send a request

Quickstart

Examples use the selected endpoint’s public parameter definitions. Replace the API key and example media, then send the request from your server.

SDKs & integrations
POSThttps://api.spicyapi.ai/api/v1/jobs/createTask

API authentication

Keep the API key in the server environment variable SPICY_API_KEY. Use Bearer authentication and Content-Type: application/json for JSON requests.

Authorization: Bearer $SPICY_API_KEY
Qwen Image 2.1 · Image Editing · Sample output
Sample outputUse this example
# Generate the idempotency key once; reuse it when retrying after a lost response.
export SPICY_IDEMPOTENCY_KEY="${SPICY_IDEMPOTENCY_KEY:-$(uuidgen)}"
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
  -d '{
  "model": "alibaba/qwen-image-2.1/edit",
  "input": {
    "aspect_ratio": "1:1",
    "background": "opaque",
    "image_urls": [
      "https://example.com/reference.png"
    ],
    "output_format": "jpeg",
    "prompt": "A ceramic teapot on a linen cloth, soft window light, muted palette",
    "resolution": "1k"
  }
}'

# Copy data.taskId from the response into TASK_ID, then poll the status.
TASK_ID="TASK_ID_FROM_RESPONSE"
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
  -H "Authorization: Bearer $SPICY_API_KEY"

Generated from this model’s input schema — the same one the API validates every request against.

Input parameters

Place these fields inside the request body’s input object. Names and types match the API. Do not assume a default when none is specified.

image_urlsarray<string>RequiredAffects price

One to ten reference images. The model does not assign roles on its own, so name in the prompt what each source contributes — which is the person, which is the setting, which is the garment. Every reference you add raises the quote, so send only the ones doing work.

Default
Not specified
Constraints
minItems:1
maxItems:10
promptstringRequired

Up to 5,000 characters. There is no negative prompt on this family, so exclusions belong here too — name what is in the frame, then say plainly what must not be. One continuous description outperforms a pile of keywords.

Default
Not specified
Constraints
minLength:1
maxLength:5000
seedintegerOptional

Steadies a draw so a series stays in the same neighbourhood. It is not a reproduction guarantee — we sent the same prompt and seed twice and got two different frames. Keep the returned asset when you need that exact image again.

Default
Not specified
Constraints
minimum:0
maximum:2147483647
output_formatstringOptional

JPEG by default. PNG when the frame goes into a compositing step, WebP when it ships straight to a page. The container does not change the price or the render.

Default
"jpeg"
Allowed values
"jpeg""png""webp"
backgroundstringOptional

opaque by default. transparent returns the subject alone on a real alpha channel — pair it with png or webp output (jpeg has no alpha and the request is rejected), and describe only the subject: naming a room, surface or scene can bring a background back. Offered at 1k and 2k, not at 1.5k. No effect on price. It cannot be combined with mask_url.

Default
"opaque"
Allowed values
"opaque""transparent"
enable_prompt_expansionbooleanOptional

On by default: before generating, the prompt is rewritten into a fuller scene description. Turn it off to have your own wording used as written. Switching it off is offered at 1k and 2k, not at 1.5k. No effect on price. It has no effect when mask_url is set.

Default
true
resolutionstringOptionalAffects price

The tier that sets output size and price: 1k is the default and the cheapest, 1.5k sits in between, 2k is where skin texture, hair and fabric weave hold together. Iterate at 1k and promote the keeper to 2k.

Default
"1k"
Allowed values
"1k""1.5k""2k"
aspect_ratiostringOptional

Composes the frame before generation rather than cropping afterwards, so pick the shape you will publish at. Fifteen options, from the 3:1 and 21:9 strips to 1:3 and 9:21 columns. All ratios cost the same.

Default
"1:1"
Allowed values
"1:1""16:9""9:16""4:3""3:4""3:2""2:3""21:9""9:21""2:1""1:2""3:1""1:3""5:4""4:5"
mask_urlstringOptional

Optional black-and-white image that switches the edit to local mode: white marks the area to repaint, black is kept as it is, and a few pixels of soft edge help the blend. Use it with exactly one reference image of the same aspect ratio (a mask of another size is scaled to fit), and describe in the prompt what should appear in the white area. The output keeps the reference image's shape, so aspect_ratio is ignored, and it cannot be combined with a transparent background. Offered at 1k and 2k, not at 1.5k. No effect on price.

Default
Not specified
Constraints
format:"uri"
contentMediaType:["image/jpeg","image/png","image/webp","image/gif"]

Track the task

HTTP 202 means the task was accepted, not completed. Save the returned taskId and query that task. Do not create another task while waiting for its result.

Accepted response
HTTP/1.1 202 Accepted
Location: /api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3

{
  "code": 200,
  "msg": "success",
  "request_id": "req_example_create",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "state": "queued",
    "estimatedCost": "0.750000"
  }
}

The accepted response includes the estimate so your app can show the same amount in task history and billing.

Read the job
curl --get "https://api.spicyapi.ai/api/v1/jobs/recordInfo" \
  --data-urlencode "taskId=$TASK_ID" \
  -H "Authorization: Bearer $SPICY_API_KEY"

Start polling at about 2 seconds and gradually increase the interval. Stop at a terminal state. After a connection failure, query the original taskId. Retry creation only with the original idempotency key and body.

Or let the callback find you
import { verifyWebhook } from "@spicyapi/sdk";

// express.raw keeps the bytes we signed; express.json never matches.
app.post("/hooks/spicy", express.raw({ type: "application/json" }), (req, res) => {
  const { payload } = verifyWebhook({
    rawBody: req.body,
    timestamp: req.header("X-Webhook-Timestamp"),
    signature: req.header("X-Webhook-Signature"),
    payloadVersion: Number(req.header("X-Webhook-Payload-Version")),
    secret: process.env.SPICY_WEBHOOK_SECRET,
  });
  res.status(200).end();
  void handleFinishedTask(payload);
});

Pass callBackUrl when you create the task and the finished record is POSTed to you — no polling loop at all. Every delivery carries X-Webhook-Timestamp, X-Webhook-Signature and X-Webhook-Payload-Version. The signature is a base64 HMAC-SHA256 over taskId.timestamp.sha256(body), so it must be checked against the raw bytes: parsing the JSON and re-serialising it changes them and the check will fail. Failed deliveries are retried, so the same taskId can arrive twice — make the handler idempotent. Answer 2xx first and do the work afterwards; a slow handler is treated as a failed delivery.

Read the result

Consume output only when state is succeeded. On failure, use errorCode, errorMessage and request_id to investigate. Example costs and states do not represent an actual bill.

Completed response example
{
  "code": 200,
  "msg": "success",
  "request_id": "req_example_record",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "alibaba/qwen-image-2.1/edit",
    "state": "succeeded",
    "output": {
      "assets": [
        {
          "key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/3f7a1c9e5b2d48f0a6c1e3d5b7f9a1c3.png",
          "mime": "image/png",
          "url": "https://example.r2.cloudflarestorage.com/results/job_01k3m8x9q2z4v7n5p6r8s0t1w3/result.png?X-Amz-Signature=SIGNATURE_FROM_RESPONSE",
          "expiresAt": "2026-08-28T12:20:08.000Z"
        }
      ]
    },
    "cost": "0.750000",
    "settled": true,
    "createdAt": "2026-08-28T12:00:00Z",
    "completedAt": "2026-08-28T12:00:30Z"
  }
}

For each available asset, open or download assets[].url directly without an API Authorization header. The signed URL expires at expiresAt; poll the task again to refresh it while the asset remains within its retention period. A pending or unavailable asset has no URL. Treat signed URLs as private.

SDKs & integrations

Use SpicyAPI in your app, terminal, AI assistant or visual workflow. Choose a setup guide below.

Use it in ComfyUI

Install the SpicyAPI nodes from ComfyUI Manager or with the command below, then search for Qwen Image 2.1 · Image Editing on the canvas. The node has the same options as this page and shows its price before it runs.

cd ComfyUI/custom_nodes
git clone https://github.com/spicyapi-ai/comfyui-spicyapi