API

Everything the studio does, it does through these routes. There is no second, private API.

Last updated 2026-08-19

Keys are not open yet. An API key needs a Creator or Studio plan, and card payment is not live on this deployment. Create an account to be ready, or write to support@pixelforge.app for early access.

Authentication

Create a key on the account page. A key starts with pf_ and is shown once. Send it on every request except GET /fleet, GET /limits and GET /skills.

Authorization: Bearer pf_xxxxxxxxxxxxxxxxxxxx

A key inherits its plan's rate limit: 120 requests a minute on Creator, 600 on Studio. Over the limit the API answers 429. Every response body is JSON, and every refusal is {"error": "..."}, so one parse handles them all.

Submit a run

POST /jobs takes the prompt and the count and answers 202 with a job id. Credits are reserved for the whole run at submit; images that never render are refunded when the run finishes.

POST /jobs
{
  "prompt": "a busy harbour at first light",
  "n": 4,
  "width": 512,
  "height": 512,
  "steps": 4,
  "seed_policy": { "type": "random" }
}

202
{ "job_id": "c47e2316-...", "n": 4, "status": "queued", "inert_knobs": [] }
FieldDefaultNotes
promptrequiredup to max_prompt_chars
n11 to max_variations. Each image gets its own seed.
width, height512multiples of 64, product under max_pixel_area
steps81 to max_steps. Fewer is faster and rougher.
skills["base-xl"]one or more ids from GET /skills
mode"generate""edit" needs at least one reference
seed_policy{"type":"random"}random, sequential or fixed; the last two take base_seed
ref_upload_idsnoneup to 4 ids from POST /uploads
reference_mode"reference""init" treats the reference as a starting latent
prompt_variantsnoneone shot description per image, rendered in FRONT of the prompt
tile_gridnone2 builds a print-size file from four tiles
lorasnone[{"id": "...", "multiplier": 1.0}] from GET /loras
advancednonerenderer parameters. An unknown key is refused by name with 422.

Unknown top-level fields are refused by name. The settings live at the top level, not inside a params object.

Varying the shot

The seed does not move the composition on this model. Measured on "a brass diving helmet on a workbench": six seeds at 4 steps, six at 8 steps, and distilled_guidance at 1.0, 2.0 and 3.5 all returned the same helmet at the same angle with the background rearranged. One fixed seed with six different shot descriptions returned six different pictures.

{
  "prompt": "a brass diving helmet on a workbench",
  "n": 4,
  "prompt_variants": [
    "",
    "Wide shot, the whole scene visible.",
    "Extreme close-up, shallow depth of field.",
    "Aerial view, seen from high above."
  ]
}

Up to 24 entries, each under 200 characters. Each entry goes in FRONT of the prompt, not after it: appended to a strongly described scene it was ignored. An empty entry renders the prompt as typed. GET /jobs/{id} returns the entry each image used as prompt_variant.

Keeping something out

negative_prompt is accepted, and on this model it does close to nothing at the guidance that keeps colour intact. Measured seed-matched over three seeds at txt_cfg 1.0, 1.4, 2.0 and 2.5: the named object survived at 1.0 and 1.4, went at 2.0 with the colour over-saturated, and the picture was unusable at 2.5. Raising the guidance also runs a second pass per step, which costs about 1.8x the render time.

Name what you want in its place instead. Three shapes of the rule, each measured at a fixed seed and each at the ordinary one-pass cost:

  • An object. ", burning logs glowing in the open firebox below" replaced a blue gas flame on all three of seeds 1033906312, 621955972 and 1300003147.
  • A colour. "tools with blue-taped handles hanging on a pegboard" replaced the red handles on all four seeds of one run. The strongly saturated red area fell from a mean 2.04% of the frame to 1.12%.
  • An absence. Name what fills the space: "an empty stool, a bare wall", not "no person".

The studio sends no negative prompt for this reason.

The model does not render words

Letterforms come back malformed. Measured on "a red telephone box on a village street at dusk" over four seeds: every sign read TELEP\WONE or TELEPIONE. Naming the wanted thing does not help here, because the failure is in the shapes of the letters, not in the subject.

Do not ask for text on a sign, a label or a packet. Add the words yourself, in a layout tool, over the picture the service returns.

Poll a run

GET /jobs/{job_id}. Results arrive as they finish, so take the url of each variation that reaches completed rather than waiting for the whole run. Poll about once every two seconds.

{
  "job_id": "c47e2316-...",
  "status": "running",
  "n": 4, "completed": 2, "failed": 0,
  "params": { "width": 512, "height": 512, "steps": 4 },
  "variations": [
    { "idx": 0, "status": "completed", "seed": 1604101814, "kept": false,
      "url": "/images/c47e2316-.../0.png", "elapsed_s": 51.1 },
    { "idx": 1, "status": "running", "seed": 1604101815, "kept": false }
  ]
}

A run's status is queued, running, completed, partial, failed or cancelled. A run of n images takes roughly ceil(n / ready) waves; read ready from GET /fleet.

Take the pictures

GET /images/{job_id}/{idx}.png                  full size
GET /images/{job_id}/{idx}.thumb.webp          thumbnail
GET /images/{job_id}/{idx}.png?download=true   with a Content-Disposition
GET /jobs/{job_id}/archive                     every finished image, one zip
GET /jobs/{job_id}/archive?kept=true           only the ones marked kept

A job id that is not yours answers 404, the same as one that does not exist. A delivered PNG carries no parameters chunk, so the prompt does not travel with the file.

Print size

One render cannot pass about a megapixel on this hardware. Measured on a lane, past this API's own cap: 1024x1024 completes in about 51 s at 4 steps, while 1536x1536 and 2048x2048 both fail with generate_image returned no results. A larger file is assembled from tiles instead.

{
  "prompt": "a brass sextant on a chart table",
  "n": 4, "width": 1024, "height": 1024, "steps": 8,
  "ref_upload_ids": ["<the picture>"],
  "reference_mode": "init",
  "tile_grid": 2,
  "advanced": { "strength": 0.12 }
}

tile_grid must be 2, n must be 4, and the run needs exactly one reference in reference_mode: init. While the tiles render, GET /jobs/{id} reports four variations. Once the join is written it reports one, at /images/{id}/full.png, with params.width and params.height set to the real size of that file. The joined size is not a round doubling, because the tiles overlap by 64 source pixels, and it follows the source's shape.

SourceJoinedAt 300 dpi
1024x10241928x1928163 x 163 mm
1152x8962183x1697185 x 144 mm
896x11521673x2151142 x 182 mm
1280x7682438x1464206 x 124 mm

Send width and height equal to the source's own size. The tile boxes are cut from the full width AND height: they used to be cut from one size, so a 1152x896 landscape source was cut inside its own 896x896 left square and 22.2% of the width never reached the print file.

Other routes

RouteWhat it does
GET /limitsthe caps this deployment enforces; read them rather than hardcoding them
GET /skillsthe models available; is_edit says whether one needs a reference
GET /fleetlane counts; ready is the number free right now
POST /uploadsmultipart upload; returns an id for ref_upload_ids
POST /jobs/{id}/cancelstops a run; unrendered credits are refunded
DELETE /jobs/{id}stops the run, then removes it and its files
PUT /jobs/{id}/variations/{idx}/keepmarks a result kept, body {"kept": true}
GET /jobs?limit=50&offset=0run history, newest first, limit up to 100
GET /lorasadapters available to this account
POST /api-keys/createalso /api-keys/list and /api-keys/revoke

A complete example

KEY=pf_xxxxxxxxxxxxxxxxxxxx
HOST=https://<your-host>

JOB=$(curl -s -X POST "$HOST/jobs" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"prompt":"a busy harbour","n":4,"width":512,"height":512,"steps":4}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["job_id"])')

while :; do
  BODY=$(curl -s -H "Authorization: Bearer $KEY" "$HOST/jobs/$JOB")
  echo "$BODY" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["status"], d["completed"], "of", d["n"])'
  echo "$BODY" | grep -q '"status": *"\(completed\|partial\|failed\|cancelled\)"' && break
  sleep 2
done

curl -s -H "Authorization: Bearer $KEY" -o 0.png "$HOST/images/$JOB/0.png"

Errors

StatusWhen
401no key, or a revoked key
402not enough credits; the message names the balance and the request
403the account's plan does not include this feature
404not yours, or does not exist
422the request is wrong; the message names the field and what is allowed
429over the plan's rate limit, or over a per-account cap
501the route needs a paid plan that is not yet open

200 images free. Every month.

No credit card. Create an account, submit one prompt, and review your first batch of variations in minutes.

Start free