# Control API reference (/docs/agents/api-reference)



SimpliGen runs a small HTTP **Control API** on your machine while the app is
open. It is the same API the [MCP server](/docs/agents/mcp-reference) talks to,
exposed directly so you can drive SimpliGen from your own scripts, services, or
agents, including with the app minimized to the tray.

<Callout type="info" title="MCP vs the Control API">
  If you just want an AI agent (Claude, Codex, etc.) to use SimpliGen, set up the
  [MCP server](/docs/agents/connect-an-agent) instead, it wraps this API. Use the
  Control API directly when you are writing a custom integration in any language.
</Callout>

## Base URL and discovery [#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
```

```json title="agent-api.json"
{ "port": 48199, "version": 1 }
```

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

<Callout type="info" title="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).
</Callout>

## Authentication [#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](/docs/agents/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 [#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:

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

***

## Health [#health]

### `GET /health` [#get-health]

Liveness probe. &#x2A;*No authentication.**

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

## Capabilities [#capabilities]

### `GET /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.

```json
{
  "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,
        "requiresImage": false,
        "acceptsReferenceImages": null,
        "acceptsVideo": 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
    }
  ]
}
```

| Field                | Meaning                                                                                                                                                                  |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `localReady`         | All model files are installed; the preset can run locally now.                                                                                                           |
| `cloudEligible`      | The preset can run on SimpliGen Cloud.                                                                                                                                   |
| `cloudCost`          | Approximate credit cost for a cloud run (null if not eligible).                                                                                                          |
| `apiProvider`        | Non-null for external-API presets (e.g. OpenAI, Google) which the Control API **cannot** dispatch.                                                                       |
| `inputs` / `options` | What 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. |

## Status [#status]

### `GET /status` [#get-status]

```json
{
  "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 }
}
```

## Projects [#projects]

Projects are the containers that hold generations.

### `GET /projects` [#get-projects]

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

### `POST /projects` [#post-projects]

```json title="Request"
{ "name": "Campaign assets" }
```

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

## Uploads [#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` [#post-uploads]

```json title="Request"
{ "filename": "ref.png", "dataBase64": "<base64 of the file bytes>" }
```

```json title="Response (201)"
{ "handle": "a1b2c3d4.png", "sizeBytes": 51234 }
```

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

## Generate [#generate]

### `POST /generate` [#post-generate]

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

```json title="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" }
}
```

| Field                           | Required    | Notes                                                                                                                                                                                                                                                                                |
| ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `presetId`                      | yes         | From `GET /capabilities`.                                                                                                                                                                                                                                                            |
| `mediaType`                     | yes         | `"image"` or `"video"`.                                                                                                                                                                                                                                                              |
| `packId`                        | no          | If given, must match the preset's pack.                                                                                                                                                                                                                                              |
| `prompt`                        | conditional | Required when the preset needs a prompt.                                                                                                                                                                                                                                             |
| `backend`                       | no          | `"auto"` (default), `"local"`, or `"cloud"`. Auto prefers local if installed, else cloud if connected.                                                                                                                                                                               |
| `image`                         | conditional | Upload handle. Required for image-to-X presets (`inputs.requiresImage`).                                                                                                                                                                                                             |
| `video`                         | conditional | Upload handle, for video-input presets.                                                                                                                                                                                                                                              |
| `referenceImages`               | conditional | Array of handles or `{ handle, label }`, for reference-image presets.                                                                                                                                                                                                                |
| `loras`                         | no          | Array 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. |
| `project`                       | no          | Project `id` or `slug`. Defaults to the active project.                                                                                                                                                                                                                              |
| `options.aspectRatio`           | no          | Must be one of the preset's supported ratios.                                                                                                                                                                                                                                        |
| `options.steps` / `options.cfg` | no          | Within the preset's advertised ranges.                                                                                                                                                                                                                                               |
| `options.negativePrompt`        | no          | Negative prompt (the UI's Advanced Options negative). Also accepted at the top level as `negativePrompt`.                                                                                                                                                                            |
| `options.durationSeconds`       | no          | Video duration.                                                                                                                                                                                                                                                                      |
| `options.resolution`            | no          | Resolution tier label.                                                                                                                                                                                                                                                               |

```json title="Response (201)"
{ "jobId": "gen_abc123", "backend": "local", "status": "queued" }
```

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

<Callout type="info" title="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).
</Callout>

### `GET /loras` [#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).

```json title="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.

## Jobs [#jobs]

### `GET /jobs/:id` [#get-jobsid]

```json
{
  "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` [#get-jobs]

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

### `POST /jobs/:id/cancel` [#post-jobsidcancel]

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 [#preparing-presets]

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

### `POST /presets/prepare` [#post-presetsprepare]

```json title="Request"
{ "presetId": "z-image-turbo", "mediaType": "image" }
```

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

### `GET /presets/prepare` [#get-presetsprepare]

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

## Characters [#characters]

Character Studio is scriptable too. Characters are **local-only** (cloud is
rejected). See [Characters](/docs/characters) for the concepts.

| Endpoint                              | Purpose                                                                                                       |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `GET /characters`                     | List characters.                                                                                              |
| `GET /characters/:id`                 | Get one character.                                                                                            |
| `GET /character-presets`              | Installed character presets (base / edit / i2v).                                                              |
| `GET /character-features`             | Available identity/appearance feature options.                                                                |
| `POST /characters`                    | Create a character (`mode: "generate"` from a description, or `"upload"` from an uploaded base image handle). |
| `POST /characters/:id/generate-image` | Generate an image of the character (`{ prompt, aspect?, count?, presetId? }`).                                |
| `POST /characters/:id/generate-frame` | Generate a single video start frame.                                                                          |
| `POST /characters/:id/animate`        | Animate the character into a video.                                                                           |
| `POST /characters/:id/preset`         | Set the character's default preset.                                                                           |
| `POST /characters/:id/enhance`        | Upscale/enhance a character asset.                                                                            |

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

## Recipes [#recipes]

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

| Endpoint                    | Purpose                                                                                |
| --------------------------- | -------------------------------------------------------------------------------------- |
| `GET /recipe-blocks`        | Entry points + every preset usable as a step, with its port contract.                  |
| `POST /recipes/validate`    | Dry-run a `spec` (compose+validate) or a `config`, no save. Returns `validation`.      |
| `POST /recipes`             | Compose from a `spec` and save a draft. Returns `{ recipeId, valid, errors, config }`. |
| `GET /recipes`              | List saved recipe drafts.                                                              |
| `GET /recipes/:id`          | Get one saved recipe and its full config.                                              |
| `POST /recipes/:id/publish` | Submit to the Creator Marketplace (needs a connected account).                         |

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.

```json title="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" }
    ]
  }
}
```

```json title="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.

## Spend caps [#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 [#a-complete-flow]

```bash
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 [#error-codes]

| HTTP | `errorCode`                                      | When                                                     |
| ---- | ------------------------------------------------ | -------------------------------------------------------- |
| 400  | `invalid_json`                                   | Body is not valid JSON.                                  |
| 400  | `invalid_options`                                | A required field is missing or malformed.                |
| 400  | `invalid_preset`                                 | Preset/pack/mediaType mismatch.                          |
| 400  | `needs_*`                                        | A required input (prompt, image, references) is missing. |
| 400  | `invalid_handle`                                 | An input was not a valid upload handle.                  |
| 400  | `unsupported_backend`                            | Preset runs on an external API.                          |
| 400  | `preset_not_installed`                           | Local backend chosen but models aren't installed.        |
| 400  | `cloud_not_connected`                            | Cloud backend chosen but no account is connected.        |
| 401  | `unauthorized`                                   | Missing or invalid token.                                |
| 402  | `exceeds_spend_cap`                              | Cloud cost exceeds the token's spend cap.                |
| 403  | `forbidden_host`                                 | Non-loopback `Host` header.                              |
| 404  | `not_found` / `invalid_preset` / `job_not_found` | Unknown route, preset, or job.                           |
| 413  | `asset_too_large`                                | Upload over 100 MB.                                      |
| 503  | `cost_unknown`                                   | Capped token; cloud cost couldn't be determined.         |
| 500  | `internal_error`                                 | Unexpected server error.                                 |
