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.
| Field | Type | Description |
|---|---|---|
gpt-image | text → image | OpenAI 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-lite | text → image | SenseNova 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#
/v1/images/generationscurl 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"
}'{
"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
}| Field | Type | Description |
|---|---|---|
promptrequired | string | What to draw. Detail helps: subject, setting, lighting, style, framing. |
modelrequired | string | Image model ID — there is no account default. See Available models. |
n | integer | Currently must be 1. Each image counts as a separate billable unit. |
size | string | Pixel size such as 1024x1024 or 1792x1024. Converted to the nearest aspect ratio the model supports. |
style | string | Optional style hint passed through to the model. |
source_job_id | string | Re-render an existing image at a different aspect ratio. Combine with size. See Change aspect ratio. |
Poll for the result#
/v1/images/generations/{id}curl https://api.xkiro.com/v1/images/generations/f32a1796-15f4-43ba-8098-22d7dc2f66c1 \
-H "Authorization: Bearer $XKIRO_API_KEY"{
"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
| Field | Type | Description |
|---|---|---|
processing | string | Still running. Keep polling. |
succeeded | string | Done. data[].url holds CDN links. |
failed | string | Something went wrong. error explains what. Not billed. |
blocked | string | The provider refused the prompt on content grounds. Retrying the same prompt will not help; reword it. |
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#
/v1/images/generationsReturns 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.
{
"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_ratioandstyleso you can rebuild a gallery without your own database.
To edit an image you already have, see Image editing.
