Image generation

Submit a prompt, receive a job ID, poll until the images are ready.

This endpoint is asynchronous

Unlike chat, it does not return an image. It returns 202 Accepted with a job ID that you poll. A single image takes tens of seconds to a few minutes — far longer than any proxy will hold a connection open, which is exactly why the API is shaped this way.

Available models#

Pass one of these as model. There is no account default, so the field is required. Both are billed the same way — one image is one unit — and both return a CDN URL.

Image models
FieldTypeDescription
gpt-imagetext → imageOpenAI image generation. The only model that accepts a source image, so pick it for edits and for re-framing something you already generated.
sensenova/sensenova-u1.5-litetext → imageSenseNova U1.5 Lite. Free tier, generation only — it does not accept a source image, so it cannot be used for edits or re-framing.

Listing them from the API

GET /v1/models returns chat models by default so that IDE model pickers stay clean. Ask for these explicitly with GET /v1/models?modality=image — or ?modality=all to get every model in one call. Each entry carries a modality field. See List models.

Create a job#

POST/v1/images/generations
curl https://api.xkiro.com/v1/images/generations \
  -H "Authorization: Bearer $XKIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-image",
    "prompt": "A lighthouse on a rocky shore at dawn, long exposure",
    "n": 1,
    "size": "1024x1024"
  }'
202 Accepted
{
  "id": "f32a1796-15f4-43ba-8098-22d7dc2f66c1",
  "object": "image.generation.job",
  "status": "processing",
  "created": 1785734400,
  "model": "gpt-image",
  "prompt": "A lighthouse on a rocky shore at dawn, long exposure",
  "aspect_ratio": "1:1",
  "style": null
}
Request body
FieldTypeDescription
promptrequiredstringWhat to draw. Detail helps: subject, setting, lighting, style, framing.
modelrequiredstringImage model ID — there is no account default. See Available models.
nintegerCurrently must be 1. Each image counts as a separate billable unit.
sizestringPixel size such as 1024x1024 or 1792x1024. Converted to the nearest aspect ratio the model supports.
stylestringOptional style hint passed through to the model.
source_job_idstringRe-render an existing image at a different aspect ratio. Combine with size. See Change aspect ratio.

Poll for the result#

GET/v1/images/generations/{id}
curl https://api.xkiro.com/v1/images/generations/f32a1796-15f4-43ba-8098-22d7dc2f66c1 \
  -H "Authorization: Bearer $XKIRO_API_KEY"
200 OK — finished
{
  "id": "f32a1796-15f4-43ba-8098-22d7dc2f66c1",
  "object": "image.generation.job",
  "status": "succeeded",
  "created": 1785734400,
  "model": "gpt-image",
  "prompt": "A lighthouse on a rocky shore at dawn, long exposure",
  "aspect_ratio": "1:1",
  "data": [
    { "url": "https://cdn.xkiro.com/images/01JQZ8K3M7N2P4R6S8T0V2W4X6-0.png" }
  ]
}

Job status

FieldTypeDescription
processingstringStill running. Keep polling.
succeededstringDone. data[].url holds CDN links.
failedstringSomething went wrong. error explains what. Not billed.
blockedstringThe provider refused the prompt on content grounds. Retrying the same prompt will not help; reword it.
Polling with backoff
async function generateImage(prompt: string) {
  const created = await fetch("https://api.xkiro.com/v1/images/generations", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.XKIRO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ prompt, n: 1, size: "1024x1024" }),
  }).then((r) => r.json());

  // Give up eventually. A job that never leaves "processing" would otherwise
  // poll forever and hold the caller open.
  const deadline = Date.now() + 5 * 60_000;
  let waitMs = 2_000;

  while (Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, waitMs));
    waitMs = Math.min(waitMs * 1.5, 10_000);

    const job = await fetch(
      `https://api.xkiro.com/v1/images/generations/${created.id}`,
      { headers: { Authorization: `Bearer ${process.env.XKIRO_API_KEY}` } },
    ).then((r) => r.json());

    if (job.status === "succeeded") return job.data.map((d: { url: string }) => d.url);
    if (job.status === "failed" || job.status === "blocked") {
      throw new Error(job.error?.message ?? job.status);
    }
  }

  throw new Error("Image generation timed out");
}

Poll every few seconds, not every few hundred milliseconds

Nothing changes faster than that, and aggressive polling counts against your rate limit — it can get you throttled while you wait for your own image.

List your jobs#

GET/v1/images/generations

Returns recent jobs newest first, so a gallery survives a page reload without any client-side storage. Paginate with before, using next_before from the previous page.

200 OK
{
  "object": "list",
  "data": [ /* job objects, newest first */ ],
  "next_before": "2026-08-03T15:12:01.094Z"
}

Change aspect ratio#

Pass source_job_id together with a new size to re-render an existing image at a different shape. This creates a new job and costs one unit; the original is untouched.

curl https://api.xkiro.com/v1/images/generations \
  -H "Authorization: Bearer $XKIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source_job_id": "f32a1796-15f4-43ba-8098-22d7dc2f66c1",
    "size": "1792x1024"
  }'

Billing and limits#

  • One image is one billable unit, regardless of size. Each image costs one unit.
  • Every plan includes a number of free images per rolling 24 hours. Past that, images are charged to your wallet — they never consume your plan's spending window.
  • Failed and cancelled jobs do not count against your allowance. Jobs refused on content grounds do, because the attempt consumed upstream capacity.
  • There is a cap on how many jobs you can have in flight at once, so one account cannot occupy the whole queue. Submit the next batch as earlier jobs finish.

Writing prompts that work#

  • Name the subject, the setting, the lighting and the framing. "A lighthouse" is a coin flip; "a white lighthouse on wet black rocks at dawn, low mist, wide shot" is a photograph.
  • Describe what you want, not what you do not. Negations are unreliable across image models.
  • Keep the prompt with the image. The job object returns prompt, aspect_ratio and style so you can rebuild a gallery without your own database.

To edit an image you already have, see Image editing.

Was this page helpful?