Getting started
Pixora renders images with its own model on dedicated GPUs. A 1024×1280 image takes about 10 seconds once a machine is running, and up to 2 minutes when one has to start. The next section explains why that shapes how you write your integration.
npm install @vovix/pixoraimport { Pixora } from '@vovix/pixora'
const px = new Pixora({
// baseUrl defaults to https://api.pixora.vovix.io/v1
apiKey: process.env.PIXORA_KEY,
})
const { styles } = await px.listStyles()You do not need the SDK. The API is plain HTTP and JSON — see Reference.
Authentication
Two paths. Use exactly one.
| Kind | Header | Use when |
|---|---|---|
apiKey | x-api-key | Machine to machine: your backend, a queue, a cron job. Carries its own quota and throttle. |
idToken | authorization (raw token, no Bearer) | A signed-in person. Only used by the Pixora web app itself, under the /app path. |
Generating images
Every image belongs to a style. A style carries the shared art direction, which is what makes ten images look like one photographer took them.
Send prompt as one string, or fill the nine recipe slots and pass them asrecipe alongside the composed prompt. Slots are stored with the image so the Studio can show and edit them one by one. Keys: shot, subject, wear, scene, light, palette, story, medium, lens.
const { images } = await px.createImages({
styleId: 'sty_...',
prompt: 'Medium shot of a woman in her late twenties, ivory silk slip dress, rooftop at golden hour…',
width: 1024,
height: 1280,
steps: 8,
count: 4,
})
// images[i].status === 'generating' — the picture does NOT exist yetThis call returns immediately. Each image gets its database row before the job reaches the queue, so a dropped connection or a closed tab never loses track of an image — ask again by id and it is there.
Five engines
Pick the model with engine. The free one is open to every account the moment it signs up;anima exists in the API but is not open to any account yet — see the note below the table.
engine | Runs on | Output | Speed | Who |
|---|---|---|---|---|
z-image-turbo (default) | GPU | up to 1.5 MP, 8 steps | ~22 s per image once the GPU is up, ~2.5 min cold | approved accounts, scheduled hours, daily limit |
sdxl-turbo | CPU (on demand) | 512 px class, 1–4 steps | a few seconds per image; ~1 min extra when the machine is asleep | every account, no schedule, its own daily limit |
realvis-xl | GPU (on demand) | 1024 px class, 4–8 steps | under 10 s per image; ~2.5 min for the first one when the machine is asleep | every account, no schedule, its own daily limit |
animagine-xl | GPU (on demand) | 1024 px class, 20–32 steps | about 20 s per image; ~3 min for the first one when the machine is asleep | every account, no schedule, its own daily limit |
anima | GPU (on demand) | 1024 px class, 4–50 steps | about 13 s per image once running | not open yet — every request returns 503 CLOSED |
anima is a flat 2D / non-anime cartoon model, licensed for us to run and sell the images we generate — not for a paid platform to run on a caller's behalf. So the engine is wired into the API for internal use, but stays closed to every account (not just unapproved ones) until that changes. See the model's catalog page for what it draws well.await px.createImages({ styleId, prompt, engine: 'sdxl-turbo', steps: 2 })
await px.gpu('sdxl-turbo') // same shape as px.gpu()With sdxl-turbo, realvis-xl, animagine-xl and anima, width/height only choose the aspect; the image is rendered in that model's own class and the real size comes back on the image (1024×1280 becomes 448×576 on sdxl-turbo, 896×1152 on the three 1024 px engines). Images made before 2026-09-06 carry engine: "pixora-1", the old name of z-image-turbo.
Why you wait, and how to wait correctly
GPUs shut down when idle so they do not burn money. That means the first call after a quiet period has to boot a machine and load the model. Measured:
| Situation | Time to first image |
|---|---|
z-image-turbo, GPU already running | ~22 seconds |
z-image-turbo, machine up, model not loaded | ~50 seconds |
z-image-turbo, machine off | ~150 seconds |
sdxl-turbo, machine running | a few seconds per image |
sdxl-turbo, machine asleep | about a minute for the first image (it keeps the model on disk, so no re-download) |
realvis-xl, machine running | under 10 seconds per image at 5 steps |
realvis-xl, machine asleep | about 2.5 minutes for the first image |
animagine-xl, machine running | about 20 seconds per image at 28 steps |
animagine-xl, machine asleep | about 3 minutes for the first image |
anima, machine running | about 13 seconds per image at 10 steps — internal use only, see above |
const done = await px.waitFor(images.map((i) => i.assetId), {
onProgress: (n, total) => console.log(`${n}/${total}`),
})waitFor already does it this way.To tell your users which row of that table they are in before they click, ask the worker. The same state drives the coloured dot in Studio.
const gpu = await px.gpu()
// gpu.state: 'ready' | 'starting' | 'off' | 'error'
// gpu.reason: a sentence you can show as-is
// gpu.queue.waiting: images ahead of yourserror means AWS could not start a GPU — usually no capacity in the region. Your images stay queued and the worker keeps retrying, but do not promise a time. Poll gpu() at most every few seconds; use waitFor for the images themselves.There are no webhooks and no server-sent events. The reason is cost: holding a connection open for the whole wait runs about twenty times more expensive than polling every three seconds, and it still dies when someone reloads the page.
Styles
A style's promptPreset is appended to the end of every prompt. The end, not the beginning: Pixora reads the opening as the subject and the rest as technical direction, so leading with art direction drowns out what you actually asked for.
await px.createStyle({
title: 'Harbour town, 2D',
promptPreset: '2D animated series background art, flat cel shading, clean bold line art',
defaultWidth: 1472,
defaultHeight: 832,
})A style can only be deleted once it is empty. Cascading the delete would drop the rows easily and leave the image files behind — garbage nobody knows to collect.
Free images without the GPU
Not every image needs to be generated. Search Wikimedia Commons for licensed photos and illustrations and import the picks straight into a style. This runs on the API side and finishes in one call, so it does not depend on GPU hours. Imports count toward the account's daily image limit like generated images do.
Two sources, chosen with provider:
provider | What you get | License |
|---|---|---|
licensed (default) | Wikimedia Commons photos and illustrations, up to 1280px | Stated on every result (CC BY, CC0, public domain…) — safe to use with credit |
web | Image search across the web, original resolution | Not stated — check the source page before commercial use |
const { providers } = await px.sources() // ['licensed', 'web']
const licensed = await px.search('old town street at dusk', { limit: 12 }) // provider defaults to 'licensed'
const web = await px.search('old town street at dusk', { provider: 'web', limit: 12 })
const { images, failed } = await px.importImages({
styleId,
kind: 'background',
items: licensed.results.slice(0, 3), // a search result is already a valid import item
})curl "https://api.pixora.vovix.io/v1/search?q=mountain+lake&provider=web&limit=12" \
-H "x-api-key: $PIXORA_API_KEY"Imported images carry engine: "import" and a source block (provider, page URL, author, license). Show the credit where the license asks for it — the Studio does. Up to 8 images per call, 15 MB each, JPEG/PNG/WebP. The first web search after a quiet period can take ~10 s while the search backend warms up; the API answers504 SOURCE_WARMING with Retry-After if it cannot make it in time.
Limits and quotas
Three gates protect the GPUs, and every rejection carries a machine-readable code. They apply to z-image-turbo; the open engines (sdxl-turbo, realvis-xl, animagine-xl) skip approval and the schedule and only has its own daily limit (and a 503 CLOSED while an admin pauses it).anima also skips approval and the schedule, but returns 503 CLOSED for everyone right now regardless of quota — see the licensing note above.
| Status | code | Meaning |
|---|---|---|
| 403 | NOT_APPROVED | New accounts can browse and build styles, but an admin approves them before they generate. |
| 503 | CLOSED | Outside the scheduled GPU hours. opensAt and Retry-After say when to come back. Nothing is queued. |
| 429 | QUOTA | Daily image limit for the account. used, limit, resetsAt. |
GET /v1/gpu returns schedule and account so you can show the state before your user clicks; add ?engine=sdxl-turbo for the free tier's own limit and counter.
| Item | Value | Why |
|---|---|---|
| Image size | multiple of 16, each side 512–1536, ≤ 1.5 MP | GPU memory ceiling |
steps | z-image-turbo 1–20, best at 8–12 · sdxl-turbo 1–4 · realvis-xl 4–8, best at 5 · animagine-xl 20–32, best at 28 · anima 4–50, 10 on the distilled checkpoint | The first three are distilled for their range, so more steps only buy compute; animagine-xl is not distilled, so fewer steps leave visible noise; anima's range is wide because the checkpoint loaded on the machine changes what "best" means |
count | 8 per call | So one caller cannot take over the queue |
| Rate | 5 requests/second, burst 10 | Per API key |
| Quota | 500 images per day | Conservative start, raised against real usage |
Over quota returns 429 with a Retry-After header. The SDK surfaces that as err.retryAfter in seconds so you are not retrying blind.
Errors
| Code | Meaning |
|---|---|
400 | Bad parameter — the message names the field |
401 | Missing or wrong credentials |
404 | Not found, or not yours |
409 | Conflict: deleting a style with images, or an image a video still uses |
429 | Over quota, see Retry-After |
403 | Missing API key on the public path, or a key trying to manage keys |
404, never 403.Reference
| Route | Does |
|---|---|
GET /v1/health | Liveness, no auth required |
GET /v1/gpu | Worker state (off, starting, ready, error) and queue depth; ?engine=sdxl-turbo for the free engine |
GET /v1/styles | Your styles, with image counts per role |
POST /v1/styles | Create a style |
GET /v1/styles/{id} | One style |
PATCH /v1/styles/{id} | Edit, set cover, archive |
DELETE /v1/styles/{id} | Delete, only when empty |
GET /v1/images?styleId= | Images in a style, filter by role, cursor paging |
POST /v1/images | Generate images (engine: z-image-turbo default, sdxl-turbo, realvis-xl, animagine-xl, or anima — the last returns 503 for every account right now) |
GET /v1/sources | Image sources currently enabled (licensed, web) |
GET /v1/search?q=&provider= | Search licensed images (Wikimedia Commons) or the web — no GPU |
POST /v1/images/import | Import up to 8 URLs into a style, ready immediately |
GET /v1/images/{id} | One image and its status |
DELETE /v1/images | Delete several images |
| Route | Does |
|---|---|
PATCH /v1/images/{id} | Name, role, notes, tags |
Key management (/v1/keys) is deliberately not available to API keys — only to a signed-in session. Otherwise a leaked key could mint new keys and revoking the original would mean nothing.