SpriteForge
Open the studio
API REFERENCE · v1 · 2026-07

The SpriteForge API

Everything the studio can forge - sprites, edits, sheets, animations, tilesets - is available over plain HTTPS with an API key. API calls are tied to your SpriteForge account: they spend the same credit balance and run under the same rate limits as generations you start in the studio.

Base URL: https://www.spriteforge.tech - all endpoints below are relative to it. Requests and responses are JSON.

Authentication & API keys

Sign in to the studio, click your account name in the header, and choose Generate API key. The key (sf_…) is shown once - only a hash is stored server-side, so copy it immediately. From the same panel you can deactivate/activate the key (temporarily disable it without losing it), regenerate it (replaces the old key immediately), or delete it. One key exists per account.

Send the key as a bearer token on every request:

Authorization: Bearer sf_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
API keys are deliberately scoped to generation endpoints only - /api/generate, /api/jobs and /api/balance. They cannot manage your account, start checkouts, or read your asset library; those require a signed-in studio session.

The key is a plain bearer token - use it from any device, server, CI job or script, several at once if you like. It isn't tied to a browser, session or IP. Jobs you submit with the key are ignored by the studio's auto-import, so an open studio tab won't race your script for the result (see claiming); everything else - credits, limits, the gallery - is shared account-wide.

Credits & pricing

API generations spend the same prepaid credits as the studio (top up from the studio header). Every generation has a fixed price determined server-side by the tier and sprite size - the client never sets a price. The flow is reserve-and-settle:

Current per-size prices for each tier are in GET /api/config (tiers[label].avg, USD, indexed against tiers[label].sizes).

Rate limits & concurrency

LimitValueOn breach
POST /api/generate20 requests / minute / account429
/api/jobs (GET + POST combined)180 requests / minute / account429
Concurrent generations3 per accountjob is queued, not rejected
Platform-wide concurrencyshared global capjob is queued, not rejected
Per-IP (pre-auth)60/min on generate & account, 600/min on jobs, 240/min on balance429

When capacity is full your job is accepted with status: "queued" (credits already reserved) and starts automatically when a slot frees. A job left queued for 24 h is failed and refunded. Web and API usage share the per-account limits - they are per account, not per key. Every 429 carries a Retry-After header (seconds); honour it in your retry loop.

How a generation works

Generation is asynchronous: you submit an intent, the server runs the LLM artist to completion (typically 20–120 s depending on tier/size), and you poll for the parsed result. Prompts, model IDs and raw model output never cross the wire.

# 1. submit - you invent the jobId (6-40 chars, [A-Za-z0-9_-])
curl -X POST https://www.spriteforge.tech/api/generate \
  -H "Authorization: Bearer $SF_KEY" -H "Content-Type: application/json" \
  -d '{
    "jobId": "job_a1b2c3d4",
    "type": "single",
    "tierLabel": "Basic",
    "subject": "a tiny copper golem with glowing blue eyes",
    "px": { "w": 32, "h": 32 }
  }'
# → { "ok": true, "jobId": "job_a1b2c3d4", "status": "running" }   (or "queued")

# 2. poll until your job's status is "done" or "failed" (1-2s interval is plenty)
curl -H "Authorization: Bearer $SF_KEY" https://www.spriteforge.tech/api/jobs
# → { "ok": true, "jobs": [ { "id": "job_a1b2c3d4", "status": "done",
#      "result": { "size": 32, "palette": ["#17161d", …], "grid": [[…]] }, … } ] }

# 3. (optional) claim it so other pollers won't re-import it
curl -X POST https://www.spriteforge.tech/api/jobs \
  -H "Authorization: Bearer $SF_KEY" -H "Content-Type: application/json" \
  -d '{ "id": "job_a1b2c3d4", "action": "claim" }'

Submitting the same jobId twice is safe - the duplicate is ignored (status: "duplicate") and not double-charged. The POST keeps the connection open until the generation finishes; you may also fire-and-forget it and rely purely on polling, since the server runs the job to completion either way.

Try it in your browser

A live console for this API - works on any device, phones included. Your key goes straight from this page to the API (same origin, kept in memory only, never stored or sent anywhere else). Don't paste your key into third-party API testers; many proxy your requests through their servers.

Runs a real 16×16 Low-tier generation charged from your credits.

POST/api/generate

One endpoint, nine generation types, selected by type. These fields are common to every request:

FieldTypeNotes
jobIdstring, requiredClient-generated ID, 6–40 chars of [A-Za-z0-9_-]. Doubles as the idempotency key.
typestring, requiredsingle · edit · set · animation · terrain · phased · wang · platformer · multiterrain
tierLabelstring, requiredQuality/price tier: Low, Basic, Pro or Ultra (authoritative list: /api/config).
pxobject, required*{ "w": int, "h": int }, each 1–64. Sets sprite size and the price. (*not used by edit, which sizes from baseArt.)
metaobject, optionalOpaque blob (≤4000 chars serialized) echoed back by /api/jobs - use it to route results in your own pipeline.
optionsobject, optionalStyle controls, below.
referencesarray, optionalReference sprites in the result format, each with an optional role: "style" (copy the look) or "subject" (copy the subject, not the look). Max count via /api/config maxRefs; small per-reference surcharge (tiers[label].refCents). Not supported on multi-call types (set, animation, terrain, wang, platformer, multiterrain).

options - shared style controls

All free-text options are capped at 500 characters. All are optional.

OptionTypeNotes
perspectivestringe.g. "side view", "3/4 top-down"
stylestringart-style line, e.g. "chunky 16-bit fantasy, warm palette"
lightstringlight direction, e.g. "Top-left"
outlinestringe.g. "Dark-tinted", "Black", "None"
projectStyle / projectDescstringproject-wide style/description context
symmetrystring"Off" or a horizontal/vertical symmetry note - mirrored server-side after parsing
seedNotestringextra free-text appended to the artist prompt
palettestring[]up to 64 hex colours to steer toward
lockPalettebooleansnap the output strictly onto options.palette

type: "single" 1 call

One sprite. Add subject (string, required, ≤8000 chars).

{ "jobId": "…", "type": "single", "tierLabel": "Pro",
  "subject": "a rusty mining pickaxe, worn wooden handle",
  "px": { "w": 24, "h": 24 },
  "options": { "style": "warm 16-bit fantasy", "outline": "Dark-tinted" } }

type: "edit" 1 call

Modify an existing sprite. Instead of px, send the sprite itself:

FieldTypeNotes
promptstring, requiredthe change to make, ≤2000 chars
baseArtobject, requiredthe sprite being edited, in the result format (size 1–64, palette ≤52 hex, grid size×size of palette indexes / -1)
maskCells[x,y][], optionalrestrict the edit to these cells (0-based, within the grid). Untouched cells are preserved.

Per-tier size limits apply: a full-sprite edit is limited to editLimits[tier].maxFullSize per side, a masked edit to editLimits[tier].maxCells selected cells (both in /api/config).

type: "set" 1 call per subject

A themed sprite set with one unified palette. Replace subject with subjects (1–6 strings, ≤300 chars each); optional options.setDesc describes the set's shared theme. Result: frames = one frame per subject, frameMode: "set". Priced per subject; if a subject fails, unrun subjects are refunded.

{ "jobId": "…", "type": "set", "tierLabel": "Basic",
  "subjects": ["iron sword", "battle axe", "wooden shield", "short bow"],
  "px": { "w": 24, "h": 24 },
  "options": { "setDesc": "medieval armory icons on transparent background" } }

type: "animation" 1 call per generated frame

A looping animation of one subject.

FieldTypeNotes
subjectstring, required≤8000 chars
frameCountint, required2–6 total frames
motionstring, optional≤1000 chars. Lines starting with a frame number direct that frame only ("3: raise the sword"); other lines describe the loop.
aggressionstring, optionalsubtle · normal (default) · strong - how hard poses are pushed
seedArtobject, optionalan existing sprite (result format) used as frame 1; its size must equal max(px.w, px.h). You're only charged for the remaining frames.

Result: frames in loop order, frameMode: "anim".

type: "terrain" 3 calls

A wall/floor/transition tile trio for top-down maps. subject = the wall material (required); subjectB = the floor material (optional, defaults to subject). Result: 3 frames - wall, wall→floor transition, floor - frameMode: "terrain".

type: "phased" 2–3 calls

Premium staged pipeline (silhouette → colour → detail) with a render check between passes - the strongest choice for humanoids and hard anatomy. Add subject (required) and optionally passes (2 or 3, default 3) and detailTier (a second tier label used for the passes after the silhouette - e.g. main tier "Ultra", detailTier: "Basic").

type: "wang" 1 call

A 16-tile Wang autotile terrain set derived deterministically from one seamless fill. subject = the terrain material (required). px must be square. Result: 16 frames in Wang mask order, frameMode: "wang".

type: "platformer" 2 calls

Side-view platformer ground tiles: an earth fill plus a top cap, expanded into directional edge and slope tiles. subject = the earth material (required); subjectB = the surface cap (optional, e.g. "lush grass"). px must be square.

type: "multiterrain" 1 call per terrain

Several terrains that blend into each other on a paintable dual-grid map.

FieldTypeNotes
terrainsarray, required2–6 of { id (≤64 chars), name (≤200), color? [r,g,b], role? (e.g. "river") }
cols / rowsint, requiredmap dimensions, each 4–32
terrainMapint[][], optionalrows×cols of terrain indexes - the pre-painted map
terrainRulesobject, optionaledge-blend rules (≤4000 chars serialized)

px must be square (the per-terrain tile size).

GET/api/jobs

Your jobs from the last 48 hours (newest first, max 30). Polling this endpoint also self-heals: stale jobs are failed and refunded, and queued jobs are nudged toward free capacity.

{ "ok": true, "jobs": [ {
    "id": "job_a1b2c3d4",
    "status": "queued" | "running" | "done" | "failed" | "claimed",
    "meta": { …your meta blob… },
    "progress": "2/5" | "queued - starts automatically" | null,
    "partial": { "index": 1, "total": 5, "current": {…}, "done": [{…}] },  // running only: live-paint state
    "result": { …result format… },   // done only
    "error": "…",                    // failed only
    "created_at": "2026-07-17T12:00:00.000Z"
} ] }

partial streams the in-progress grid while the artist draws (throttled to ~0.5 s), so you can render live progress exactly like the studio does.

POST/api/jobs

Body { "id": "<jobId>", "action": "claim" } - marks a done job as consumed and drops its stored result, so other tabs/devices/pollers of the same account won't import it twice. Optional for single-consumer API pipelines, but recommended once you've stored the result. Response: { "ok": true, "claimed": true|false }.

Jobs submitted with an API key are tagged server-side and skipped by the studio's background auto-import - your open studio tabs will never claim them out from under your script, and the result stays available to you until you claim it (results are also dropped when the 48-hour job window expires). Jobs started in the studio behave as before: any of your studio tabs may import and claim them, so treat those results as studio-owned.

GET/api/balance

Your remaining credits: { "balance_cents": 1234 } (USD cents). Top-ups are done in the studio.

GET/api/config

Public discovery endpoint (no auth): current tier labels with per-size average prices (tiers), edit-size limits per tier (editLimits), the reference-image cap (maxRefs) and per-reference surcharge, and the max sprite size (maxSpriteSize). Read tier labels and prices from here rather than hardcoding them.

The result format

Every finished job's result (and every sprite you send - baseArt, seedArt, references) uses the same shape:

{
  "size": 32,                       // grid side length
  "palette": ["#17161d", "#dcb684", …],  // ≤64 hex colours
  "grid": [[ -1, 0, 1, … ]],        // size rows × size cols; palette index, -1 = transparent
  "frames": [ { "grid": [[…]] }, … ],    // multi-frame types: one grid per frame, shared palette
  "frameMode": "set" | "anim" | "terrain" | "wang" | …   // multi-frame types only
}

Rendering to PNG is a straight pixel loop: cell value -1 → transparent, otherwise palette[value].

Errors

Errors are JSON: { "error": { "message": "…" } }.

StatusMeaning
400Invalid intent - the message names the offending field and its constraint.
401Missing/invalid/deactivated API key, or the key was used on an endpoint keys can't access.
402Insufficient credits for the reservation - top up in the studio.
429Rate limit exceeded - back off and retry after a minute.
5xx / 502 / 504Upstream/model failure - the reservation is refunded automatically; retry with a fresh jobId.
Treat your API key like a password: it can spend your credits. If it leaks, Regenerate (or Deactivate) it immediately from the account panel in the studio.