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/pixora
import { 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.

KindHeaderUse when
apiKeyx-api-keyMachine to machine: your backend, a queue, a cron job. Carries its own quota and throttle.
idTokenauthorization (raw token, no Bearer)A signed-in person. Only used by the Pixora web app itself, under the /app path.
For your own integration, use an API key. Create one under API keysin the studio — the value is shown once. Keys live at the API root; the Cognito path is an internal detail of the web app.

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 yet

This 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.

engineRuns onOutputSpeedWho
z-image-turbo (default)GPUup to 1.5 MP, 8 steps~22 s per image once the GPU is up, ~2.5 min coldapproved accounts, scheduled hours, daily limit
sdxl-turboCPU (on demand)512 px class, 1–4 stepsa few seconds per image; ~1 min extra when the machine is asleepevery account, no schedule, its own daily limit
realvis-xlGPU (on demand)1024 px class, 4–8 stepsunder 10 s per image; ~2.5 min for the first one when the machine is asleepevery account, no schedule, its own daily limit
animagine-xlGPU (on demand)1024 px class, 20–32 stepsabout 20 s per image; ~3 min for the first one when the machine is asleepevery account, no schedule, its own daily limit
animaGPU (on demand)1024 px class, 4–50 stepsabout 13 s per image once runningnot 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:

SituationTime 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 runninga few seconds per image
sdxl-turbo, machine asleepabout a minute for the first image (it keeps the model on disk, so no re-download)
realvis-xl, machine runningunder 10 seconds per image at 5 steps
realvis-xl, machine asleepabout 2.5 minutes for the first image
animagine-xl, machine runningabout 20 seconds per image at 28 steps
animagine-xl, machine asleepabout 3 minutes for the first image
anima, machine runningabout 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}`),
})
Do not set a hard deadline from when you called. Measure from the last time something progressed. A batch of eight still takes about 100 seconds after the first image lands, so a flat five-minute timeout fails exactly when the system is working normally. 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 yours
error 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:

providerWhat you getLicense
licensed (default)Wikimedia Commons photos and illustrations, up to 1280pxStated on every result (CC BY, CC0, public domain…) — safe to use with credit
webImage search across the web, original resolutionNot 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.

StatuscodeMeaning
403NOT_APPROVEDNew accounts can browse and build styles, but an admin approves them before they generate.
503CLOSEDOutside the scheduled GPU hours. opensAt and Retry-After say when to come back. Nothing is queued.
429QUOTADaily 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.

ItemValueWhy
Image sizemultiple of 16, each side 512–1536, ≤ 1.5 MPGPU memory ceiling
stepsz-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 checkpointThe 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
count8 per callSo one caller cannot take over the queue
Rate5 requests/second, burst 10Per API key
Quota500 images per dayConservative 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

CodeMeaning
400Bad parameter — the message names the field
401Missing or wrong credentials
404Not found, or not yours
409Conflict: deleting a style with images, or an image a video still uses
429Over quota, see Retry-After
403Missing API key on the public path, or a key trying to manage keys
We do not reveal whether someone else's style exists: asking for something that is not yours returns 404, never 403.

Reference

RouteDoes
GET /v1/healthLiveness, no auth required
GET /v1/gpuWorker state (off, starting, ready, error) and queue depth; ?engine=sdxl-turbo for the free engine
GET /v1/stylesYour styles, with image counts per role
POST /v1/stylesCreate 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/imagesGenerate 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/sourcesImage sources currently enabled (licensed, web)
GET /v1/search?q=&provider=Search licensed images (Wikimedia Commons) or the web — no GPU
POST /v1/images/importImport up to 8 URLs into a style, ready immediately
GET /v1/images/{id}One image and its status
DELETE /v1/imagesDelete several images
RouteDoes
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.