SimpliGen Docs

Control API reference

The local HTTP API that SimpliGen exposes on your machine, for building custom integrations and running generations headless.

SimpliGen runs a small HTTP Control API on your machine while the app is open. It is the same API the MCP server talks to, exposed directly so you can drive SimpliGen from your own scripts, services, or agents, including with the app minimized to the tray.

MCP vs the Control API

If you just want an AI agent (Claude, Codex, etc.) to use SimpliGen, set up the MCP server instead, it wraps this API. Use the Control API directly when you are writing a custom integration in any language.

Base URL and discovery

The server binds to loopback only and defaults to:

http://127.0.0.1:48199

If that port is taken, SimpliGen falls back to an OS-assigned port. The current port (and API version) is always written to a discovery file in the app's user data directory:

# Windows
%APPDATA%\simpligen\agent-api.json
# macOS
~/Library/Application Support/simpligen/agent-api.json
# Linux
~/.config/simpligen/agent-api.json
agent-api.json
{ "port": 48199, "version": 1 }

Read this file to find the live port rather than hardcoding 48199.

Requirements

The Control API only runs when the app is open and the machine is entitled to agent access (it is part of your license). Requests are only served when the Host header is a loopback alias; anything else gets 403 forbidden_host (a DNS-rebinding guard).

Authentication

Every endpoint except GET /health requires a pairing token in the Authorization header:

Authorization: Bearer sg-agent-xxxxxxxx...

Create a token in the app under Settings → Connect an agent (see Connect an agent). The raw token is shown once; only a hash is stored. Each token can carry an optional spend cap for cloud generations. Revoking a token in the app takes effect immediately.

A missing or invalid token returns 401 unauthorized.

Conventions

  • Requests: JSON bodies (Content-Type: application/json), up to 50 MB.
  • Responses: always JSON.
  • Errors: non-2xx responses have the shape { "errorCode": "...", "message": "..." }.
  • IDs: presets are addressed by presetId (+ optional packId); jobs by jobId.

A minimal authenticated call:

TOKEN="sg-agent-..."
curl -s http://127.0.0.1:48199/status -H "Authorization: Bearer $TOKEN"

Health

GET /health

Liveness probe. No authentication.

{ "ok": true, "name": "simpligen", "version": 1 }

Capabilities

GET /capabilities

Lists every preset you can generate with, one entry per (preset, mediaType), with its inputs, options, local readiness, and cloud eligibility/cost.

{
  "capabilities": [
    {
      "packId": "local-core-pack",
      "presetId": "z-image-turbo",
      "mediaType": "image",
      "name": "Z-Image Turbo",
      "description": "Ultra-fast, high-quality images in seconds",
      "apiProvider": null,
      "inputs": {
        "acceptsPrompt": true,
        "requiresPrompt": true,
        "requiresImage": false,
        "acceptsReferenceImages": null,
        "acceptsVideo": false,
        "acceptsAudio": false,
        "acceptsReferenceVideos": false,
        "acceptsReferenceAudios": false,
        "supportsLoras": true
      },
      "options": {
        "steps": null,
        "cfg": null,
        "duration": null,
        "resolution": null,
        "aspectRatio": ["1:1", "3:4", "4:3", "9:16", "16:9"]
      },
      "cloudAliasTo": null,
      "localReady": true,
      "cloudEligible": true,
      "cloudCost": 4
    }
  ]
}
FieldMeaning
localReadyAll model files are installed; the preset can run locally now.
cloudEligibleThe preset can run on SimpliGen Cloud.
cloudCostApproximate credit cost for a cloud run (null if not eligible).
apiProviderNon-null for external-API presets (e.g. OpenAI, Google) which the Control API cannot dispatch.
inputs / optionsWhat the preset accepts and the option ranges/aspect ratios you may pass to POST /generate. steps/cfg are non-null only for presets that expose advanced controls.
promptFamilyThe prompting guide family for this preset (for example z-image), or null when none is declared. GET /prompt-guide/:presetId returns the guide.
inputs.requiresPromptfalse for presets whose instructions are built in (cutouts, head or outfit swaps, upscales): an empty prompt is valid, and any text you send is added as extra notes.

Status

GET /status

{
  "engineRunning": true,
  "engineVersion": "v0.26.0",
  "cloudConnected": true,
  "creditBalance": 1280,
  "activeProject": { "id": "default", "name": "My Project" },
  "characters": { "enabled": true, "base": 1, "edit": 1, "i2v": 1, "enhancePresets": 2 }
}

POST /engine/start

Starts the local engine when it is not already running. Takes no body.

{ "engineRunning": true, "engineVersion": "v0.37.0", "engineStarting": false }

A cold boot takes longer than the request waits. When that happens the reply is 202 with engineStarting: true and the engine keeps booting, so poll GET /status until engineRunning is true.

Starting an engine that already runs returns 200 with alreadyRunning: true and changes nothing. If no engine is installed the reply is 409 engine_not_installed: installing one is a multi gigabyte download and stays a job for the app, not the API. A machine the engine refuses to run on answers 409 engine_unsupported_hardware, which retrying will not fix.

POST /engine/stop

Stops the local engine and frees its VRAM. Cloud generation is unaffected. Stopping an engine that is already down is a success.

POST /engine/restart

Stops and starts it again, for a wedged engine or after a custom node was installed or removed. Same 202 behaviour as start when the boot outlasts the request.

Projects

Projects are the containers that hold generations.

GET /projects

{ "projects": [ { "id": "default", "name": "My Project", "slug": "my-project" } ] }

POST /projects

Request
{ "name": "Campaign assets" }

Returns 201 with { "project": { ... } }. name is required (400 invalid_options otherwise).

Uploads

Inputs (source images, reference images, source videos) are never passed as local paths. Upload the bytes first and pass the returned handle to POST /generate.

POST /uploads

Request
{ "filename": "ref.png", "dataBase64": "<base64 of the file bytes>" }
Response (201)
{ "handle": "a1b2c3d4.png", "sizeBytes": 51234 }

Max upload is 100 MB (413 asset_too_large). Unsupported extensions return 400 unsupported_file_type.

Generate

POST /generate

Enqueues a generation and returns immediately with a jobId. Poll GET /jobs/:id (see Jobs) for the result.

Request
{
  "presetId": "z-image-turbo",
  "packId": "local-core-pack",
  "mediaType": "image",
  "prompt": "a red fox in the snow, cinematic",
  "backend": "auto",
  "project": "default",
  "image": "a1b2c3d4.png",
  "referenceImages": [{ "handle": "e5f6.png", "label": "style" }],
  "loras": [
    { "name": "RoomLora", "weight": 0.85 },
    { "name": "Mia", "weight": 0.75 }
  ],
  "options": { "aspectRatio": "16:9", "steps": 8, "negativePrompt": "blurry, watermark" }
}
FieldRequiredNotes
presetIdyesFrom GET /capabilities.
mediaTypeyes"image" or "video".
packIdnoIf given, must match the preset's pack.
promptconditionalRequired unless the preset reports inputs.requiresPrompt: false. Those presets have their instructions built in: send "" to run them as they are, or add text to pass extra notes.
backendno"auto" (default), "local", or "cloud". Auto prefers local if installed, else cloud if connected.
imageconditionalUpload handle. Required for image-to-X presets (inputs.requiresImage).
videoconditionalUpload handle, for video-input presets.
referenceImagesconditionalArray of handles or { handle, label }, for reference-image presets.
audioconditionalUpload handle, for audio-driven presets (lip-sync, audio-to-video). Required when the preset reports acceptsAudio, otherwise the job is rejected with needs_audio.
audioStartSecondsnoWhere in the track to start, in seconds. Also accepted under options.
referenceVideosconditionalArray of handles or { handle, soundtrackMode, durationSeconds }, for reference-clip presets.
referenceAudiosconditionalArray of handles or { handle, durationSeconds }, for reference-sound presets.
lorasnoArray of { name, weight } to apply, for presets where inputs.supportsLoras is true. name matches a LoRA's display name (or filename) from GET /loras; weight defaults to the LoRA's default strength. Unknown names return 400 unknown_lora. Local backend only for now.
projectnoProject id or slug. Defaults to the active project.
options.aspectRationoMust be one of the preset's supported ratios.
options.steps / options.cfgnoWithin the preset's advertised ranges.
options.negativePromptnoExtra negative terms (the UI's Advanced Options negative). Also accepted at the top level as negativePrompt. Additive: the preset's default negative is always applied and your terms are appended after it (, ). Send nothing to use the preset default. On SimpliGen Cloud runs extra terms are not sent, and the preset's default negative is used as is.
options.durationSecondsnoVideo duration.
options.resolutionnoResolution tier label.
Response (201)
{ "jobId": "gen_abc123", "backend": "local", "status": "queued" }

For a cloud job the response also includes "cloudCost": <credits>.

Backends

External-API presets (where apiProvider is non-null, e.g. GPT Image 2, Veo) cannot be dispatched here, you will get 400 unsupported_backend. Use a local or SimpliGen Cloud preset. Cloud requires a connected account (400 cloud_not_connected); local requires the models installed (400 preset_not_installed, prepare them first).

GET /loras

Lists the user's installed LoRAs so agents can discover what to pass in POST /generate's loras array. Match on name (the display name).

Response
{
  "loras": [
    { "name": "RoomLora", "filename": "RoomLora_000002400.safetensors", "baseModel": "flux", "triggerWords": "a room", "defaultStrength": 0.9 },
    { "name": "Mia", "filename": "mia_v2.safetensors", "baseModel": "flux", "triggerWords": "mia", "defaultStrength": 1.0 }
  ]
}

Pass the LoRA where it fits the preset's base model (baseModel), and use its own trigger words in your prompt so it activates.

Prompt guides

GET /prompt-guide/:presetId

Returns the prompting guide for a preset: how its model reads a prompt, the structure and length it likes, camera and motion wording, negatives, how to word characters, and worked examples. Read it before writing the prompt for POST /generate. Guides are served by SimpliGen and cached, so they also work offline from the last copy of each guide that was opened before.

Response (200)
{
  "presetId": "z-image-turbo",
  "family": "z-image",
  "familyName": "Z-Image",
  "version": 1,
  "generic": false,
  "text": "# Z-Image prompting guide ...\n\n## Negative prompts in SimpliGen\n...\n\n## Notes for this preset\n\n..."
}
FieldMeaning
family / familyNameThe guide family the preset resolves to, and its display name (see promptFamily on GET /capabilities).
versionThe version of this family's guide.
generictrue when no model-specific guide exists for this preset. The text then says so plainly and gives only general advice.
textThe family guide in Markdown, followed by a ## Negative prompts in SimpliGen section that every guide shares. When the preset has notes of its own, a ## Notes for this preset section is appended.
HTTPerrorCodeWhen
404unknown_presetNo preset with that id.
503guides_unavailableThe guides could not be loaded and no cached copy exists: "Prompt guides could not be loaded. Check the internet connection and try again."

Jobs

GET /jobs/:id

{
  "job": {
    "jobId": "gen_abc123",
    "status": "completed",
    "backend": "local",
    "mediaType": "image",
    "presetId": "z-image-turbo",
    "projectId": "default",
    "resultUrl": "local-file:///C:/Users/you/.../output.png",
    "resultPath": "C:/Users/you/.../output.png",
    "errorMessage": null,
    "createdAt": "2026-06-25T20:00:00.000Z",
    "completedAt": "2026-06-25T20:00:08.000Z"
  }
}

status is one of queued, processing, completed, failed. On success, resultPath is the decoded local file path; on failure, errorMessage explains why. Unknown id returns 404 job_not_found.

GET /jobs

Returns the 25 most recent jobs as { "jobs": [ ...job... ] }.

POST /jobs/:id/cancel

Cancels a queued/running job. Returns { "jobId", "status", "note?" }. A cloud job already in flight may still complete on the server (noted in note).

Preparing presets

Local presets download their model files on first use. You can prepare them ahead of time.

POST /presets/prepare

Request
{ "presetId": "z-image-turbo", "mediaType": "image" }

Returns 200 { "status": "already_installed" } or 202 { "status": "started" } (download runs in the background).

GET /presets/prepare

Returns in-flight downloads: { "downloads": [ ... ] }. Poll this, or GET /capabilities (watch localReady), until the preset is ready.

Characters

Character Studio is scriptable too. Characters are local-only (cloud is rejected). See Characters for the concepts.

EndpointPurpose
GET /charactersList characters.
GET /characters/:idGet one character.
GET /character-presetsInstalled character presets (base / edit / i2v).
GET /character-featuresAvailable identity/appearance feature options.
POST /charactersCreate a character (mode: "generate" from a description, or "upload" from an uploaded base image handle).
POST /characters/:id/generate-imageGenerate an image of the character ({ prompt, aspect?, count?, presetId? }).
POST /characters/:id/generate-frameGenerate a single video start frame.
POST /characters/:id/animateAnimate the character into a video.
POST /characters/:id/presetSet the character's default preset.
POST /characters/:id/enhanceUpscale or enhance a character asset. Name the source with exactly one of target: "base", jobId (a previous generation), or handle (from POST /uploads). Optional: mediaType, presetId, scale, keepResolution, applyAsBase.

All generation endpoints return a jobId you poll via GET /jobs/:id, exactly like POST /generate.

Recipes

Author multi-step recipes (flows) out of packs and presets. Discover the building blocks, compose from a high-level spec, then optionally publish.

EndpointPurpose
GET /recipe-blocksEntry points + every preset usable as a step, with its port contract.
POST /recipes/validateDry-run a spec (compose+validate) or a config, no save. Returns validation.
POST /recipesCompose from a spec and save a draft. Returns { recipeId, valid, errors, config }.
GET /recipesList every recipe on this device (installed from the Store or authored here) with its inputs (key, kind, label, required, options) and step count.
GET /recipes/:idGet one recipe, its full config, its inputs, and readiness (per step: which preset it runs and whether it is installed locally).
POST /recipes/:id/runRun a recipe. Body: { inputs: { key: value }, engine?: "local" | "cloud" | "auto" }. Subject inputs take a character or product id, image/video/audio inputs take upload handles, everything else takes plain values. Returns { runId, status: "running", engine, steps, cloudCost? }. Cloud runs are priced up front and count against the token's spend cap.
GET /recipes/:id/runsRecent runs of a recipe, newest first.
GET /recipe-runs/:idStatus of a run: status (running, completed, failed, cancelled), steps (each with status, job id and result path) and resultPath once the last media step completes.
POST /recipe-runs/:id/cancelStop a run. No further steps start; a local in-flight step is interrupted.
POST /recipes/:id/publishSubmit to the Creator Marketplace (needs a connected account).

Products and subjects

A recipe's subject input takes either a character or a product. Products are the other subject type: a named thing with a base photo, used by Product Studio and UGC recipes.

EndpointPurpose
GET /subjectsEvery subject a recipe can take: characters and products (id, type, name, status).
GET /productsList products.
GET /products/:idGet one product.
POST /productsCreate a product from an uploaded photo: { name, image: <upload handle>, description?, category? }. The description and category are woven into recipe prompts. Returns { productId, product }.
DELETE /products/:idRemove a product and its photo.

A spec is a high-level description; the app auto-wires ports, the step-to-step media chain, and a prompt input per step, then validates.

POST /recipes request
{
  "spec": {
    "name": "Product to spinning video",
    "category": "product",
    "steps": [
      { "preset": "krea2-pack:krea2-turbo" },
      { "preset": "wan-2.2-video-pack:wan-2.2-i2v" }
    ]
  }
}
Response (201)
{
  "recipeId": "b1f2…",
  "valid": true,
  "errors": [],
  "config": { "blocks": [ "…" ], "inputs": [ "…" ], "output": { "from": "n2.video" } }
}

Composing and saving needs no creator account. POST /recipes/:id/publish requires a connected account and returns not_connected (with needsConnect: true) until the user connects one in the app; the same draft then publishes unchanged. Every preset the recipe uses must belong to a distributable first-party pack.

Video Studio

Films are Video Studio documents: clips (shots to render) on a timeline over audio tracks. Every editing route answers { change, timeline }: what changed, and the film's timeline after it (the same shape GET /films/:id answers). With the film open in the studio the change shows live and undoes with Ctrl+Z; a closed film is edited and saved.

Clips are named in the path by id, by name, or by number (their order on the timeline, earliest first). Times are seconds, or bars once the film has a tempo: a position is 3, { "seconds": 3 } or { "bar": 5 } (bar 1 is the first downbeat), a length is 5, { "seconds": 5 } or { "bars": 2 }.

RouteWhat it does
GET /films?projectId=Films of a project (default: the current one).
POST /filmsCreate a film: name, optional projectId, aspect (16:9, 9:16, 1:1), quality (480p, 720p, 1080p), renderOn (local, cloud), presets ({ t2v, i2v, r2v }). Answers the timeline.
GET /films/:idThe timeline.
PATCH /films/:idFilm settings: aspect, quality, hires2x, renderOn, presets, subject, cast (names or ids), location (name, id or null).
POST /films/:id/tracksAdd a track: upload (a handle from POST /uploads), kind (singing, music, dialogue, effects), name, at.
PATCH /films/:id/tracks/:trackat, muted, volume (0 to 1).
POST /films/:id/tempoDetect the tempo from the longest unmuted track, or track. Answers { bpm, offset, track, timeline }.
POST /films/:id/clipsAdd a clip: at, length, name, lane.
PATCH /films/:id/clips/:clipshots (each { prompt, seconds, line, speaker, framing }), cast, location ("none" drops the film's, "film" inherits it again), link (fresh, continue, motion; none and frame are also accepted, as fresh and continue), hears ([{ track, on }]), preset.
POST /films/:id/clips/:clip/moveat, lane.
POST /films/:id/clips/:clip/trimlength, trimIn (where the clip starts inside its take; 0 is the start of the take).
DELETE /films/:id/clips/:clipRemove the clip.
POST /films/:id/planLay out a draft (below).
POST /films/:id/fadestarget (a clip or a track), fadeIn, fadeOut.
POST /films/:id/markersat, name.
POST /films/:id/export-rangefrom and to (both needed), or clear: true.
POST /films/:id/rendersRender clips ("stale", the default, or a list). Answers 202 with { started, held, credits } at once, or 200 with a note when nothing needs a render.
GET /films/:id/clips/:clip/takesThe clip's takes, newest first.
POST /films/:id/clips/:clip/takegenerationId: make that take the one the clip shows.
POST /films/:id/exportStitch the film. Answers 201 with { path, resultUrl, blanks }.

Laying out a draft

POST /films/:id/plan
{
  "mode": "append",
  "cast": ["Kai", "Mira"],
  "location": "Rooftop",
  "clips": [
    { "bars": 2, "shots": [{ "prompt": "@Kai looks over the city", "framing": "wide" }] },
    { "bars": 2, "link": "continue",
      "shots": [{ "prompt": "@Kai sings to camera", "line": "Neon rain", "speaker": "Kai", "framing": "close-up" }] },
    { "bars": 3, "location": "Neon Street", "cast": ["Mira"],
      "shots": [{ "prompt": "@Mira walks through the rain", "seconds": 4 }, { "prompt": "she turns", "seconds": 3 }] }
  ]
}

The plan is checked as a whole first. A plan with problems answers 400 invalid_plan with every problem in details.problems (each prefixed with its clip, such as clip 2: ...) and changes nothing. mode: "replace" replaces every clip and is refused when clips have takes unless "discardTakes": true.

Renders and the spend cap

A cloud render is priced before anything is queued and counts against the token's spend cap: over the cap answers 402 exceeds_spend_cap, an unknown price 503 cost_unknown. Clips that continue from the clip before render one at a time, each after the take it starts from; the call answers at once, and GET /films/:id shows each clip's take: none, rendering, current, edited, longer, unknown (it cannot be told yet) or imported (a video brought in, never rendered).

Locations

RouteWhat it does
GET /locationsThe Library's locations: id, name, description, hasPicture.
POST /locationsCreate a location from its picture: name, description, and upload (a handle) or fromJob (a finished image job).

Spend caps

When a token has a spend cap, cloud generations are priced before dispatch and rejected if they would exceed the remaining allowance (402 exceeds_spend_cap). If the cost can't be determined (cloud status unavailable), a capped token fails closed with 503 cost_unknown. Local generations are always free of credit cost.

A complete flow

BASE=http://127.0.0.1:48199
AUTH="Authorization: Bearer sg-agent-..."

# 1. Pick a ready preset
curl -s $BASE/capabilities -H "$AUTH"

# 2. Generate
JOB=$(curl -s $BASE/generate -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"presetId":"z-image-turbo","mediaType":"image","prompt":"a red fox in the snow","options":{"aspectRatio":"16:9"}}' \
  | jq -r .jobId)

# 3. Poll until done, then read resultPath
curl -s $BASE/jobs/$JOB -H "$AUTH" | jq '.job | {status, resultPath, errorMessage}'

Error codes

HTTPerrorCodeWhen
400invalid_jsonBody is not valid JSON.
400invalid_optionsA required field is missing or malformed.
400invalid_presetPreset/pack/mediaType mismatch.
400needs_*A required input (prompt, image, references) is missing.
400invalid_handleAn input was not a valid upload handle.
400unsupported_backendPreset runs on an external API.
400preset_not_installedLocal backend chosen but models aren't installed.
400cloud_not_connectedCloud backend chosen but no account is connected.
400invalid_planA film plan has problems; details.problems lists them all.
400no_tempoA time in bars before the film has a tempo; detect the tempo first.
400no_audioTempo asked of a film with no audio track.
400no_pulseNo steady beat could be heard in the track.
401unauthorizedMissing or invalid token.
402exceeds_spend_capCloud cost exceeds the token's spend cap.
402exceeds_balanceThe cloud balance does not cover the renders.
403forbidden_hostNon-loopback Host header.
403studio_not_installedThe Video Studio is not installed on this computer.
404not_found / invalid_preset / job_not_foundUnknown route, preset, or job.
404unknown_presetGET /prompt-guide/:presetId with an id that is not a preset.
404film_not_foundNo film with that id.
404clip_not_foundNo clip by that id, name or number.
404track_not_foundNo track by that name or id.
404cast_not_foundNo character by that name or id in the Library.
404location_not_foundNo location by that name or id, or it has no picture yet.
409clips_not_readyExport asked while clips still need a render; details.clips names them.
413asset_too_largeUpload over 100 MB.
503guides_unavailableThe prompt guides could not be loaded and none are cached.
503cost_unknownCapped token; cloud cost couldn't be determined.
503studio_busyThe SimpliGen window could not be reached or did not answer in time. If it timed out, the change may still have been made: read the film before trying again.
500internal_errorUnexpected server error.