Qwen-ImageAPI reference
Generate and edit images with the Qwen-Image models from code: a three-step quick start, every field, per-image credits, webhooks and idempotency.
Developer API access is by application.
The image API is in beta, and its fields may change. The studio is not affected.
Quick startin three calls
Set a key, send the job with a price cap, then read the result.
- 01
Set your key
Developer API access is by application: request it in the studio under API keys. A key is shown once, so keep it in an environment variable.
export API_KEY="sk_test_…" - 02
Send the job
POST the image with an Idempotency-Key and a max_credits cap. Edit models also take image_url. You get 202 and a queued job, and the credits are held.
curl https://api.pershot.app/v1/images/generations \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen-image-2512", "prompt": "A ceramic mug on a walnut table, soft window light, product photo", "size": "1K", "aspect_ratio": "1:1", "max_credits": 2 }'{ "id": "job_01J9ZK4T8W", "object": "job", "kind": "image", "status": "queued", "money_status": "reserved", "queue_position": 2, "requested_model": "qwen-image-2512", "credits_quoted": 2, "credits_charged": null } - 03
Read the result
GET the job until its status is succeeded. outputs[].url is signed and expires after 24 hours, so download what you keep.
curl https://api.pershot.app/v1/jobs/job_01J9ZK4T8W \ -H "Authorization: Bearer $API_KEY"
Parametersand limits
Read from the OpenAPI document, which is generated from the same schemas the API validates against. Unknown fields are refused.
| Field | Type | Allowed | Notes |
|---|---|---|---|
modelRequired | string | qwen-image-2512-lightning · qwen-image-2512 · qwen-image-edit-2511-lightning · qwen-image-edit-2511 | The model id. |
promptRequired | string | 2–2,000 characters | What to render: the subject, the camera and the light. |
sizeDefault "1K" | string | 1K · 2K | 1K for drafts and first frames, 2K for detail. |
aspect_ratioDefault "1:1" | string | 16:9 · 9:16 · 1:1 | Frame shape. |
image_urlOptional | string | https URL | First frame for video; the source image for the edit models. A public https URL or an upload URL. |
seedOptional | integer | 0–2,147,483,647 | Repeat a result with the same inputs. |
quote_idOptional | string | qt_… | Run at a quote from POST /v1/quotes, valid for 10 minutes. |
max_creditsOptional | number | > 0 | The most this job may cost. Above it the job is refused, never charged. |
webhook_urlOptional | string | https URL | An https URL that receives the job's events. |
end_user_idOptional | string | 1–128 characters | For approved developer accounts (access is by application): your own user id for this request, shown in usage. |
project_idOptional | string | prj_… | |
shot_idOptional | string | sht_… | |
routing_preferenceOptional | string | stability · speed |
Send exactly one of quote_id or max_credits. This endpoint refuses model "auto": for video, call POST /v1/auto/plan first, then send the model it picks with max_credits.
- Models
qwen-image-2512-lightningGenerate · Qwen-Image-2512 · Lightning 4-stepqwen-image-edit-2511-lightningEdit · Qwen-Image-Edit-2511 · Lightningqwen-image-2512Generate · Qwen-Image-2512qwen-image-edit-2511Edit · Qwen-Image-Edit-2511- Rate and concurrency
- 60 requests a minute per key, and 3 jobs at a time per account by default. More keys do not add concurrency.
- For AI agents
- Plain text for AI agents: inputs, legal values, every price and a request example
qwen-image-2512-lightningqwen-image-2512qwen-image-edit-2511-lightningqwen-image-edit-2511
Limits
Read from the same table the API checks against. Each row says who checks the limit and what happens past it.
- EnforcedWe check it and answer 400 before anything is quoted or held.
- SupplierThe supplier checks it. A job it refuses fails, and nothing is charged.
- RecommendedNobody checks it. Staying inside it gives better results.
| Limit | Value | Unit | Who checks | Past the limit |
|---|---|---|---|---|
| Request | ||||
Prompt lengthprompt | 2 to 2,000 | characters | Enforced | 400 invalid_request on prompt, before any quote or hold. |
Image sizesize | 1K, 2KDefault 1K | – | Enforced | 400 invalid_request on size. |
Aspect ratioaspect_ratio | 16:9, 9:16, 1:1Default 1:1 | – | Enforced | 400 invalid_request on aspect_ratio. |
| Images you send | ||||
Source imageimage_url | None | – | Enforced | Edit models need image_url and generate models refuse it: 400 invalid_request on image_url. |
| Output | ||||
| Output pixels | 1K16:91344×7689:16768×13441:11024×10242K16:92048×11529:161152×20481:12048×2048 | px | Enforced | We ask for exactly these sizes. |
| Output side | 256 to 2,048 | px per side | Supplier | Every size above fits. The supplier refuses larger ones. |
| Output format | JPEG | – | Enforced | Every image is a JPEG. |
| Links | ||||
| Output link | 24 | hours | Enforced | After that the link answers 403. GET the job again for a fresh link. The file itself is kept for 12 months (the job's outputs_expire_at gives the date), longer if the result is saved to Assets or used in a project. |
| Limit | Value | Unit | Who checks | Past the limit |
|---|---|---|---|---|
| Request | ||||
Prompt lengthprompt | 2 to 2,000 | characters | Enforced | 400 invalid_request on prompt, before any quote or hold. |
Image sizesize | 1K, 2KDefault 1K | – | Enforced | 400 invalid_request on size. |
Aspect ratioaspect_ratio | 16:9, 9:16, 1:1Default 1:1 | – | Enforced | 400 invalid_request on aspect_ratio. |
| Images you send | ||||
Source imageimage_url | Exactly 1 | – | Enforced | Edit models need image_url and generate models refuse it: 400 invalid_request on image_url. |
Upload sizefile | up to 10 | MB | Enforced | POST /v1/uploads answers 400 invalid_request on file. |
Upload formatsfile | JPEG, PNG, WebP | – | Enforced | Read from the file itself, not its name. Anything else: 400 invalid_request on file. |
| Output | ||||
| Output pixels | 1K16:91344×7689:16768×13441:11024×10242K16:92048×11529:161152×20481:12048×2048 | px | Enforced | We ask for exactly these sizes. |
| Output side | 512 to 2,048 | px per side | Supplier | Every size above fits. The supplier refuses larger ones. |
| Output format | JPEG | – | Enforced | Every image is a JPEG. |
| Links | ||||
| Upload link | 7 | days | Enforced | After that the link answers 403. Upload the file again. |
| Output link | 24 | hours | Enforced | After that the link answers 403. GET the job again for a fresh link. The file itself is kept for 12 months (the job's outputs_expire_at gives the date), longer if the result is saved to Assets or used in a project. |
We never fetch images from other hosts, so for those links the supplier checks the size, and a job it refuses is not charged. The same limits come back as JSON: GET /v1/models returns a limits object for every model.
Pricingworked out in the open
One price per image, by model and size, in credits (US$1 = 100 credits). Generating and editing are priced separately.
| Model | 1K | 2K |
|---|---|---|
qwen-image-2512-lightningGenerate | 1 cr≈ $0.01≈ 1,000 images per $10 | 3 cr≈ $0.03≈ 333 images per $10 |
qwen-image-edit-2511-lightningEdit | 2 cr≈ $0.02≈ 500 images per $10 | 4 cr≈ $0.04≈ 250 images per $10 |
qwen-image-2512Generate | 2 cr≈ $0.02≈ 500 images per $10 | 6 cr≈ $0.06≈ 166 images per $10 |
qwen-image-edit-2511Edit | 3 cr≈ $0.03≈ 333 images per $10 | 10 cr≈ $0.10≈ 100 images per $10 |
Webhooksand idempotency
Retries never charge twice, and you do not have to poll.
- Idempotency
- Every generation POST needs an Idempotency-Key (1 to 255 visible ASCII characters). The same key within 24 hours returns the same job with HTTP 200, so a retry after a timeout never creates or charges a second job.
- Refused prompts
- A prompt refused by the pre-check (422 content_rejected) creates nothing, so the same key can be used again with a new prompt.
- Events
job.queuedjob.processingjob.succeededjob.failedjob.cancelled- Signature
- X-Signature carries t, the unix time, and v1, an HMAC-SHA256 of t, a dot and the raw body, keyed with your whsec_ secret. Refuse timestamps older than five minutes and dedupe on X-Event-Id.
- Delivery
- A 2xx within 10 seconds counts as delivered. Failed deliveries retry after 1 min, 5 min, 30 min, 2 h and 6 h. Redirects are not followed.
{
"id": "evt_01J9ZKB1QX",
"type": "job.succeeded",
"created_at": "2026-09-29T14:02:31Z",
"data": {
"job": {
"id": "job_01J9ZK4T8W",
"object": "job",
"kind": "image",
"status": "succeeded",
"money_status": "settled",
"requested_model": "qwen-image-2512",
"credits_charged": 2,
"outputs": [
{
"kind": "image",
"url": "https://…/job_01J9ZK4T8W.png?sig=…"
}
]
}
}
}import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received, before any JSON parsing.
export function verifyWebhook(header, rawBody, secret, toleranceSec = 300) {
let t = null;
const signatures = [];
for (const item of (header ?? "").split(",")) {
const [key, value] = item.split("=", 2);
if (key === "t") t = Number(value);
if (key === "v1") signatures.push(Buffer.from(value, "hex"));
}
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return signatures.some((s) => s.length === expected.length && timingSafeEqual(s, expected));
}
// verifyWebhook(req.headers["x-signature"], rawBody, process.env.WEBHOOK_SECRET)
// Answer 2xx quickly, and skip an X-Event-Id you have already handled.Errorsand what to do
Every error has the same shape: an error object with code, message, param and request_id. The ones you are most likely to meet:
| Code | HTTP | What to do |
|---|---|---|
invalid_request | 400 | Fix the field named in param. |
unauthorized | 401 | Check the key and the Bearer header. |
insufficient_credits | 402 | Top up, then retry with the same Idempotency-Key. |
forbidden_model | 403 | Add the model or tool to the key's allowlist in the studio, or use another key. Tools such as Enhance need their own entry. |
quote_expired | 409 | Get a new quote; quotes last 10 minutes. |
price_changed | 409 | The price rose above your cap. Quote again or raise max_credits. |
content_rejected | 422 | Nothing was charged. Change the prompt or the images. |
rate_limited | 429 | Wait for Retry-After, then retry. |
upstream_unavailable | 503 | The model is unavailable. Retry later; a job that never starts is not charged. |