API reference
The Sula API lets you generate images and video from your own code, or from Claude, using an API key. It's a small JSON API over HTTPS.
Base URL: https://your-sula-host (while self-hosted in dev this is http://127.0.0.1:4790).
Authentication
Create a key in the app under API keys. Send it as a Bearer token on every request. Keys are shown once, store them like a password; anyone with a key spends your credits.
Authorization: Bearer sula_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Revoke a key any time in the app; revoked keys immediately stop working.
Quickstart
Generate an image (returns synchronously):
curl -X POST $BASE/api/generate \
-H "Authorization: Bearer $SULA_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"flux-2-pro","prompt":"a matte black water bottle on beige, studio light"}'
{ "id":"a1b2…", "state":"done", "kind":"image",
"file":"a1b2….jpg", "credits":{"image":14,"main":0} }
Download the result from $BASE/media/<file> (same Bearer key).
POST /api/generate
Create a generation. Images return state:"done" immediately; videos return state:"running" with an id you poll.
Body
| Field | Type | Notes |
|---|---|---|
model | string | Required. See models. |
prompt | string | Required. For talking video, put the spoken line in "quotes". |
aspect_ratio | string | Optional. e.g. "9:16" (default), "16:9", "1:1". |
enhance | bool | Optional, images only, default true. Short prompts get expanded with photographic detail before generating; the response then carries enhanced: true and prompt_used. Pass false for literal prompts. |
Python
import requests, os, time
BASE, KEY = os.environ["BASE"], os.environ["SULA_KEY"]
h = {"Authorization": f"Bearer {KEY}"}
r = requests.post(f"{BASE}/api/generate", headers=h,
json={"model":"veo-3.1-fast",
"prompt":'a woman holding a pink bottle, she says: "you need these"',
"aspect_ratio":"9:16"}).json()
gid = r["id"]
while r.get("state") == "running":
time.sleep(4)
r = requests.get(f"{BASE}/api/status", headers=h, params={"id":gid}).json()
print("done:", f"{BASE}/media/{r['file']}")
Check the model is callable before you build against it. Any model whose
Status is not live returns
503 and is not charged, so the example above will 503 while its provider
key is missing.
POST /api/tool
Run an on-device edit tool on one of your generations (pass a generation id as source). Image tools return a new image synchronously; captionrm-video is a video tool that runs asynchronously, the response is {"state":"running","id":…}; poll /api/status?id=… until done.
| op | Credits | Source | Does |
|---|---|---|---|
upscale | 3 | image | 4× super-resolution |
captionrm-video | 20 | video | Erase burned-in subtitles from a clip, on-device LaMa inpaint, keeps the original audio. Async. |
curl -X POST $BASE/api/tool \
-H "Authorization: Bearer $SULA_KEY" \
-H "Content-Type: application/json" \
-d '{"op":"upscale","source":"<image-id>"}'
GET /api/status?id=<id>
Poll a video job. Returns {"state":"running"}, then {"state":"done","file":"…mp4"}, or {"state":"failed"} (failed jobs are auto-refunded).
GET /api/models
Public, no key needed. Lists models, their credit cost, live status, and the plan tiers. This is the source of truth for pricing: the table below is built from it on every page load, and you should read it the same way rather than hardcoding costs.
Per model: credits is the cost of one request, or of one block when
per_block_s is set. live is whether it is callable now, and
status says why when it is not; video models report
"live":false until the video provider is connected. Alongside the list,
video_available reports whether video as a whole is currently accepting
jobs, with video_reason when it is not.
History & media
GET /api/history, your last 100 generations.
GET /media/<file>, download a generation (auth required; you can only fetch your own files).
GET /api/me
Returns your email and credit balances: {"email":…, "credits":{"image":14,"main":0}}. The image pool is your free signup credits (images only); main is paid credits (any model).
Models & credits
Every row below is rendered from GET /api/models when this page loads, so it
is whatever the API will charge you at the moment you read it. Nothing here is copied
into the page by hand.
Rendering the table needs JavaScript. With JavaScript off, read the same list straight from the endpoint, which needs no API key:
curl -s $BASE/api/models
Each entry carries id, kind,
credits, live and status. Treat
credits from that response as the price, and
live as whether you can call it right now.
Use Sula from Claude (MCP)
Sula speaks MCP two ways, so Claude can generate for you in plain English ("make me a talking ad for my serum in Jessica's voice") using your API key and your credits. The simplest is the built-in HTTP endpoint, no extra process:
claude mcp add --transport http sula http://127.0.0.1:4790/mcp \
--header "Authorization: Bearer sula_your_key"
Setup for Cursor and other clients is on the MCP page. Prefer a local stdio server? That ships too:
Claude Code (stdio)
claude mcp add sula \
-e SULA_API_KEY=sula_your_key \
-- python3 /path/to/sula_mcp.py
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"sula": {
"command": "python3",
"args": ["/path/to/sula_mcp.py"],
"env": { "SULA_API_KEY": "sula_your_key" }
}
}
}
Claude then gets four tools: sula_generate_image (FLUX or Nano Banana), sula_talking_ad (Veo person, your voice pick, 8–32s), sula_remove_captions (clean a local clip), and sula_credits. Results are saved to your Downloads folder.
Errors
| Code | Meaning |
|---|---|
401 | Missing/invalid/revoked key. |
400 | Bad request (missing prompt or unknown model). |
402 | Not enough credits. Response includes your current balances. |
503 | Model temporarily unavailable (e.g. video before the provider is connected). |