GUIDES MCP OPEN APP

SpriteLab API

Generate clean, game-ready pixel art over REST: sprites from text, image conversion, exact-size micro sprites, themed reskins, full-frame backgrounds and animated spritesheets. JSON in, images out, with the full cleanup pipeline applied to every result.

Live for everyone. Create a key in the app under Account → API keys and start calling. Questions or feature requests? Join the Discord.
ON THIS PAGE

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:

PlanPlan credits over the APIA 20-credit animation costs
Squire, RangerNot used (purchased credits only)20 purchased
Knight+50% per call30 plan credits
Paladin+25% per call25 plan credits
DragonlordNo markup20 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.

CallCost
POST /generate1 CR epic / 6 CR mythic
POST /pack2 CR epic / 9 CR mythic — whole grid, any size
POST /convert3 CR
POST /micro3 CR epic / 10 CR mythic
POST /reskin2 CR epic / 6 CR mythic — per variant
POST /api/v1/edit2 CR epic / 6 CR mythic per variant
POST /merge2 CR epic / 6 CR mythic — per variant
POST /backgrounds4 CR epic / 15 CR mythic
POST /tileset10 CR epic / 30 CR mythic
POST /animate20 CR Standard / 30 CR Pro
POST /ui-kit3 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.

StatusCodeMeaning
400bad_json / bad_prompt / bad_image / bad_size / bad_quality / bad_size_mode / bad_cleanupMalformed body or a field out of range. The message says which
400size_mode_needs_heightsize_mode native or crisp sent with the legacy width+height. Send height only
401auth_missing / auth_invalidNo Bearer header, or the key is wrong or revoked
402insufficient_purchased_creditsNot enough API-spendable credits (purchased, plus plan credits on Knight and up). Top up or upgrade in the app under Billing
403blockedAccount suspended
404not_found / job_not_foundDoesn't exist or isn't yours (deliberately the same response)
409not_resizable/resize: this row type can't go back through the sprite pipeline
429rate_limitedOver 60 requests per minute on this key
5xxupstream / pipelineGeneration 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

POST /generate 1–6 CR

One cleaned pixel art sprite from a text prompt. Returns raw PNG bytes with the sprite id and remaining balance in headers. Full reference →

FieldTypeDescription
promptstringrequiredWhat to draw.
qualitystringepic (default, 1 CR) or mythic (6 CR).
heightint16–512, default 128. Sprite is exactly this tall, width follows natural proportions.
widthintLegacy: width + matching height forces an exact square (stretches). Skip for characters.
size_modestringexact (default), native (the model's own pixel size, crispest, height ignored) or crisp (that size times the whole number closest to height). More.
cleanupstringadvanced (default, keeps the model's own pixels so 1px details survive) or classic (the previous cleanup).
directionstringauto, front, left, right, back, top, angled. Default right. Ignored when style_reference_id is set.
detailint1–3 shading richness (1 flat, 3 rich). Default 1.
isometricboolIsometric 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_tileintIsometric only. 64 (default, 64×32 tiles) or 32.
iso_footprintstringIsometric only. auto (default, guessed from the prompt), 1x1, 2x1, 1x2, 2x2, 3x3.
iso_heightstringIsometric only. auto, short, medium (a person), tall, very_tall, or metres.
style_notesstringFree-text style direction appended to the prompt, e.g. "muted earthy palette, thick outline".
style_reference_idstringAn 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, brightnessOptional 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.png
import 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>
POST /rotate 4–15 CR

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

FieldTypeDescription
promptstringrequired*The character or mob. *Optional when you send sprite_id or image_b64.
sprite_idstringTurn 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_b64stringOr send any sprite image (PNG/JPG, base64) to turn around, same rules as sprite_id.
facingstringWith 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.
sharpnessintWith 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.
perspectivestringWith 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.
qualitystringepic (4 CR) or mythic (12 CR).
heightint16–512, default 128. Per-frame sprite height.
directionsint4 (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_sidesbool4-dir only. Default true: draw 3 frames and mirror right→left for perfect L/R symmetry.
isometricboolFour 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_heightIsometric only. Tile 64 or 32; height auto, short, medium, tall, very_tall or metres.
detailint1–3 shading richness.
style_notesstringFree-text style direction appended to the prompt.
max_colours, outline_thickness, outline_colour, contrast, saturationOptional 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_b64

Response

{
  "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"
}
POST /pack 2–9 CR

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 →

FieldTypeDescription
promptstringrequiredTheme for the set, e.g. "fantasy potions".
gridstring2x2 to 8x8, default 3x3. Any size up to 8x8.
variancestringvariants, family or wild — how different the cells are.
qualitystringepic (2 CR) or mythic (9 CR) for the whole grid.
isometricboolIsometric 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.
itemsintIsometric only. 4 or 6 (default).
iso_tileintIsometric 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": "…" }
  ]
}
POST /convert 3 CR

Any image in, a faithful pixel art redraw out. Transparent background by default, or keep the original. Full reference →

FieldTypeDescription
image_b64stringrequiredThe source image (base64 or data URL).
remove_backgroundboolDefault true → transparent sprite. false keeps the original background.
heightintOutput height, default 128. Plus the usual tuning fields.
size_mode, cleanupstringSame 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": "…" }
POST /micro 3–10 CR

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 →

FieldTypeDescription
promptstringrequiredWhat to draw.
sizeint16–64, default 32. The exact output dimensions.
qualitystringepic (3 CR) or mythic (10 CR).
detailstringclean (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": "…" }
POST /reskin 2–6 CR / variant

Pixel-perfect themed variants of a sprite you already have. Output dimensions equal the input. Each variant is billed and refunded independently. Full reference →

FieldTypeDescription
image_b64stringrequiredThe source sprite to re-theme.
themesstring[]Themes to apply, e.g. ["lava", "ice"]. Omit and we pick a spread.
variantsint1–20, default 4. Billed per variant.
qualitystringepic (2 CR/variant) or mythic (6 CR/variant).
match_silhouetteboolSnap 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": "…" }
  ]
}
POST /merge 2–6 CR / variant

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.

FieldTypeDescription
target_b64stringrequiredThe base sprite.
donor_b64stringrequiredThe item to add.
apply_asstringattach (default), armor, outfit, weapon, hat, accessory, style or custom. Armor/outfit/hat re-dress the character; attach adds the item as-is.
instructionsstringOptional extra guidance, e.g. "held on the left arm".
variantsint1–6, default 2. Billed per variant.
qualitystringepic (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

POST/edit2 / 6 CR / variant

Edit a PNG sprite from a prompt, with an optional painted region. Full reference →

FieldTypeDescription
image_b64stringrequiredPNG as base64 or data URL.
promptstringrequiredDescribe the change in 1 to 500 characters.
region_b64stringOptional PNG mask, same dimensions as the image. Non-transparent pixels are painted; fully opaque masks use non-black pixels.
variantsint1 to 4, default 1.
qualitystringepic (2 CR) or mythic (6 CR) per variant.
keep_silhouetteboolDefault 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.

POST /backgrounds 4–15 CR

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 →

FieldTypeDescription
promptstringrequired*The scene. *Or send sketch_b64 to redraw a rough layout.
width, heightintDefault 1920×1080, up to 7680×4320.
qualitystringepic (4 CR) or mythic (15 CR).
detailint1–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": "…" }
POST /tileset 10–30 CR

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.

FieldTypeDescription
promptstringrequiredThe surface material, e.g. "mossy cobblestone".
base_promptstringOptional second material for a two-terrain pair (e.g. "dirt"). Same price.
projectionstringtopdown (default) or iso.
iso_depthboolDefault true. Raised side faces for iso single terrain: 8px at width 32, 16px at width 64. Pairs are always flat.
side_promptstringOptional side material for raised iso tiles, max 200 characters. Defaults to a natural cross-section of the surface.
layoutstring15 corner/dual-grid (terrain) or 17 edge/standard-grid (paths, walls). Default 15. Ignored for iso, which always uses the corner set.
tile_sizeintTop-down: 16 or 32 (default 32). Iso: 32 or 64 (default 64), the diamond width. Invalid iso sizes return 400. Plus edge_style, detail.
qualitystringepic (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"])    # Tiled
const 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_tsx

Returns 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"}.

POST /animate 20 CR Standard / 30 CR Proasync

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 →

FieldTypeDescription
image_b64stringrequiredThe sprite to animate. Standard max 256px per axis; Pro max 320px. Min 8px, aspect 1:2 to 2:1.
promptstringrequiredThe motion, e.g. "gentle idle bounce".
framesintDefault 8. Standard: even 2–16; Pro: any integer 3–16. Invalid counts return 400.
enginestringstandard (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 }
POST /ui-kit 3 CR concept + 8 CR / sheetasync

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 →

FieldTypeDescription
image_b64stringMockup to rebuild (skips the concept charge). Or send prompt / style.
max_sheetsint1–3, default 2. Cost cap; unused sheets are refunded.
pixelboolConvert the kit to pixel art (default HD).
POST /resize Free

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 →

FieldTypeDescription
sprite_idstringrequiredId of a sprite you generated (from a prior response).
heightintrequired*New height. *Not needed with size_mode: "native". A matching width forces the legacy square.
size_modestringexact, native or crisp, same as /generate.
cleanupstringadvanced 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.png
import 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>
POST /enhance-prompt Free

Rewrite a rough prompt into a richer sprite description. Free.

FieldTypeDescription
promptstringrequiredYour 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…" }
GET /credits Free

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" }
GETPOSTPATCHDELETE /palettes Free

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.

CallBodyDescription
GET /palettesYour 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 }
POST /palettes/match Free

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.

FieldTypeDescription
sprite_idsstring[]required1–40 sprite ids from earlier responses.
coloursint4–64, default 24. Size of the shared palette built from the sprites themselves.
palette_idintUse a saved palette instead.
palettestring[] | stringOr pass hex colours directly.
applyboolDefault 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

MCP & Aseprite

Every endpoint here is also an MCP tool for Claude Code, Cursor and any MCP client (one config line, same key), and powers the SpriteLab Aseprite plugin (coming soon, not released yet). Integrating at real volume? Message us on Discord — we do flat-rate bulk pricing.