Overview
The SpriteLab API turns text and images into clean, game-ready pixel art over REST. Every result runs through the same cleanup pipeline as the app: background removal, a limited palette, and a rebuilt 1px outline, so what you get back drops straight into an engine.
Base URL
BASE https://spritelab.dev/api/v1
JSON in, images out. Most endpoints return a JSON envelope with base64 images; /generate and /resize return raw PNG bytes (frozen for the Aseprite plugin). Everything is one round trip except animation, which is async.
Authentication
Every request needs a Bearer key in the Authorization header. Keys look like sl_live_… and are created in the app under Account → API keys. The plaintext is shown once at creation and never again, so store it somewhere safe.
Authorization: Bearer sl_live_YOUR_KEY
Rate limit: 60 requests per minute per key. Every response carries X-RateLimit-Limit and X-RateLimit-Reset; going over returns 429.
Billing & credits
API calls cost the same credits as the web app. They're paid from purchased (top-up) credits, which never expire and are bought in the app under Billing. On Knight and up the API can also spend your monthly plan credits, at a small markup that shrinks as your plan grows:
| Plan | Plan credits over the API | A 20-credit animation costs |
|---|---|---|
| Squire, Ranger | Not used (purchased credits only) | 20 purchased |
| Knight | +50% per call | 30 plan credits |
| Paladin | +25% per call | 25 plan credits |
| Dragonlord | No markup | 20 plan credits |
Plan credits are spent first (they reset monthly); when they can't cover a call, it's paid from purchased credits at face value. Refunds go back to whichever balance paid.
Every mode is available over the API, including the ones that are subscriber-only in the web app (Micro, Reskin, Backgrounds, Tileset, Merge, packs above 3x3). Credits are the only requirement, no plan needed.
| Call | Cost |
|---|---|
| POST /generate | 1 CR epic / 6 CR mythic |
| POST /pack | 2 CR epic / 9 CR mythic — whole grid, any size |
| POST /convert | 3 CR |
| POST /micro | 3 CR epic / 10 CR mythic |
| POST /reskin | 2 CR epic / 6 CR mythic — per variant |
| POST /api/v1/edit | 2 CR epic / 6 CR mythic per variant |
| POST /merge | 2 CR epic / 6 CR mythic — per variant |
| POST /backgrounds | 4 CR epic / 15 CR mythic |
| POST /tileset | 10 CR epic / 30 CR mythic |
| POST /animate | 20 CR Standard / 30 CR Pro |
| POST /ui-kit | 3 CR concept (skipped with a mockup) + 8 CR per sheet; Mythic 8 + 15. Early access |
| POST /resize, /enhance-prompt, GET /credits, /jobs/{id}, /ui-kit/{id} | Free |
Failed generations refund automatically, including individual failed reskin variants and failed animation jobs. Every generation response carries your remaining balance (the credits_remaining field, or the X-SpriteLab-Credits-Remaining header on PNG responses), so you rarely need to poll /credits.
Optional auto top-up: flip it on in the app (Billing → API keys). When an API call leaves your top-up balance under 200 credits, we charge the card on file a fixed amount you choose and add the credits, so batch scripts never die on an empty bucket. At most one charge per 10 minutes, off by default.
Conventions
Two response shapes. /generate and /resize return raw image/png bytes with metadata in headers — pipe them straight to a file. Everything newer returns a JSON envelope with base64 images:
{ "id": "…", "mode": "…", "cost": 1, "credits_remaining": 419, "image_b64": "…" }
Image inputs (image_b64, sketch_b64) accept plain base64 or a full data URL. PNG, JPG or WebP, max 10MB decoded, max 4096px per axis.
Sizing. Send height only and the sprite comes out exactly that tall, width following its natural proportions. Sending width plus a matching height forces the legacy square canvas, which stretches non-square subjects. Micro is exact NxN by design. size_mode: "native" returns a sprite at the model's own pixel size, the crispest result; the pixel size headers tell you that size so you can ask for whole multiples of it.
Quality is a two-value knob everywhere it appears: "epic" (fast, cheap, the default) or "mythic" (finer detail, higher cost).
Errors
Errors are JSON: {"error": "human message", "code": "machine_code"}. Codes are stable — match on them, not the message.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_json / bad_prompt / bad_image / bad_size / bad_quality / bad_size_mode / bad_cleanup | Malformed body or a field out of range. The message says which |
| 400 | size_mode_needs_height | size_mode native or crisp sent with the legacy width+height. Send height only |
| 401 | auth_missing / auth_invalid | No Bearer header, or the key is wrong or revoked |
| 402 | insufficient_purchased_credits | Not enough API-spendable credits (purchased, plus plan credits on Knight and up). Top up or upgrade in the app under Billing |
| 403 | blocked | Account suspended |
| 404 | not_found / job_not_found | Doesn't exist or isn't yours (deliberately the same response) |
| 409 | not_resizable | /resize: this row type can't go back through the sprite pipeline |
| 429 | rate_limited | Over 60 requests per minute on this key |
| 5xx | upstream / pipeline | Generation failed on our side. Any credits charged are refunded |
Async jobs
Animation takes 60 to 120 seconds, so POST /animate returns 202 with a job_id instead of blocking. Poll GET /jobs/{job_id} every few seconds until status is succeeded or failed. A failed job refunds automatically and says "refunded": true. Every other endpoint is synchronous.
Endpoints
One cleaned pixel art sprite from a text prompt. Returns raw PNG bytes with the sprite id and remaining balance in headers. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| prompt | string | required | What to draw. |
| quality | string | epic (default, 1 CR) or mythic (6 CR). | |
| height | int | 16–512, default 128. Sprite is exactly this tall, width follows natural proportions. | |
| width | int | Legacy: width + matching height forces an exact square (stretches). Skip for characters. | |
| size_mode | string | exact (default), native (the model's own pixel size, crispest, height ignored) or crisp (that size times the whole number closest to height). More. | |
| cleanup | string | advanced (default, keeps the model's own pixels so 1px details survive) or classic (the previous cleanup). | |
| direction | string | auto, front, left, right, back, top, angled. Default right. Ignored when style_reference_id is set. | |
| detail | int | 1–3 shading richness (1 flat, 3 rich). Default 1. | |
| isometric | bool | Isometric prop. Drawn over a 2:1 iso blockout so the angle is exact, sized to a real-world height on the same grid as isometric tilesets. Placement comes back in X-SpriteLab-Pivot-X/Y (centre of the floor base, put it on the tile centre). Size and tuning are ignored; not resizable. | |
| iso_tile | int | Isometric only. 64 (default, 64×32 tiles) or 32. | |
| iso_footprint | string | Isometric only. auto (default, guessed from the prompt), 1x1, 2x1, 1x2, 2x2, 3x3. | |
| iso_height | string | Isometric only. auto, short, medium (a person), tall, very_tall, or metres. | |
| style_notes | string | Free-text style direction appended to the prompt, e.g. "muted earthy palette, thick outline". | |
| style_reference_id | string | An earlier sprite id. New sprite matches its palette, proportions and outline — a whole cast in one style. | |
| max_colours, outline_thickness, outline_colour, palette_lock, dithering, contrast, saturation, brightness | Optional tuning. contrast, saturation, brightness are 0–3 (1.0 = unchanged); max_colours 2–64; outline_thickness 0–4 (0 = none). |
Request
curl -X POST https://spritelab.dev/api/v1/generate \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "a knight in blue plate armor", "quality": "epic", "height": 64}' \
-o knight.pngimport requests
r = requests.post(
"https://spritelab.dev/api/v1/generate",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"prompt": "a knight in blue plate armor", "quality": "epic", "height": 64},
)
open("knight.png", "wb").write(r.content)
print("credits left:", r.headers["X-SpriteLab-Credits-Remaining"])import { writeFileSync } from "fs";
const res = await fetch("https://spritelab.dev/api/v1/generate", {
method: "POST",
headers: {
"Authorization": "Bearer sl_live_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ prompt: "a knight in blue plate armor", quality: "epic", height: 64 }),
});
writeFileSync("knight.png", Buffer.from(await res.arrayBuffer()));Response
200 OK
Content-Type: image/png
X-SpriteLab-Sprite-Id: 3f9a…c1
X-SpriteLab-Credits-Remaining: 419
<binary PNG bytes>
One character facing front, right, back and left in a single call — each facing as its own frame plus a stitched spritesheet and an animated GIF. Ideal for top-down / RPG movement. Synchronous (typically 60–130s).
| Field | Type | Description | |
|---|---|---|---|
| prompt | string | required* | The character or mob. *Optional when you send sprite_id or image_b64. |
| sprite_id | string | Turn around one of your existing sprites instead of describing a new one. Any side works; every direction keeps that sprite's own colours. Not with isometric. | |
| image_b64 | string | Or send any sprite image (PNG/JPG, base64) to turn around, same rules as sprite_id. | |
| facing | string | With sprite_id / image_b64: which way the upload faces, auto (default, detected), front, back, left or right. Set it if a result comes out facing the wrong way. | |
| sharpness | int | With sprite_id / image_b64: 0–100, default 0. For scaled-up pixel art, higher turns in-between shades into solid art pixels. No effect on other uploads. | |
| perspective | string | With sprite_id / image_b64: the camera for the new views. match (default) copies your sprite's angle, side side-on platformer, topdown 3/4 top-down RPG, overhead looking almost straight down. | |
| quality | string | epic (4 CR) or mythic (12 CR). | |
| height | int | 16–512, default 128. Per-frame sprite height. | |
| directions | int | 4 (default) front/right/back/left, or 8 which adds the diagonals and returns them in rotation order S,SE,E,NE,N,NW,W,SW. 8-dir costs 6 CR (epic) / 16 CR (mythic), ignores mirror_sides, and cannot be combined with isometric. | |
| mirror_sides | bool | 4-dir only. Default true: draw 3 frames and mirror right→left for perfect L/R symmetry. | |
| isometric | bool | Four iso facings (SE, SW, NW, NE) drawn on the 2:1 iso grid at a real-world size, one shared scale, aligned on the same floor point (pivot_x/pivot_y in the response). SW/NW are mirrors. 4-dir only; height and tuning are ignored. | |
| iso_tile, iso_height | Isometric only. Tile 64 or 32; height auto, short, medium, tall, very_tall or metres. | ||
| detail | int | 1–3 shading richness. | |
| style_notes | string | Free-text style direction appended to the prompt. | |
| max_colours, outline_thickness, outline_colour, contrast, saturation | Optional tuning subset honoured for rotation sheets. |
Request
curl -X POST https://spritelab.dev/api/v1/rotate \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "a village blacksmith", "quality": "epic", "height": 48}'import requests, base64
r = requests.post(
"https://spritelab.dev/api/v1/rotate",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"prompt": "a village blacksmith", "quality": "epic", "height": 48},
).json()
for f in r["frames"]:
open(f'smith_{f["dir"]}.png', "wb").write(base64.b64decode(f["image_b64"]))
open("smith_sheet.png", "wb").write(base64.b64decode(r["sheet_b64"]))const r = await fetch("https://spritelab.dev/api/v1/rotate", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "a village blacksmith", quality: "epic", height: 48 }),
}).then(x => x.json());
// r.frames[i].dir / .image_b64, r.sheet_b64, r.animated_gif_b64Response
{
"id": "7c2e…",
"mode": "rotation",
"cost": 4,
"credits_remaining": 415,
"directions": 4,
"mirror_sides": true,
"frames": [
{ "dir": "S", "image_b64": "…" },
{ "dir": "E", "image_b64": "…" },
{ "dir": "N", "image_b64": "…" },
{ "dir": "W", "image_b64": "…" }
],
"sheet_b64": "…",
"animated_gif_b64": "…",
"sheet_download_url": "/api/v1/sprites/7c2e…/download?kind=sheet",
"gif_download_url": "/api/v1/sprites/7c2e…/download?kind=animated"
}
A whole grid of distinct sprites in one call, cleaned and split server-side into ready-to-use PNGs. Cheapest per-sprite path in the API. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| prompt | string | required | Theme for the set, e.g. "fantasy potions". |
| grid | string | 2x2 to 8x8, default 3x3. Any size up to 8x8. | |
| variance | string | variants, family or wild — how different the cells are. | |
| quality | string | epic (2 CR) or mythic (9 CR) for the whole grid. | |
| isometric | bool | Isometric prop set: the theme is planned into items matching props, each drawn on the 2:1 iso grid at a real-world size. grid and variance are ignored. Every cell comes back with its name, footprint and pivot_x/pivot_y (floor centre: place it on the tile centre). Same price. | |
| items | int | Isometric only. 4 or 6 (default). | |
| iso_tile | int | Isometric only. 64 (default) or 32. |
Request
curl -X POST https://spritelab.dev/api/v1/pack \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "fantasy potions", "grid": "3x3", "variance": "family"}'import requests, base64
r = requests.post(
"https://spritelab.dev/api/v1/pack",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"prompt": "fantasy potions", "grid": "3x3", "variance": "family"},
).json()
for cell in r["cells"]:
open(f'potion_{cell["index"]}.png', "wb").write(base64.b64decode(cell["image_b64"]))const r = await fetch("https://spritelab.dev/api/v1/pack", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "fantasy potions", grid: "3x3", variance: "family" }),
}).then(res => res.json());
console.log(r.cells.length, "sprites,", r.credits_remaining, "credits left");Response
{
"id": "…", "mode": "pack", "cost": 2, "credits_remaining": 417,
"grid": "3x3",
"sheet_b64": "…",
"cells": [
{ "index": 0, "width": 42, "height": 58, "image_b64": "…" },
{ "index": 1, "width": 40, "height": 61, "image_b64": "…" }
]
}
Any image in, a faithful pixel art redraw out. Transparent background by default, or keep the original. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| image_b64 | string | required | The source image (base64 or data URL). |
| remove_background | bool | Default true → transparent sprite. false keeps the original background. | |
| height | int | Output height, default 128. Plus the usual tuning fields. | |
| size_mode, cleanup | string | Same as /generate. The response adds cleanup, native_height, native_width and scale. |
Request
curl -X POST https://spritelab.dev/api/v1/convert \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"image_b64": "iVBORw0K…", "remove_background": true, "height": 128}'import requests, base64
src = base64.b64encode(open("photo.png", "rb").read()).decode()
r = requests.post(
"https://spritelab.dev/api/v1/convert",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"image_b64": src, "remove_background": True, "height": 128},
).json()
open("pixelated.png", "wb").write(base64.b64decode(r["image_b64"]))const r = await fetch("https://spritelab.dev/api/v1/convert", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ image_b64: srcBase64, remove_background: true, height: 128 }),
}).then(res => res.json());Response
{ "id": "…", "mode": "convert", "cost": 3, "credits_remaining": 414, "image_b64": "…" }
Exact-size 16 to 64px sprites, every pixel deliberately on the grid. Icons, items, tiny characters. The NxN output is the artifact — not resizable after. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| prompt | string | required | What to draw. |
| size | int | 16–64, default 32. The exact output dimensions. | |
| quality | string | epic (3 CR) or mythic (10 CR). | |
| detail | string | clean (default) or detailed. Forced clean below 24px. |
Request
curl -X POST https://spritelab.dev/api/v1/micro \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "a red health potion", "size": 32, "detail": "clean"}'import requests, base64
r = requests.post(
"https://spritelab.dev/api/v1/micro",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"prompt": "a red health potion", "size": 32, "detail": "clean"},
).json()
open("potion.png", "wb").write(base64.b64decode(r["image_b64"]))
print("closed outline:", r["outline_gaps"] == 0)const r = await fetch("https://spritelab.dev/api/v1/micro", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "a red health potion", size: 32, detail: "clean" }),
}).then(res => res.json());Response
{ "id": "…", "mode": "micro", "cost": 3, "credits_remaining": 411, "size": 32, "outline_gaps": 0, "image_b64": "…" }
Pixel-perfect themed variants of a sprite you already have. Output dimensions equal the input. Each variant is billed and refunded independently. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| image_b64 | string | required | The source sprite to re-theme. |
| themes | string[] | Themes to apply, e.g. ["lava", "ice"]. Omit and we pick a spread. | |
| variants | int | 1–20, default 4. Billed per variant. | |
| quality | string | epic (2 CR/variant) or mythic (6 CR/variant). | |
| match_silhouette | bool | Snap every variant's alpha to the source exactly. Default off. |
Request
curl -X POST https://spritelab.dev/api/v1/reskin \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"image_b64": "iVBORw0K…", "themes": ["lava", "ice", "gold"], "variants": 3}'import requests, base64
r = requests.post(
"https://spritelab.dev/api/v1/reskin",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"image_b64": src, "themes": ["lava", "ice", "gold"], "variants": 3},
).json()
for v in r["variants"]:
open(f'{v["theme"]}.png', "wb").write(base64.b64decode(v["image_b64"]))const r = await fetch("https://spritelab.dev/api/v1/reskin", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ image_b64: src, themes: ["lava", "ice", "gold"], variants: 3 }),
}).then(res => res.json());Response
{
"id": "…", "mode": "reskin", "cost": 6, "credits_remaining": 405,
"variants": [
{ "theme": "lava", "image_b64": "…" },
{ "theme": "ice", "image_b64": "…" },
{ "theme": "gold", "image_b64": "…" }
]
}
Add a donor item onto a base sprite — armor on a hero, a gem on a sword, wings on a ship. The item is redrawn in the base sprite's style, in place; output dimensions equal the base's. Each variant is billed and refunded independently. Worn items with a clear shape (armor, outfits, helmets) land best.
| Field | Type | Description | |
|---|---|---|---|
| target_b64 | string | required | The base sprite. |
| donor_b64 | string | required | The item to add. |
| apply_as | string | attach (default), armor, outfit, weapon, hat, accessory, style or custom. Armor/outfit/hat re-dress the character; attach adds the item as-is. | |
| instructions | string | Optional extra guidance, e.g. "held on the left arm". | |
| variants | int | 1–6, default 2. Billed per variant. | |
| quality | string | epic (2 CR/variant) or mythic (6 CR/variant). |
Request
curl -X POST https://spritelab.dev/api/v1/merge \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"target_b64": "iVBORw0K…", "donor_b64": "iVBORw0K…", "apply_as": "armor", "variants": 2}'import requests, base64
r = requests.post(
"https://spritelab.dev/api/v1/merge",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"target_b64": hero, "donor_b64": armor, "apply_as": "armor", "variants": 2},
).json()
for i, v in enumerate(r["variants"]):
open(f"merged_{i}.png", "wb").write(base64.b64decode(v["image_b64"]))const r = await fetch("https://spritelab.dev/api/v1/merge", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ target_b64: hero, donor_b64: armor, apply_as: "armor", variants: 2 }),
}).then(res => res.json());Response
{
"mode": "merge", "apply_as": "armor", "cost": 4, "credits_remaining": 405,
"source_b64": "…", "donor_b64": "…",
"variants": [
{ "image_b64": "…", "width": 64, "height": 64 },
{ "image_b64": "…", "width": 64, "height": 64 }
]
}
EDIT
Edit a PNG sprite from a prompt, with an optional painted region. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| image_b64 | string | required | PNG as base64 or data URL. |
| prompt | string | required | Describe the change in 1 to 500 characters. |
| region_b64 | string | Optional PNG mask, same dimensions as the image. Non-transparent pixels are painted; fully opaque masks use non-black pixels. | |
| variants | int | 1 to 4, default 1. | |
| quality | string | epic (2 CR) or mythic (6 CR) per variant. | |
| keep_silhouette | bool | Default true. False allows silhouette and canvas growth. |
Returns variants: [{image_b64, width, height, changed, region_b64?}], credits_charged and credits_remaining. Failed variants are omitted and refunded. Unchanged variants return the original PNG with changed: false and are refunded. No auto-history. Chained edits are coming.
Full-frame pixel art scenes up to 8K, from a prompt or a rough sketch. Returns the final image plus its native low-res grid. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| prompt | string | required* | The scene. *Or send sketch_b64 to redraw a rough layout. |
| width, height | int | Default 1920×1080, up to 7680×4320. | |
| quality | string | epic (4 CR) or mythic (15 CR). | |
| detail | int | 1–3. Plus max_colours, contrast, saturation, style_notes. |
Request
curl -X POST https://spritelab.dev/api/v1/backgrounds \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "a misty pine forest at dawn", "width": 1920, "height": 1080}'import requests, base64
r = requests.post(
"https://spritelab.dev/api/v1/backgrounds",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"prompt": "a misty pine forest at dawn", "width": 1920, "height": 1080},
).json()
open("forest.png", "wb").write(base64.b64decode(r["image_b64"]))const r = await fetch("https://spritelab.dev/api/v1/backgrounds", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "a misty pine forest at dawn", width: 1920, height: 1080 }),
}).then(res => res.json());Response
{ "id": "…", "mode": "scene", "cost": 4, "credits_remaining": 401, "image_b64": "…", "native_b64": "…" }
A top-down 15-piece corner or 17-piece edge atlas, or an isometric corner set with optional raised side faces, from one prompt. Uniquely, the response also carries drop-in Godot 4 .tres and Tiled .tsx files with the autotiling pre-wired, including isometric geometry and side-face offsets. Write each next to the atlas PNG and import, zero hand-assignment.
| Field | Type | Description | |
|---|---|---|---|
| prompt | string | required | The surface material, e.g. "mossy cobblestone". |
| base_prompt | string | Optional second material for a two-terrain pair (e.g. "dirt"). Same price. | |
| projection | string | topdown (default) or iso. | |
| iso_depth | bool | Default true. Raised side faces for iso single terrain: 8px at width 32, 16px at width 64. Pairs are always flat. | |
| side_prompt | string | Optional side material for raised iso tiles, max 200 characters. Defaults to a natural cross-section of the surface. | |
| layout | string | 15 corner/dual-grid (terrain) or 17 edge/standard-grid (paths, walls). Default 15. Ignored for iso, which always uses the corner set. | |
| tile_size | int | Top-down: 16 or 32 (default 32). Iso: 32 or 64 (default 64), the diamond width. Invalid iso sizes return 400. Plus edge_style, detail. | |
| quality | string | epic (10 CR) or mythic (30 CR). |
Request
curl -X POST https://spritelab.dev/api/v1/tileset \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "lush grass", "base_prompt": "dirt", "layout": "15"}'import requests, base64
r = requests.post(
"https://spritelab.dev/api/v1/tileset",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"prompt": "lush grass", "base_prompt": "dirt", "layout": "15"},
).json()
# the atlas + the drop-in engine files
open(r["atlas_filename"], "wb").write(base64.b64decode(r["atlas_b64"]))
open("terrain.tres", "w").write(r["godot_tres"]) # Godot 4
open("terrain.tsx", "w").write(r["tiled_tsx"]) # Tiledconst r = await fetch("https://spritelab.dev/api/v1/tileset", {
method: "POST",
headers: { Authorization: "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "lush grass", base_prompt: "dirt", layout: "15" }),
}).then(r => r.json());
// r.atlas_b64, r.godot_tres, r.tiled_tsxReturns atlas_b64, preview_b64, godot_tres, tiled_tsx, atlas_filename, plus projection, tile_size, tile_w/tile_h (ground size), cell_w/cell_h (atlas cell size), depth, atlas_w/atlas_h, layout, pair, cost and credits_remaining.
Iso uses a 4×4 atlas with layout iso-corner-bitmask-4x4. Each cell is tile_w by tile_h + depth. Cell index is the corner bitmask: top=1, right=2, left=4, bottom=8. Cell 0 is transparent for single terrain or base fill for pairs. For a raised block, send {"prompt":"grass", "projection":"iso", "side_prompt":"layered soil"}.
A sprite plus a motion prompt in, an animated spritesheet and GIF out. Async: returns a job id, then poll GET /jobs/{id}. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| image_b64 | string | required | The sprite to animate. Standard max 256px per axis; Pro max 320px. Min 8px, aspect 1:2 to 2:1. |
| prompt | string | required | The motion, e.g. "gentle idle bounce". |
| frames | int | Default 8. Standard: even 2–16; Pro: any integer 3–16. Invalid counts return 400. | |
| engine | string | standard (default, 20 CR) or pro (30 CR). Inputs over 256 on either axis that fit 320x320 automatically use Pro. Above 320 returns 400. Explicit Pro stays Pro for smaller inputs. |
Request
# 1. submit
curl -X POST https://spritelab.dev/api/v1/animate \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"image_b64": "iVBORw0K…", "prompt": "gentle idle bounce", "frames": 8}'
# -> { "job_id": "a1b2…", "status": "queued" }
# 2. poll until succeeded
curl https://spritelab.dev/api/v1/jobs/a1b2… \
-H "Authorization: Bearer sl_live_YOUR_KEY"import requests, time, base64
h = {"Authorization": "Bearer sl_live_YOUR_KEY"}
job = requests.post("https://spritelab.dev/api/v1/animate", headers=h,
json={"image_b64": src, "prompt": "gentle idle bounce", "frames": 8}).json()
while True:
s = requests.get(f'https://spritelab.dev/api/v1/jobs/{job["job_id"]}', headers=h).json()
if s["status"] in ("succeeded", "failed"):
break
time.sleep(3)
if s["status"] == "succeeded":
open("anim.gif", "wb").write(base64.b64decode(s["gif_b64"]))const h = { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" };
const job = await fetch("https://spritelab.dev/api/v1/animate", {
method: "POST", headers: h,
body: JSON.stringify({ image_b64: src, prompt: "gentle idle bounce", frames: 8 }),
}).then(r => r.json());
let s;
do {
await new Promise(r => setTimeout(r, 3000));
s = await fetch(`https://spritelab.dev/api/v1/jobs/${job.job_id}`, { headers: h }).then(r => r.json());
} while (!["succeeded", "failed"].includes(s.status));Response
// POST /animate
202 Accepted
{ "job_id": "a1b2…", "status": "queued" }
// GET /jobs/{id} (poll)
{ "job_id": "a1b2…", "status": "succeeded", "id": "…",
"sheet_b64": "…", "gif_b64": "…", "frame_count": 8, "fps": 8, "refunded": false }
A UI mockup or a description in, a complete game UI kit out: panels, buttons with their states, bars, slots and icons, sliced, named and 9-slice ready, with a Godot theme and Unity package. Async: returns a kit id, poll GET /ui-kit/{id}, then download the zip. Early access. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| image_b64 | string | Mockup to rebuild (skips the concept charge). Or send prompt / style. | |
| max_sheets | int | 1–3, default 2. Cost cap; unused sheets are refunded. | |
| pixel | bool | Convert the kit to pixel art (default HD). |
Re-run the local pipeline on a sprite you already generated at new dimensions or tuning. No model call, so it's free. Returns PNG bytes. Owner-only. Full reference →
| Field | Type | Description | |
|---|---|---|---|
| sprite_id | string | required | Id of a sprite you generated (from a prior response). |
| height | int | required* | New height. *Not needed with size_mode: "native". A matching width forces the legacy square. |
| size_mode | string | exact, native or crisp, same as /generate. | |
| cleanup | string | advanced or classic. Defaults to the cleanup the sprite was made with. | |
| max_colours, outline_thickness, palette_lock, … | Any tuning field, same as /generate. palette_lock also takes palette:<id> for a saved palette. |
Request
curl -X POST https://spritelab.dev/api/v1/resize \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"sprite_id": "3f9a…c1", "height": 32, "max_colours": 8}' \
-o small.pngimport requests
r = requests.post(
"https://spritelab.dev/api/v1/resize",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"sprite_id": "3f9a…c1", "height": 32, "max_colours": 8},
)
open("small.png", "wb").write(r.content)const res = await fetch("https://spritelab.dev/api/v1/resize", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ sprite_id: "3f9a…c1", height: 32, max_colours: 8 }),
});
writeFileSync("small.png", Buffer.from(await res.arrayBuffer()));Response
200 OK
Content-Type: image/png
X-SpriteLab-Credits-Remaining: 401
<binary PNG bytes>
Rewrite a rough prompt into a richer sprite description. Free.
| Field | Type | Description | |
|---|---|---|---|
| prompt | string | required | Your rough prompt. |
Request
curl -X POST https://spritelab.dev/api/v1/enhance-prompt \
-H "Authorization: Bearer sl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "knight"}'import requests
r = requests.post("https://spritelab.dev/api/v1/enhance-prompt",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"},
json={"prompt": "knight"}).json()
print(r["enhanced"])const r = await fetch("https://spritelab.dev/api/v1/enhance-prompt", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "knight" }),
}).then(res => res.json());Response
{ "enhanced": "a stoic knight in weathered steel plate armor, blue tabard, longsword…" }
What you can spend over the API and your tier. credits is the total: purchased credits plus the plan credits you can use (Knight and up, after the plan markup). A cheap probe before a batch.
No parameters.
Request
curl https://spritelab.dev/api/v1/credits \
-H "Authorization: Bearer sl_live_YOUR_KEY"import requests
r = requests.get("https://spritelab.dev/api/v1/credits",
headers={"Authorization": "Bearer sl_live_YOUR_KEY"}).json()
print(r["credits"], r["tier"])const r = await fetch("https://spritelab.dev/api/v1/credits", {
headers: { "Authorization": "Bearer sl_live_YOUR_KEY" },
}).then(res => res.json());Response
{ "credits": 706, "purchased_credits": 40, "plan_credits": 666, "plan_markup_pct": 50, "tier": "knight" }
Saved palettes, shared with the app (the Palettes button and Palette lock). Use one on any tuning field with palette_lock: "palette:<id>", or on a whole set with /palettes/match.
| Call | Body | Description | |
|---|---|---|---|
| GET /palettes | Your saved palettes: id, name, colours. | ||
| POST /palettes | {name?, colours} | Save 2–256 #RRGGBB colours (a list, or one string with any separator). If the same colours are already saved, in any order, you get that palette back with existing: true. Up to 60 per account. | |
| PATCH /palettes/{id} | {name} | Rename. | |
| DELETE /palettes/{id} | Delete. 404 palette_not_found if it doesn't exist. |
Request
curl -X POST https://spritelab.dev/api/v1/palettes \
-H "Authorization: Bearer sl_live_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"name": "Dusk", "colours": "#1D2B53 #7E2553 #FF004D #FFEC27"}'import requests
H = {"Authorization": "Bearer sl_live_YOUR_KEY"}
p = requests.post("https://spritelab.dev/api/v1/palettes", headers=H,
json={"name": "Dusk", "colours": ["#1D2B53", "#7E2553", "#FF004D", "#FFEC27"]}).json()
# then on any sprite call: {"prompt": "...", "palette_lock": f"palette:{p['id']}"}const p = await fetch("https://spritelab.dev/api/v1/palettes", {
method: "POST",
headers: { "Authorization": "Bearer sl_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ name: "Dusk", colours: "#1D2B53 #7E2553 #FF004D #FFEC27" }),
}).then(res => res.json());Response
{ "id": 17, "name": "Dusk", "colours": ["#1D2B53", "#7E2553", "#FF004D", "#FFEC27"], "created_at": 1791674935 }
Give a set of sprites you generated one shared palette, so they look like one artist made them. Shapes never change, only colours. Preview first, then send apply: true to save it to the sprites (their downloads and history update). POST /palettes/match/undo with the same sprite_ids restores the original colours. Animations, rotations, pack sheets, tilesets and backgrounds are skipped and listed in skipped.
| Field | Type | Description | |
|---|---|---|---|
| sprite_ids | string[] | required | 1–40 sprite ids from earlier responses. |
| colours | int | 4–64, default 24. Size of the shared palette built from the sprites themselves. | |
| palette_id | int | Use a saved palette instead. | |
| palette | string[] | string | Or pass hex colours directly. | |
| apply | bool | Default false: preview, returns each sprite as image_b64. true: writes them and returns applied. |
Response (preview)
{ "palette": ["#000000", "#1D2B53", "..."], "sprites": [{ "id": "dfb7…", "image_b64": "iVBOR…", "width": 64, "height": 64 }], "skipped": [] }
OpenAPI spec
A machine-readable OpenAPI 3.1 spec lives at GET /api/v1/openapi.json (same Bearer key while the beta is private). Grab it and import into Postman, Insomnia or a client generator:
curl -H "Authorization: Bearer sl_live_YOUR_KEY" \
https://spritelab.dev/api/v1/openapi.json -o spritelab-openapi.json