The AI Sprite Maker API does everything the web generator does. You send a job, we reserve its credits, run it in the background, and hand back a PNG sprite sheet with JSON metadata your engine can read. It uses the same credit balance and the same prices as the website.
At a glance
Base URL https://aispritemaker.com/api/v1 · JSON in and out · Authorization: Bearer asm_live_… · jobs are asynchronous: POST /jobs, then poll GET /jobs/:id · included with every plan, Free too. Machine-readable spec: /openapi.json (OpenAPI 3.1).
Quickstart
- Sign in (any plan works, including Free) and create a key at Dashboard → API keys. The key is shown once; store it in an environment variable.
- Queue a job. This one animates a character with the library
walkmotion in 4 directions, which costs 4 × 10 = 40 credits (drawing the character from the prompt is included):
curl https://aispritemaker.com/api/v1/jobs \
-H "Authorization: Bearer $ASM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "animation",
"prompt": "small fox knight, blue cape, round shield",
"motion_id": "walk",
"style": "16-bit",
"view": "topdown",
"directions": 4,
"size": 64
}'
const res = await fetch("https://aispritemaker.com/api/v1/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ASM_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "animation",
prompt: "small fox knight, blue cape, round shield",
motion_id: "walk",
style: "16-bit",
view: "topdown",
directions: 4,
size: 64,
}),
});
const { job } = await res.json();
console.log(job.id, job.status, job.credits_reserved);
import os, requests
res = requests.post(
"https://aispritemaker.com/api/v1/jobs",
headers={"Authorization": f"Bearer {os.environ['ASM_API_KEY']}"},
json={
"type": "animation",
"prompt": "small fox knight, blue cape, round shield",
"motion_id": "walk",
"style": "16-bit",
"view": "topdown",
"directions": 4,
"size": 64,
},
)
job = res.json()["job"]
print(job["id"], job["status"], job["credits_reserved"])
- Poll the job every 2 seconds until
statusissucceeded,failedorcanceled, then download the files inoutputs. A full polling loop is in Polling a job to completion.
Authentication
Every request needs an API key in the Authorization header:
Authorization: Bearer asm_live_3f9c2a…
- Keys start with
asm_live_. We store only a SHA-256 hash, so a lost key cannot be recovered; revoke it and make a new one. - Keys belong to your account and spend your credits. Use one key per app or environment so you can revoke one without breaking the others.
- API access is included with every plan, including Free. Free accounts are limited by their credits (10 a day plus 30 on sign-up), 128 px frames and one job at a time.
- Never put a key in client-side code or a public repo. Call the API from your server, build script or editor tool.
Conventions
- Base URL:
https://aispritemaker.com/api/v1 - Format: JSON request and response bodies, UTF-8. Send
Content-Type: application/json. - IDs: jobs use sortable ULIDs (
01JC8Y…). Treat all IDs as opaque strings. - Times: ISO 8601 in UTC, e.g.
2026-11-06T09:41:07.118Z. - Asset URLs are signed and short-lived. Download outputs when the job finishes, or fetch the job again to get fresh URLs.
- Idempotency: send an
Idempotency-Keyheader onPOST /jobs(any unique string up to 64 characters, e.g. a UUID). Retrying with the same key returns the original job instead of creating and charging a second one.
Credits and pricing
Each job reserves the most it can cost when you create it, charges what it actually used when it finishes, and releases the rest. Failed and cancelled jobs release everything. credits_reserved and credits_charged on the job show both numbers.
| Job | type | Credits |
|---|---|---|
| Still sprite, character, item or UI piece | sprite | 2 per image (count: 4 = 4 × 2) |
| Edit, inpaint or recolour an image | edit | 2 |
| Library motion, per direction | animation | 10 × directions |
| 4-direction turnaround | directions | 8 |
| 8-direction turnaround | directions | 14 |
| Seamless tileset (up to 16 tiles) | tileset | 6 |
| Custom motion not in the library | custom_motion | 30 |
| Video or GIF to sprite, AI cleanup | video_to_sprite | 5 per second of footage |
An animation costs a flat 10 credits per direction, including drawing the character when you only pass a prompt. A directions or custom_motion job given only a text prompt (no character_asset_id or reference image) first draws the character, which adds 2 credits. On Pro one credit costs about $0.008, so a 4-direction walk cycle is roughly $0.076 per direction. The pricing page has the full table.
Endpoints
| Method | Path | What it does |
|---|---|---|
GET | /me | Your account, plan, credit balance and concurrency |
GET | /motions | The motion library (public, no key needed) |
POST | /jobs | Create a job |
GET | /jobs/:id | Get one job and its outputs |
GET | /jobs | List your jobs, newest first |
POST | /jobs/:id/cancel | Cancel a queued or running job |
POST | /uploads | Get a URL to upload a reference image, GIF or video |
GET | /assets | List your output files, newest first |
GET | /assets/:id/file/:name | Download an output file |
GET POST DELETE | /keys, /keys/:id | Manage API keys |
GET /me
Returns who the key belongs to and what they can spend.
curl https://aispritemaker.com/api/v1/me \
-H "Authorization: Bearer $ASM_API_KEY"
const me = await fetch("https://aispritemaker.com/api/v1/me", {
headers: { Authorization: `Bearer ${process.env.ASM_API_KEY}` },
}).then((r) => r.json());
me = requests.get(
"https://aispritemaker.com/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['ASM_API_KEY']}"},
).json()
Response:
{
"user": { "id": "u_7Hq2…", "email": "you@studio.dev", "name": "Mara", "image": null },
"plan": "pro",
"credits": { "total": 2361, "daily": 10, "subscription": 2251, "packs": 100 },
"concurrency": 4
}
credits.total is what you can spend right now. Daily credits are used first, then plan credits, then sign-up and promotional credits, then packs.
GET /motions
Lists the motion library. No authentication required, so you can build a motion picker without exposing a key.
curl https://aispritemaker.com/api/v1/motions
{
"motions": [
{
"id": "walk",
"name": "Walk",
"category": "locomotion",
"loop": true,
"frames": 8,
"fps": 10,
"views": ["side", "topdown"],
"preview_url": "https://aispritemaker.com/showcase/motions/walk.png"
}
]
}
Pass id as motion_id when you create an animation job. frames and fps are the defaults the output will use. Browse them visually in the motion library.
POST /jobs
Creates a job, reserves its credits and returns 202 Accepted with the job in queued state.
Body parameters
| Field | Type | Used by | Description |
|---|---|---|---|
type | string | all | sprite, animation, directions, tileset, edit, custom_motion or video_to_sprite |
prompt | string ≤ 1000 | most | What to draw: subject, outfit, colours, props. Leave out style and camera words; style and view handle those |
style | string | all | Style preset: 16-bit (default), 8-bit, gba, snes, game-boy, hd-pixel, chibi, isometric, 1-bit, pico-8. Older aliases 16bit, 8bit, nes, gameboy, 1bit and pico8 also work |
view | string | all | side, topdown or isometric |
size | integer | all | Frame size in px: 32, 48, 64, 96, 128, 256, 512. Default 64 (32 for tilesets). Capped by plan: Free 128, Starter 256, Pro and Studio 512 |
palette | integer 2–256 | all | Maximum colours. Defaults to the style preset (32 for 16-bit, 24 for gba, 4 for 8-bit). game-boy, 1-bit and pico-8 always use their fixed palettes and ignore this field |
count | 1 or 4 | sprite | Number of variations |
motion_id | string | animation | A motion from GET /motions |
directions | 1, 4 or 8 | animation, directions | Directions to render. Side-view-only motions accept 1 |
character_asset_id | string | animation, directions, edit, custom_motion, sprite | Reuse a sprite from an earlier job so every animation shows the same character |
reference_upload_id | string | same as above | An uploaded PNG, JPEG or WebP to use as the character or style reference |
motion_prompt | string ≤ 600 | custom_motion | Describe a motion that is not in the library |
video_upload_id | string | video_to_sprite | An uploaded GIF or video |
Required fields by type
type | Needs |
|---|---|
sprite | prompt, or a reference |
tileset | prompt |
edit | prompt (the instruction) and a reference (reference_upload_id or character_asset_id) |
animation | motion_id, and prompt or a reference |
directions | prompt or a reference; directions must be 4 or 8 |
custom_motion | motion_prompt, and prompt or a reference |
video_to_sprite | video_upload_id |
Example: draw a character, then animate that exact character
# 1. Four variations of a still sprite
curl https://aispritemaker.com/api/v1/jobs \
-H "Authorization: Bearer $ASM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1d2c1e-hero-v1" \
-d '{"type":"sprite","prompt":"dwarf miner, orange beard, pickaxe, leather apron","style":"16-bit","view":"side","size":64,"count":4}'
# 2. Pick one output asset id and animate it
curl https://aispritemaker.com/api/v1/jobs \
-H "Authorization: Bearer $ASM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"animation","character_asset_id":"as_01JC9…","motion_id":"attack-slash","view":"side","directions":1}'
const api = (path, body) =>
fetch(`https://aispritemaker.com/api/v1${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${process.env.ASM_API_KEY}`,
"Content-Type": "application/json",
},
body: body && JSON.stringify(body),
}).then((r) => r.json());
const { job: still } = await api("/jobs", {
type: "sprite",
prompt: "dwarf miner, orange beard, pickaxe, leather apron",
style: "16-bit",
view: "side",
size: 64,
count: 4,
});
// ...wait for it to succeed, then:
const { job: anim } = await api("/jobs", {
type: "animation",
character_asset_id: chosenAssetId,
motion_id: "attack-slash",
view: "side",
directions: 1,
});
API = "https://aispritemaker.com/api/v1"
H = {"Authorization": f"Bearer {os.environ['ASM_API_KEY']}"}
still = requests.post(f"{API}/jobs", headers=H, json={
"type": "sprite",
"prompt": "dwarf miner, orange beard, pickaxe, leather apron",
"style": "16-bit", "view": "side", "size": 64, "count": 4,
}).json()["job"]
# ...wait for it to succeed, then:
anim = requests.post(f"{API}/jobs", headers=H, json={
"type": "animation",
"character_asset_id": chosen_asset_id,
"motion_id": "attack-slash",
"view": "side",
"directions": 1,
}).json()["job"]
Response 202 Accepted
{
"job": {
"id": "01JC9A6Q8W3R0V5N2TGBX4K7MD",
"type": "sprite",
"status": "queued",
"step": "queued",
"progress": 0,
"credits_reserved": 8,
"credits_charged": 0,
"created_at": "2026-11-06T09:41:07.118Z",
"outputs": []
}
}
GET /jobs/:id
Returns the job. While it runs, step names the current stage and progress goes from 0 to 1. When status is succeeded, outputs lists the files.
{
"job": {
"id": "01JC9B2M4F…",
"type": "animation",
"status": "succeeded",
"step": "done",
"progress": 1,
"credits_reserved": 40,
"credits_charged": 40,
"created_at": "2026-11-06T09:42:10.002Z",
"finished_at": "2026-11-06T09:43:31.540Z",
"outputs": [
{
"id": "as_01JC9B…",
"kind": "sheet",
"url": "https://aispritemaker.com/api/v1/assets/as_01JC9B…/file/sheet.png?sig=…",
"width": 512,
"height": 256,
"meta": {
"frames": 8, "columns": 8, "rows": 4, "fps": 10,
"frame_w": 64, "frame_h": 64,
"directions": ["down", "left", "right", "up"]
}
},
{ "id": "as_01JC9C…", "kind": "gif", "url": "…/file/preview.gif?sig=…", "width": 64, "height": 64 },
{ "id": "as_01JC9D…", "kind": "json", "url": "…/file/sheet.json?sig=…", "width": 512, "height": 256 },
{ "id": "as_01JC9E…", "kind": "frames_zip", "url": "…/file/frames.zip?sig=…", "width": 64, "height": 64 }
]
}
}
Output kinds
kind | What it is |
|---|---|
sheet | PNG sprite sheet. One row per direction, in the order given by meta.directions, meta.columns frames per row |
sprite | A single transparent PNG (still sprites, turnaround cells) |
gif | Animated preview GIF |
json | Frame rectangles, fps and direction names for engines and packers |
frames_zip | Every frame as its own PNG |
preview | A small preview image for listings |
Job status values
status | Meaning |
|---|---|
queued | Accepted, waiting for a worker slot (Free runs 1 job at a time, Starter 2, Pro 4, Studio 10) |
running | Generating. Check step and progress |
succeeded | Done. outputs is filled and credits_charged is final |
failed | Stopped with error.code and error.message. Reserved credits were released |
canceled | Cancelled by you before it finished. Reserved credits were released |
GET /jobs
Lists your jobs, newest first. Query parameters: limit (1–100, default 20) and cursor (the next_cursor from the previous page).
curl "https://aispritemaker.com/api/v1/jobs?limit=20" \
-H "Authorization: Bearer $ASM_API_KEY"
{ "jobs": [ { "id": "01JC9B2M4F…", "type": "animation", "status": "succeeded", "…": "…" } ], "next_cursor": "01JC97…" }
When next_cursor is null there are no more pages.
POST /jobs/:id/cancel
Cancels a job that is queued or running. Credits for work that has not produced output are released. Cancelling a finished job returns it unchanged.
curl -X POST https://aispritemaker.com/api/v1/jobs/01JC9B2M4F…/cancel \
-H "Authorization: Bearer $ASM_API_KEY"
POST /uploads
Reference images and footage are uploaded straight to storage in two steps: ask for an upload URL, then PUT the bytes to it.
content_type | Max size | Use for |
|---|---|---|
image/png, image/jpeg, image/webp | 10 MB | Character or style references, images to edit |
image/gif | 20 MB | video_to_sprite |
video/mp4, video/webm, video/quicktime | 50 MB | video_to_sprite |
# 1. Ask for an upload slot
curl https://aispritemaker.com/api/v1/uploads \
-H "Authorization: Bearer $ASM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content_type":"image/png","bytes":48213}'
# -> {"upload_id":"up_01JC…","put_url":"https://…"}
# 2. Send the file
curl -X PUT "$PUT_URL" -H "Content-Type: image/png" --data-binary @hero.png
import { readFile } from "node:fs/promises";
const file = await readFile("hero.png");
const { upload_id, put_url } = await api("/uploads", {
content_type: "image/png",
bytes: file.byteLength,
});
await fetch(put_url, { method: "PUT", headers: { "Content-Type": "image/png" }, body: file });
// now use { reference_upload_id: upload_id } in POST /jobs
data = open("hero.png", "rb").read()
up = requests.post(f"{API}/uploads", headers=H,
json={"content_type": "image/png", "bytes": len(data)}).json()
requests.put(up["put_url"], data=data, headers={"Content-Type": "image/png"})
# now use {"reference_upload_id": up["upload_id"]} in POST /jobs
Uploads that are never used expire automatically. Uploaded files are deleted 14 days after the job that used them finishes.
Rolling out: custom motions and server-side video conversion
video_to_sprite jobs accept GIF uploads today. MP4, WebM and MOV uploads are accepted by /uploads, but server-side conversion of them, and custom_motion jobs, depend on a video pipeline that is still being switched on. Until then those requests return 503 feature_unavailable and nothing is charged; watch the changelog. The free Video to Sprite Sheet tool converts video in your browser in the meantime.
GET /assets
Lists files your jobs produced, newest first, with fresh signed URLs. Query parameters: limit (1–100, default 20), cursor, and kind to filter (for example sheet or sprite). Use it to find a character_asset_id to reuse.
curl "https://aispritemaker.com/api/v1/assets?kind=sprite&limit=10" \
-H "Authorization: Bearer $ASM_API_KEY"
GET /assets/:id/file/:name
Downloads an output file. The url on each output already points here with a signature, so you normally just GET that URL. Without a valid signature, the request must carry your key. Responses are either the bytes or a redirect to a short-lived storage URL, so follow redirects (curl -L).
curl -L -o walk.png "https://aispritemaker.com/api/v1/assets/as_01JC9B…/file/sheet.png?sig=…"
API keys: GET, POST and DELETE /keys
List and revoke keys from code, for example when rotating them in CI. Creating a key (POST /keys) only works from a signed-in browser session in the dashboard, never with another API key; a key-authenticated POST gets 403 session_required. An account can have up to 20 active keys.
# list (only prefixes are returned, never full keys)
curl https://aispritemaker.com/api/v1/keys -H "Authorization: Bearer $ASM_API_KEY"
# revoke
curl -X DELETE https://aispritemaker.com/api/v1/keys/key_01JC… -H "Authorization: Bearer $ASM_API_KEY"
OpenAPI spec
The full API is described in an OpenAPI 3.1 document at https://aispritemaker.com/openapi.json: every endpoint, request body, response shape, enum (job types, styles, sizes) and error status. Import it into Postman, Insomnia or Bruno, or generate a typed client:
npx openapi-typescript https://aispritemaker.com/openapi.json -o aispritemaker.d.ts
Polling a job to completion
Jobs usually take from a few seconds (still sprites) to a couple of minutes (multi-direction animations). Poll every 2 seconds; polling is cheap and does not count against your credits.
JOB=01JC9B2M4F…
while :; do
STATUS=$(curl -s https://aispritemaker.com/api/v1/jobs/$JOB \
-H "Authorization: Bearer $ASM_API_KEY" | jq -r .job.status)
echo "$STATUS"
case $STATUS in succeeded|failed|canceled) break;; esac
sleep 2
done
async function waitForJob(id, { interval = 2000, timeout = 10 * 60_000 } = {}) {
const started = Date.now();
for (;;) {
const { job } = await api(`/jobs/${id}`);
if (job.status === "succeeded") return job;
if (job.status === "failed" || job.status === "canceled") {
throw new Error(`${job.status}: ${job.error?.code} ${job.error?.message ?? ""}`);
}
if (Date.now() - started > timeout) throw new Error("timed out");
await new Promise((r) => setTimeout(r, interval));
}
}
const done = await waitForJob(anim.id);
const sheet = done.outputs.find((o) => o.kind === "sheet");
console.log(sheet.url, sheet.meta);
import time
def wait_for_job(job_id, interval=2, timeout=600):
started = time.time()
while True:
job = requests.get(f"{API}/jobs/{job_id}", headers=H).json()["job"]
if job["status"] == "succeeded":
return job
if job["status"] in ("failed", "canceled"):
raise RuntimeError(f"{job['status']}: {job.get('error')}")
if time.time() - started > timeout:
raise TimeoutError(job_id)
time.sleep(interval)
done = wait_for_job(anim["id"])
sheet = next(o for o in done["outputs"] if o["kind"] == "sheet")
open("attack.png", "wb").write(requests.get(sheet["url"]).content)
Errors
Errors use standard HTTP status codes and a consistent body:
{ "error": { "code": "insufficient_credits", "message": "This needs 40 credits; you have 12." } }
Match on code, which is stable. message is for humans and may change. Validation errors also include an issues array with the field path and message for each problem.
| Status | code | What happened | What to do |
|---|---|---|---|
| 400 | invalid_json | Body is not valid JSON | Check quoting and Content-Type |
| 400 | invalid_request | A field is missing, the wrong type, or not allowed for this job type | Read issues; the table under POST /jobs lists required fields |
| 400 | unknown_motion | motion_id is not in the library | Fetch GET /motions |
| 400 | invalid_upload / unsupported_type | Wrong kind of file for this job, or an unsupported content type | Use PNG, JPEG or WebP for references and GIF or video for footage |
| 401 | unauthorized | No key or session | Send Authorization: Bearer asm_live_… |
| 401 | invalid_api_key | Key is wrong or revoked | Create a new key |
| 402 | insufficient_credits | Balance is lower than the job’s reservation | Wait for daily credits, buy a pack, or upgrade |
| 403 | plan_limit | size is above your plan’s maximum | Use a smaller size or upgrade |
| 404 | job_not_found / asset_not_found / upload_not_found / not_found | The ID does not exist or belongs to another account | Check the ID |
| 409 | upload_incomplete | The job references an upload whose bytes never arrived | PUT the file to put_url first |
| 413 | too_large | Upload exceeds the size limit | See the upload table |
| 429 | rate_limited | Too many requests | Back off and retry after a minute |
| 429 | concurrency_limit | Your plan’s parallel job limit is full | Wait for a running job to finish |
| 429 | too_many_uploads | More than 50 pending uploads | Use or let pending uploads expire |
| 500 | internal_error | Our bug | Retry with the same Idempotency-Key; email us if it keeps happening |
| 503 | feature_unavailable | The feature is temporarily off or not yet enabled | Retry later or use the alternative in the message |
A job can also fail after it was accepted, for example if moderation blocks the prompt or every model provider is down. Then GET /jobs/:id returns status: "failed" with an error object, and the reserved credits are already back in your balance.
Rate limits
| Limit | Free | Starter | Pro | Studio |
|---|---|---|---|---|
| API requests per key | 120 / min | 120 / min | 120 / min | 120 / min |
Job creations (POST /jobs) | 20 / min | 20 / min | 20 / min | 20 / min |
| Jobs running at the same time | 1 | 2 | 4 | 10 |
Maximum size | 128 px | 256 px | 512 px | 512 px |
| Priority queue | — | — | — | Yes |
Requests over a limit get 429 and cost nothing. Extra jobs beyond your concurrency are rejected rather than queued, so keep your own queue and submit the next job when one finishes. If you need higher limits for a production pipeline, email hello@aispritemaker.com with your expected volume.
Webhooks
Coming soon
Job-completion webhooks (an HTTPS POST to your URL, signed with HMAC-SHA256, when a job succeeds or fails) are in development. Until they ship, poll GET /jobs/:id as shown above. We will announce the payload format and signing details in the changelog before turning them on.
Using the API from AI agents
The same operations are exposed as tools on our MCP server, so Claude Code, Cursor, Codex and other MCP clients can generate sprites straight into your project. See the MCP server docs. A machine-readable summary of the product and API lives at /llms.txt.
Changelog and versioning
The API is versioned in the path (/api/v1). We add fields and endpoints without changing the version, so ignore fields you don’t recognise. Breaking changes get a new version, and the old one keeps working for at least 90 days. Changes are listed in the changelog.