SpriteForge
Open the studio
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.
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/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.
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:
/api/jobs poll and refunded.Current per-size prices for each tier are in GET /api/config (tiers[label].avg, USD, indexed against tiers[label].sizes).
| Limit | Value | On breach |
|---|---|---|
| POST /api/generate | 20 requests / minute / account | 429 |
| /api/jobs (GET + POST combined) | 180 requests / minute / account | 429 |
| Concurrent generations | 3 per account | job is queued, not rejected |
| Platform-wide concurrency | shared global cap | job is queued, not rejected |
| Per-IP (pre-auth) | 60/min on generate & account, 600/min on jobs, 240/min on balance | 429 |
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.
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.
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.
One endpoint, nine generation types, selected by type. These fields are common to every request:
| Field | Type | Notes |
|---|---|---|
| jobId | string, required | Client-generated ID, 6–40 chars of [A-Za-z0-9_-]. Doubles as the idempotency key. |
| type | string, required | single · edit · set · animation · terrain · phased · wang · platformer · multiterrain |
| tierLabel | string, required | Quality/price tier: Low, Basic, Pro or Ultra (authoritative list: /api/config). |
| px | object, required* | { "w": int, "h": int }, each 1–64. Sets sprite size and the price. (*not used by edit, which sizes from baseArt.) |
| meta | object, optional | Opaque blob (≤4000 chars serialized) echoed back by /api/jobs - use it to route results in your own pipeline. |
| options | object, optional | Style controls, below. |
| references | array, optional | Reference 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). |
All free-text options are capped at 500 characters. All are optional.
| Option | Type | Notes |
|---|---|---|
| perspective | string | e.g. "side view", "3/4 top-down" |
| style | string | art-style line, e.g. "chunky 16-bit fantasy, warm palette" |
| light | string | light direction, e.g. "Top-left" |
| outline | string | e.g. "Dark-tinted", "Black", "None" |
| projectStyle / projectDesc | string | project-wide style/description context |
| symmetry | string | "Off" or a horizontal/vertical symmetry note - mirrored server-side after parsing |
| seedNote | string | extra free-text appended to the artist prompt |
| palette | string[] | up to 64 hex colours to steer toward |
| lockPalette | boolean | snap the output strictly onto options.palette |
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" } }
Modify an existing sprite. Instead of px, send the sprite itself:
| Field | Type | Notes |
|---|---|---|
| prompt | string, required | the change to make, ≤2000 chars |
| baseArt | object, required | the sprite being edited, in the result format (size 1–64, palette ≤52 hex, grid size×size of palette indexes / -1) |
| maskCells | [x,y][], optional | restrict 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).
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" } }
A looping animation of one subject.
| Field | Type | Notes |
|---|---|---|
| subject | string, required | ≤8000 chars |
| frameCount | int, required | 2–6 total frames |
| motion | string, optional | ≤1000 chars. Lines starting with a frame number direct that frame only ("3: raise the sword"); other lines describe the loop. |
| aggression | string, optional | subtle · normal (default) · strong - how hard poses are pushed |
| seedArt | object, optional | an 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".
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".
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").
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".
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.
Several terrains that blend into each other on a paintable dual-grid map.
| Field | Type | Notes |
|---|---|---|
| terrains | array, required | 2–6 of { id (≤64 chars), name (≤200), color? [r,g,b], role? (e.g. "river") } |
| cols / rows | int, required | map dimensions, each 4–32 |
| terrainMap | int[][], optional | rows×cols of terrain indexes - the pre-painted map |
| terrainRules | object, optional | edge-blend rules (≤4000 chars serialized) |
px must be square (the per-terrain tile size).
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.
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.
Your remaining credits: { "balance_cents": 1234 } (USD cents). Top-ups are done in the studio.
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.
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 are JSON: { "error": { "message": "…" } }.
| Status | Meaning |
|---|---|
| 400 | Invalid intent - the message names the offending field and its constraint. |
| 401 | Missing/invalid/deactivated API key, or the key was used on an endpoint keys can't access. |
| 402 | Insufficient credits for the reservation - top up in the studio. |
| 429 | Rate limit exceeded - back off and retry after a minute. |
| 5xx / 502 / 504 | Upstream/model failure - the reservation is refunded automatically; retry with a fresh jobId. |