`** (size it with `width`/`height`/
`aspect-ratio` inline or via your own class). `data-status`: `loading` ·
`loaded` · `error` (omitted ⇒ the image shows).
* **`
`** — `data-fit`: `cover` (default) ·
`contain`. Always provide `alt`.
* optional **`
`** — shown (overlay) only
when `data-status="error"`.
```html
Image unavailable
```
***
## React parity
`@civitai/components-react` renders exactly this markup. The
`html-vs-react-parity` browser test asserts `getComputedStyle()` is identical
between hand-written HTML (per this doc) and the React components, in both
themes — so this contract is executable, not aspirational.
---
---
url: /orchestration/recipes/3d.md
---
# 3D model generation
The `polyGen` step turns a text prompt or a reference image into a downloadable 3D model. Three engines are available, picked with the `engine` + `model` discriminator pair:
| `engine` / `model` | Best for | Notes |
|--------------------|----------|-------|
| `comfy` / `hunyuan3D` | **Image → 3D on Civitai's own workers** | Tencent Hunyuan3D (open model, v2 / v2.1 / v2-mini). `imageTo3D` plus a `shapeGen` → `texGen` split. Lowest Buzz cost and the most knobs (steps, cfg, sampler, octree). Image-only; no rigging. |
| `fal` / `meshy` | Text-to-3D, rigging & animation | Meshy v6, FAL-hosted. `textTo3D` **and** `imageTo3D`. GLB + FBX + preview thumbnail, optionally a rigged, animation-ready character with walk/run clips. The only text-capable engine. |
| `fal` / `tripo` | Fast hosted image → 3D with fine texture control | Tripo v2.5, FAL-hosted. `imageTo3D` only. GLB (or FBX with `quad`) + preview image. `HD`/PBR texturing and quad topology. |
**Default choice for new integrations**: for image-to-3D, lead with `engine: "comfy", model: "hunyuan3D"` — it runs on Civitai's own Comfy workers, costs the least, and exposes the most control. Reach for `model: "meshy"` when you need a text prompt, rigging, or animation (its only home), and `model: "tripo"` for fast hosted image-to-3D with quad topology or HD/PBR textures.
Every generation returns a GLB in the step's `output.model`. 3D generation runs for **minutes**, not seconds — always submit with `wait=0` and either poll `GetWorkflow` or register a webhook (see [Results & webhooks](/orchestration/guide/results-and-webhooks)).
## Prerequisites
* A Civitai orchestration token ([Quick start → Prerequisites](/orchestration/guide/getting-started#prerequisites))
* For any `imageTo3D` call: a publicly reachable source image URL (a Civitai CDN URL or your own hosted image)
## Meshy — text-to-3D
Meshy's text path generates geometry and textures from a prompt alone. `mode: "preview"` returns untextured geometry fast for iteration; `mode: "full"` (default) adds textures.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "polyGen",
"input": {
"engine": "fal",
"model": "meshy",
"operation": "textTo3D",
"prompt": "a low-poly viking helmet with engraved runes",
"mode": "full",
"topology": "triangle",
"targetPolycount": 30000
}
}]
}
```
## Meshy — image-to-3D
Reconstruct a model from a single reference image. Set `shouldTexture: false` for a bare mesh.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "polyGen",
"input": {
"engine": "fal",
"model": "meshy",
"operation": "imageTo3D",
"imageUrl": "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd4b4ad5-040f-4f0e-baa3-6e1ff00add65/original=true,quality=90,optimized=true/26781018.jpeg",
"shouldTexture": true,
"topology": "quad",
"targetPolycount": 50000,
"enablePbr": true
}
}]
}
```
### Rigging & animation (Meshy only)
Set `enableRigging: true` to get an armature-bound character plus basic walk/run clips; add `enableAnimation: true` for a full animated take. These populate the extra `output` blobs described in [Reading the result](#reading-the-result). Works best on a full-body humanoid subject in an A-pose.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "polyGen",
"input": {
"engine": "fal",
"model": "meshy",
"operation": "textTo3D",
"prompt": "a friendly cartoon wizard in robes, full body, A-pose",
"mode": "full",
"topology": "quad",
"targetPolycount": 60000,
"enablePbr": true,
"enableRigging": true
}
}]
}
```
### Meshy parameters
| Field | Required | Default | Notes |
|-------|----------|---------|-------|
| `operation` | ✅ | — | `textTo3D` or `imageTo3D`. |
| `prompt` | `textTo3D` only | — | Max 600 chars. |
| `imageUrl` | `imageTo3D` only | — | Public source image URL. |
| `mode` | | `full` | `textTo3D` only. `preview` = untextured geometry (fast), `full` = textured. |
| `enablePromptExpansion` | | `false` | `textTo3D` only. Expands the prompt with an LLM before generation. |
| `shouldTexture` | | `true` | `imageTo3D` only. `false` returns a bare mesh. |
| `topology` | | `triangle` | `triangle` or `quad`. |
| `targetPolycount` | | `30000` | Range `100`–`300000`. |
| `symmetryMode` | | `auto` | `off`, `auto`, or `on`. |
| `shouldRemesh` | | `true` | Remesh to the target topology/polycount. |
| `enablePbr` | | `false` | Generate PBR (metallic/roughness) texture maps. |
| `texturePrompt` | | — | Optional text guidance for texturing (max 600 chars). |
| `enableRigging` | | `false` | Produce a rigged character + basic walk/run animations. |
| `enableAnimation` | | `false` | Produce an animated GLB/FBX. |
| `seed` | | random | Reproducibility. Assigned automatically when omitted. |
## Tripo — image-to-3D
::: info Rolling out
Tripo v2.5 is newly added. The interactive runner below works once it's live in production; until then it returns a `400` for discriminator value `tripo`.
:::
Tripo reconstructs a model from a single image with fine control over texturing. `texture` picks the fidelity (`no`/`standard`/`HD`); `pbr` adds PBR maps; `quad` switches the mesh to quad topology (and produces FBX instead of GLB).
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "polyGen",
"input": {
"engine": "fal",
"model": "tripo",
"imageUrl": "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd4b4ad5-040f-4f0e-baa3-6e1ff00add65/original=true,quality=90,optimized=true/26781018.jpeg",
"texture": "HD",
"pbr": true,
"orientation": "align_image"
}
}]
}
```
Quad topology yields cleaner edge loops for downstream editing. Because Tripo emits FBX (not GLB) in quad mode, read the result from `output.fbxModel` rather than `output.model`:
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "polyGen",
"input": {
"engine": "fal",
"model": "tripo",
"imageUrl": "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd4b4ad5-040f-4f0e-baa3-6e1ff00add65/original=true,quality=90,optimized=true/26781018.jpeg",
"texture": "standard",
"quad": true,
"autoSize": true
}
}]
}
```
### Tripo parameters
| Field | Required | Default | Notes |
|-------|----------|---------|-------|
| `imageUrl` | ✅ | — | Public source image URL. |
| `texture` | | `standard` | `no` (mesh only), `standard`, or `HD`. |
| `pbr` | | `false` | Generate PBR maps. Implies a textured result even when `texture: "no"`. |
| `faceLimit` | | adaptive | Cap the output face count. Range `1000`–`500000`. |
| `autoSize` | | `false` | Scale the model to real-world dimensions (metres). |
| `quad` | | `false` | Quad topology. **Produces an FBX mesh instead of GLB** (read `output.fbxModel`). |
| `textureAlignment` | | `original_image` | `original_image` or `geometry`. |
| `orientation` | | `default` | `align_image` auto-rotates the model to match the input image. |
| `seed` | | random | Geometry reproducibility. Assigned automatically when omitted. |
| `textureSeed` | | random | Texture reproducibility. |
## Hunyuan3D — image-to-3D
Tencent's open Hunyuan3D running on Civitai's own Comfy workers — the cheapest 3D path and the one with the most control. Image-only. The default `imageTo3D` operation generates the mesh and (when `shouldTexture` is true) textures it in one submission. Pick fidelity with `modelVersion`:
| `modelVersion` | Notes |
|----------------|-------|
| `v2` | **Default.** Fast — 5 steps, 3072 shape resolution. |
| `v2.1` | Highest quality — 30 steps, 4096 shape resolution. |
| `v2-mini` | Smallest/fastest variant, same defaults as `v2`. |
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "polyGen",
"input": {
"engine": "comfy",
"model": "hunyuan3D",
"operation": "imageTo3D",
"modelVersion": "v2.1",
"imageUrl": "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd4b4ad5-040f-4f0e-baa3-6e1ff00add65/original=true,quality=90,optimized=true/26781018.jpeg",
"shouldTexture": true,
"octreeResolution": 256
}
}]
}
```
`v2-mini` is the quickest way to preview a mesh:
### Split shapeGen / texGen (advanced)
The `imageTo3D` operation runs shape then texture internally. To cache or reuse an untextured mesh — or to re-texture one mesh several times — run the two operations as separate steps and reference the shape output from the texture step. `mesh` takes a step `$ref` to the shape step's GLB:
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"steps": [
{
"$type": "polyGen",
"name": "shape",
"input": {
"engine": "comfy",
"model": "hunyuan3D",
"operation": "shapeGen",
"modelVersion": "v2.1",
"imageUrl": "https://.../reference.jpg"
}
},
{
"$type": "polyGen",
"name": "texture",
"input": {
"engine": "comfy",
"model": "hunyuan3D",
"operation": "texGen",
"modelVersion": "v2.1",
"imageUrl": "https://.../reference.jpg",
"mesh": { "$ref": "shape", "path": "output.model.url" },
"enablePbr": true
}
}
]
}
```
### Hunyuan3D parameters
| Field | Required | Default | Notes |
|-------|----------|---------|-------|
| `operation` | | `imageTo3D` | `imageTo3D` (mesh + texture), `shapeGen` (mesh only), or `texGen` (texture an existing mesh). |
| `modelVersion` | | `v2` | `v2`, `v2.1`, or `v2-mini`. |
| `imageUrl` | ✅ | — | Public source image URL. |
| `prompt` | | — | Optional guidance for `imageTo3D`/`shapeGen` (max 600 chars). |
| `mesh` | `texGen` only | — | AIR or step `$ref` to the mesh to texture. |
| `shouldTexture` | | `true` | `imageTo3D` only. `false` returns a bare mesh (and bills as shape-only). |
| `shouldRemesh` | | `false` | `imageTo3D`/`texGen`. Remesh before texturing. |
| `enablePbr` | | `true` | `imageTo3D`/`texGen`. Generate PBR maps. |
| `steps` | | 5 (`v2`/`v2-mini`), 30 (`v2.1`) | Range `1`–`150`. Scales cost linearly (see below). |
| `cfgScale` | | 5 (`v2`/`v2-mini`), 7.5 (`v2.1`) | Range `0`–`30`. |
| `octreeResolution` | | `256` | Range `16`–`512`. Higher = finer mesh; scales cost linearly. |
| `resolution` | | 3072 (`v2`/`v2-mini`), 4096 (`v2.1`) | Shape resolution. Range `1`–`8192`. |
| `sampler` | | `euler` | ComfyUI sampler (e.g. `dpmpp_2m`, `uni_pc`). |
| `scheduler` | | `normal` | `normal`, `karras`, `exponential`, `sgm_uniform`, `simple`, `ddim_uniform`, `beta`. |
| `vaeNumChunks` | | `8000` | Range `1000`–`500000`. VAE decode chunking. |
| `meshThreshold` | | `0.6` | Range `0`–`1`. Surface extraction threshold. |
| `shift` | | `1` | Timestep shift. |
| `modelAir` | | version default | Override the model checkpoint AIR (`urn:air:hy3d:repository:huggingface:...`). |
| `seed` | | random | Assigned automatically when omitted. |
## Reading the result
A succeeded `polyGen` step carries the model blobs in `output`. The required `model` is always a GLB; the other blobs appear only when the engine produced them (FBX, thumbnail, and Meshy's rigging/animation outputs).
```json
{
"status": "succeeded",
"steps": [{
"name": "0",
"$type": "polyGen",
"status": "succeeded",
"output": {
"model": { "id": "blob_abc.glb", "available": true, "url": "https://.../signed.glb", "format": "glb" },
"fbxModel": { "id": "blob_abc.fbx", "available": true, "url": "https://.../signed.fbx", "format": "fbx" },
"thumbnail": { "id": "blob_abc-thumbnail.png", "available": true, "url": "https://.../signed.png" }
}
}]
}
```
Fields:
* **`model`** — the generated model in GLB format (always present). For Tripo `quad` runs the GLB is empty (`available: false`) and the mesh lands in `fbxModel` instead.
* **`fbxModel`** — FBX export, when the engine produced one.
* **`thumbnail`** — preview image of the model, when available.
* **`riggedModel` / `riggedFbxModel`** — Meshy rigging output (`enableRigging: true`).
* **`animatedModel` / `animatedFbxModel`** — Meshy animation output (`enableAnimation: true`).
* **`basicAnimations`** — Meshy walk/run clips (`walkingModel`, `runningModel`, …) bundled when rigging is enabled.
Hunyuan3D populates `model` only (the textured GLB) — no FBX or thumbnail.
Blob URLs are signed and expire — refetch the workflow or call [`GetBlob`](/orchestration/reference/operations/GetBlob) for a fresh URL.
## Runtime
3D generation is minutes-scale, so a single long-poll won't cover it — submit with `wait=0` and poll `GetWorkflow` (or use a webhook). Meshy holds its worker claim for up to 20 minutes; textured and rigged runs sit at the higher end of that. Tripo is typically faster, but as a fresh release treat its timings as preliminary until fleet capacity settles. Hunyuan3D is compute-intensive and depends on Comfy GPU worker availability — it can queue before it runs, and `v2.1` at high `octreeResolution`/`steps` is the slowest path; `v2` and `v2-mini` are the quickest.
## Cost
Billed in Buzz on the workflow's `transactions`. Use `whatif=true` for an exact preview; see [Payments (Buzz)](/orchestration/guide/submitting-work#payments-buzz) for currency selection.
**Meshy** is a flat rate — rigging, animation, PBR, topology, and polycount don't change the price:
| Shape | Buzz |
|-------|------|
| Any Meshy `textTo3D` or `imageTo3D` run | **1040** |
**Tripo** is driven by texture fidelity plus the quad surcharge:
```
buzz = (textureDollars + (quad ? 0.05 : 0)) × 1000 × 1.3
textureDollars = 0.2 (no) | 0.3 (standard, or "no" with pbr) | 0.4 (HD)
```
| Shape | Buzz |
|-------|------|
| `texture: "standard"` (default) | **390** |
| `texture: "no"` (mesh only) | 260 |
| `texture: "HD"` | 520 |
| `+ quad` (any texture) | +65 |
| `texture: "HD"` + `quad` | 585 |
`faceLimit`, `autoSize`, `orientation`, `textureAlignment`, and both seeds don't affect Tripo's price.
**Hunyuan3D** bills a base shape charge scaled by mesh resolution and step count, plus a flat texturing charge when a texture is produced:
```
shape = 60 × (octreeResolution / 256) × (steps / defaultSteps)
texGen = 100 (added when texturing; the whole cost for the texGen operation)
defaultSteps = 30 (v2.1), 5 (v2 / v2-mini)
```
| Shape | Buzz |
|-------|------|
| `imageTo3D`, textured, defaults | **160** |
| `imageTo3D`, `shouldTexture: false` (mesh only) | 60 |
| `shapeGen` only | 60 |
| `texGen` only | 100 |
| `imageTo3D` textured, `octreeResolution: 512` | 220 |
`modelVersion` shifts the step baseline but not the price at defaults (each version's default `steps` normalises the `steps` factor to 1). Doubling `octreeResolution` or `steps` doubles the shape charge; the `texGen` charge is flat.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `400` — "No derived type found for discriminator value 'tripo'" | Tripo isn't live in your target environment yet | Use `model: "meshy"`, or wait for the Tripo rollout. |
| `400` — "Prompt is required" / "ImageUrl is required" | `operation`/`model` doesn't match the field you sent | `textTo3D` needs `prompt`; every `imageTo3D` and all Tripo calls need `imageUrl`. |
| `output.model.available` is `false` | Tripo `quad` run (mesh went to FBX), or the source image was rejected | Read `output.fbxModel` for quad runs; otherwise check the source image is reachable. |
| Hunyuan3D step sits `unassigned` / queued | No free Comfy worker holds the model | Wait for capacity, or fall back to hosted `model: "meshy"` / `"tripo"`. |
| Request timed out (`wait` expired) | 3D generation exceeds the long-poll window | Submit with `wait=0` and poll, or register a webhook. |
| Step `failed`, `reason = "blocked"` | Content moderation | Don't retry the same input — see [Errors & retries → Step-level failures](/orchestration/guide/errors-and-retries#step-level-failures). |
## Related
* [`InvokePolyGenStepTemplate`](/orchestration/reference/operations/InvokePolyGenStepTemplate) — the per-recipe endpoint
* [`SubmitWorkflow`](/orchestration/reference/operations/SubmitWorkflow) — generic path for chaining
* [`GetWorkflow`](/orchestration/reference/operations/GetWorkflow) — for polling
* [`GetBlob`](/orchestration/reference/operations/GetBlob) — refetch an expired model/thumbnail URL
* [Results & webhooks](/orchestration/guide/results-and-webhooks) — handling long-running workflows
* Full parameter catalog: the `PolyGenInput` schema in the [API reference](/orchestration/reference/)
* [Endpoint OpenAPI spec](https://orchestration.civitai.com/v2/consumer/recipes/polyGen/openapi.yaml) — standalone OpenAPI 3.1 YAML for this endpoint
---
---
url: /orchestration/recipes/ace-step-audio.md
---
# ACE-Step music generation
[ACE-Step 1.5](https://github.com/ace-step/ACE-Step) is an open text-to-music model that produces full songs from a style description plus structured lyrics. The orchestrator exposes it through a single `aceStepAudio` step, which runs on Civitai's ComfyUI workers. The default checkpoint is the 2B turbo model (`ace_step_1.5_turbo_aio.safetensors`) — an eight-step distillation that generates a 30-second song in ~10 s of worker time.
Without a cover image the step emits an MP3 audio blob. Attach `cover.imageUrl` and the output is an MP4 video with that image as the still background, sized 512×512.
## Variants
There's one step type and one invocation path; the only variant axis is the optional `diffusionModel` override, which swaps the underlying diffusion checkpoint.
All values come from Comfy-Org's [`ace_step_1.5_ComfyUI_files`](https://huggingface.co/Comfy-Org/ace_step_1.5_ComfyUI_files) HuggingFace bundle. The default (unset) is the 2B turbo all-in-one checkpoint.
| `diffusionModel` | Variant | Params | `steps` | `cfg` | Best for |
|---|---|---|---|---|---|
| *(unset)* | `urn:air:ace:checkpoint:huggingface:Comfy-Org/ace_step_1.5_ComfyUI_files@main/checkpoints/ace_step_1.5_turbo_aio.safetensors` | 2B turbo (AIO) | `8` | `1.0` | **Default** — single all-in-one file; fastest path. |
| 2B turbo | `urn:air:ace:checkpoint:civitai:2549270@2864880` | 2B | `8` | `1.0` | Split-file equivalent of the default AIO. Prefer the AIO unless you're already pulling split files. |
| 2B base | `urn:air:ace:checkpoint:civitai:2549270@2864864` | 2B | `50` | `~4` | Non-turbo 2B base — higher fidelity than turbo at the cost of sampling time. |
| XL turbo | `urn:air:ace:checkpoint:civitai:2549270@2864949` | 4B | `8` | `1.0` | More fidelity at turbo speed. Higher VRAM; slower first-submission while the worker pulls the split files. |
| XL base | `urn:air:ace:checkpoint:civitai:2549270@2864892` | 4B | `50` | `~4` | Highest-fidelity base 4B. Non-turbo; typically slowest. |
| XL SFT | `urn:air:ace:checkpoint:civitai:2549270@2864917` | 4B | `50` | `~4` | Supervised-fine-tuned 4B; sibling of XL base with the same runtime characteristics. |
Turbo variants are distilled to converge in 8 steps with CFG effectively off (`1.0`). Non-turbo base / SFT variants expect the full 50-step schedule with classifier-free guidance on (around `4`) — submitting them with the default `steps: 8` / `cfg: 1.0` produces underbaked output.
**Default choice for new integrations**: omit `diffusionModel` entirely. The 2B turbo AIO file is the default and is what Civitai's workers are consistently warm on. Reach for an XL split-file override only when the default fidelity isn't enough and you can tolerate a slow first-submission while the worker pulls the additional files.
## Prerequisites
* A Civitai orchestration token ([Quick start → Prerequisites](/orchestration/guide/getting-started#prerequisites))
* A `musicDescription` — a short, genre-prefixed style blurb (e.g. `"Neo-Soul: warm Rhodes, brush kit, introspective"`)
* A `lyrics` string — structured with section markers (`[Verse]`, `[Chorus]`, `[Bridge]`, …). Use `""` for pure instrumentals (and set `vocalWeight: 0.0` / `instrumentalWeight: 1.0`)
* A `seed` — any integer; same seed + same input reproduces the track deterministically
## Default (2B turbo, audio-only)
The default path — no `diffusionModel` override, no cover. Output is an MP3 blob.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=60
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "aceStepAudio",
"input": {
"musicDescription": "Neo-Soul: A warm, organic neo-soul track with smooth Rhodes chords, mellow bass, and gentle drums. Soulful and introspective mood.",
"lyrics": "[Verse 1]\nSunlight breaks through the morning haze\nCoffee steam rising, starting the day\n\n[Chorus]\nThis is the rhythm of my life\nSimple moments, pure delight",
"duration": 30,
"bpm": 95,
"key": "D major",
"language": "en",
"seed": 12345
}
}]
}
```
## Instrumental (no vocals)
Drop vocals by pairing an empty `lyrics` string with `vocalWeight: 0.0` and `instrumentalWeight: 1.0`. The model still needs both fields — an empty `lyrics` with the default `vocalWeight` of 0.9 will produce scat-like placeholder vocals.
## Audio with cover image (MP4 output)
Attach `cover.imageUrl` and the step emits a `video` blob (`.mp4`) with the image as a static 512×512 background instead of an MP3.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=60
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "aceStepAudio",
"input": {
"musicDescription": "Rock: A driving rock track with powerful guitars and thundering drums.",
"lyrics": "[Intro]\n[Verse]\nBreaking through the walls tonight\nNothing is gonna stop this fight",
"duration": 30,
"bpm": 140,
"key": "E minor",
"seed": 42,
"cover": {
"imageUrl": "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/07f78344-e165-4e96-8340-caf0e562f070/anim=false,width=450,optimized=true/1.jpeg"
}
}
}]
}
```
`cover.imageUrl` accepts either a plain URL string or a workflow `$ref` pointing at an earlier step's output (e.g. chain an `imageGen` step to generate the album art, then feed it into `aceStepAudio` — see [Workflows → Dependencies](/orchestration/guide/workflows#dependencies-parallelism)).
## Switching the diffusion model
Set `diffusionModel` to a full AIR URN. The 2B turbo AIO is the default; everything else is a drop-in override.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=60
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "aceStepAudio",
"input": {
"musicDescription": "Cinematic Orchestral: Sweeping strings, bold brass, and thundering percussion.",
"lyrics": "",
"duration": 30,
"bpm": 110,
"key": "D minor",
"instrumentalWeight": 1.0,
"vocalWeight": 0.0,
"seed": 3,
"diffusionModel": "urn:air:ace:checkpoint:civitai:2549270@2864949"
}
}]
}
```
The split-file XL checkpoints require the worker to download them on first use, so a fresh submission can sit in `scheduled` for a minute or two before a worker is warm. Use the `wait=60` resume loop (see [Runtime](#runtime)) or webhooks — don't wait on a single `wait=60` POST for the first XL call.
## Parameters
| Field | Required | Default | Notes |
|---|---|---|---|
| `musicDescription` | ✅ | — | Style / genre description. Prefix with a genre label (`"Neo-Soul:"`, `"Jazz:"`) for best results. |
| `lyrics` | ✅ | — | Structured lyrics with `[Verse]`, `[Chorus]`, `[Bridge]` markers. Use `""` for pure instrumentals. |
| `seed` | ✅ | — | Any `int32`. Same inputs + same seed reproduce the track. |
| `duration` | | `60` | Seconds, range `1`–`190`. Longer durations increase Buzz linearly — see [Cost](#cost). |
| `bpm` | | `120` | Beats per minute, range `40`–`200`. |
| `timeSignature` | | `"4"` | Beats per measure. `"3"` / `"4"` / `"6"` common. |
| `language` | | `"en"` | Language code — `en`, `zh`, `ja`, `ko`, … |
| `key` | | `"C major"` | Musical key, e.g. `"E minor"`, `"Bb major"`. |
| `instrumentalWeight` | | `0.85` | Range `0.0`–`1.0`. Raise toward `1.0` for instrumental-heavy mixes. |
| `vocalWeight` | | `0.9` | Range `0.0`–`1.0`. Set to `0.0` when `lyrics` is empty or you want a pure instrumental. |
| `diffusionModel` | | *(2B turbo AIO)* | Full AIR URN for the diffusion checkpoint. See the [Variants](#variants) table. |
| `cover.imageUrl` | | *(none)* | URL (or workflow `$ref`) to a cover image. When set, output is an MP4 video with the image as the 512×512 background instead of an MP3. |
## Reading the result
Audio-only runs emit a single `audio` blob (MP3):
```json
{
"status": "succeeded",
"cost": { "total": 4 },
"steps": [{
"name": "$0",
"$type": "aceStepAudio",
"status": "succeeded",
"output": {
"blob": {
"type": "audio",
"id": "blob_....mp3",
"available": true,
"url": "https://orchestration-new.civitai.com/v2/consumer/blobs/blob_....mp3?sig=...&exp=...",
"urlExpiresAt": "2027-04-14T15:13:40Z",
"duration": 30,
"jobId": "..."
}
},
"jobs": [{
"id": "...",
"status": "succeeded",
"startedAt": "2026-04-14T15:13:28.512Z",
"completedAt": "2026-04-14T15:13:37.319Z",
"cost": 4
}]
}]
}
```
Fields:
* **`blob.type`** — `"audio"` for MP3 output (no cover), `"video"` when `cover.imageUrl` was supplied (MP4 output).
* **`blob.id`** — stable blob key, ending in `.mp3` or `.mp4`.
* **`blob.url`** — signed URL. Fetch within `urlExpiresAt` or refetch the workflow / call [`GetBlob`](/orchestration/reference/operations/GetBlob) for a fresh URL.
* **`blob.duration`** — on audio blobs only, the requested duration in seconds (echoes `input.duration`). Video blobs omit this and expose `width` / `height` (both 512) instead.
* **`blob.available`** — `true` once the file is persisted. Whatif previews return `false` because no job actually ran.
When `cover.imageUrl` is set, `blob` is a video blob — same shape, `type: "video"`, `.mp4` extension, `width: 512`, `height: 512`. Despite the C# source commenting "WebM", the current Civitai pipeline emits MP4.
## Runtime
Measured end-to-end against `orchestration.civitai.com` on 2026-04-14:
| Shape | POST → terminal |
|---|---|
| `duration: 30`, 2B turbo, no cover | ~15 s (job itself ~9 s) |
| `duration: 60`, 2B turbo, no cover | ~15 s (job itself ~14 s) |
| `duration: 30`, 2B turbo, with cover image | ~13 s (job itself ~7 s) |
| `duration: 30`, XL turbo (4B) cold worker | >60 s (needs `wait=60` resume loop; worker had to pull split files) |
The 2B turbo default beats the 60-s long-poll window comfortably for every duration up to the 190-s cap, so **submit with `wait=60` and expect the POST itself to return terminal state**. If it doesn't (cold XL variant, capacity pressure), the response comes back non-terminal at the 60-s ceiling — re-issue `GET /v2/consumer/workflows/{id}?wait=60` in a loop until the response is terminal. See [Results & webhooks](/orchestration/guide/results-and-webhooks) for the resume pattern.
For backend integrations that can't hold a connection, register a webhook URL and submit with `wait=0` (fire-and-forget).
## Cost
Billed in Buzz on the workflow's `transactions`. Use `whatif=true` for an exact preview; see [Payments (Buzz)](/orchestration/guide/submitting-work#payments-buzz) for currency selection.
Cost is driven purely by `duration` — a flat base charge plus a per-second factor. Nothing else in the input affects price (model variant, BPM, cover image, instrumental weights, lyrics length are all free).
```
total = 1 + duration × 0.1
```
| Shape | Buzz |
|---|---|
| `duration: 10` (shortest useful clip) | 2 |
| `duration: 30` (default recipe example) | **4** |
| `duration: 60` (schema default) | 7 |
| `duration: 90` (typical full song) | 10 |
| `duration: 180` (near max, 3-minute track) | 19 |
Arithmetic check against the formula: `1 + 30 × 0.1 = 4` ✅, `1 + 60 × 0.1 = 7` ✅, `1 + 180 × 0.1 = 19` ✅. Prod whatif previews confirmed these exact Buzz figures on 2026-04-14. The orchestrator surfaces the raw `Factors["total"]` value — non-integer formula outputs (e.g. `duration: 15` → `2.5`) are passed through unchanged in `cost.total`; there's no `Math.Ceiling` / `Math.Round` in the handler.
Cover images, key, BPM, time signature, language, and instrumental / vocal weights don't affect Buzz price — ACE-Step bills flat-plus-per-second on duration only.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| `400` with `"duration must be between 1 and 190"` (or similar range complaint) | `duration` outside `[1, 190]`, `bpm` outside `[40, 200]`, or a weight outside `[0.0, 1.0]` | Clamp the field to the range in the parameters table. |
| `400` with `"musicDescription is required"` / `"lyrics is required"` / `"seed is required"` | Missing one of the three required fields. `lyrics: ""` is valid; the field itself must still be present. | Include every required field explicitly. |
| `400` with `"Unable to analyze … file"` on the cover image | `cover.imageUrl` pointed at a host that rejected the orchestrator's fetch (range requests, UA block, ALB cookie gating) | Use a Civitai CDN URL, or generate the cover with an `imageGen` step and `$ref` its output. |
| Output has scat-like placeholder vocals on an "instrumental" track | `lyrics: ""` but `vocalWeight` left at default `0.9` | Set `vocalWeight: 0.0` (and ideally `instrumentalWeight: 1.0`) whenever `lyrics` is empty. |
| Step `failed`, `reason = "blocked"` | Content moderation on the description / lyrics / cover image | Don't retry the same input — see [Errors & retries → Step-level failures](/orchestration/guide/errors-and-retries#step-level-failures). |
| Workflow stuck in `scheduled` for >60 s on an XL `diffusionModel` override | No warm worker has the split-file checkpoint yet; the first submission of a given XL variant triggers a download | Keep polling with `?wait=60`; subsequent submissions in the same hour land on the now-warm worker in ~15 s. |
| Request timed out (`wait=60` returned non-terminal) | Cold XL variant, capacity pressure, or `duration` near 190 s on a busy shard | Re-issue `GET /v2/consumer/workflows/{id}?wait=60` until the response is terminal. |
## Related
* [`InvokeAceStepAudioStepTemplate`](/orchestration/reference/operations/InvokeAceStepAudioStepTemplate) — the per-recipe endpoint
* [`SubmitWorkflow`](/orchestration/reference/operations/SubmitWorkflow) — generic path for chaining `aceStepAudio` into multi-step workflows
* [`GetWorkflow`](/orchestration/reference/operations/GetWorkflow) — for the `wait=60` resume loop
* [Transcription](./transcription) — inverse direction (audio → text); chain after `aceStepAudio` to auto-caption a track
* [Text-to-speech](./text-to-speech) — sibling audio recipe for spoken output
* [Flux 2 image generation](./flux2) — common upstream for generating cover art to feed into `cover.imageUrl`
* [Workflows → Dependencies](/orchestration/guide/workflows#dependencies-parallelism) — for chaining an `imageGen` cover generator into this step
* [Results & webhooks](/orchestration/guide/results-and-webhooks) — handling long-running submissions (cold XL variants, webhooks)
* Full parameter catalog: the `AceStepAudioInput` schema in the [API reference](/orchestration/reference/)
* [Endpoint OpenAPI spec](https://orchestration.civitai.com/v2/consumer/recipes/aceStepAudio/openapi.yaml) — standalone OpenAPI 3.1 YAML for this endpoint
---
---
url: /site/guide/air.md
description: >-
The AI Resource Identifier (AIR) URN format used across Civitai and the
Orchestration API.
---
# AIR identifiers
An **AI Resource Identifier** (AIR) is the canonical URN-style string Civitai
uses to reference any AI resource — a checkpoint, LoRA, VAE, embedding, or
upscaler — consistently across the site API, the Orchestration API, and
partner integrations.
Every response from [`GET /model-versions/{id}`](../reference/model-versions#get-a-model-version)
includes an `air` field you can pass directly to generation APIs.
## Format
```
urn:air:{ecosystem}:{type}:{source}:{id}[@{version}][+{fileId}][.{format}]
```
The `urn:` and `air:` prefixes are both optional — parsers accept
`urn:air:sdxl:checkpoint:civitai:827184@2514310`,
`air:sdxl:checkpoint:civitai:827184@2514310`, and bare
`sdxl:checkpoint:civitai:827184@2514310` interchangeably. **Use the full
`urn:air:...` form** in API requests; it's the unambiguous canonical form.
### Fields
| Field | Required | Description |
|-------|----------|-------------|
| `ecosystem` | Optional | Model family bucket: `sd15`, `sdxl`, `sd3`, `flux1`, `other`, etc. |
| `type` | Optional | Resource kind: `checkpoint`, `lora`, `embedding`, `vae`, `controlnet`, `upscaler`. |
| `source` | Required | Hosting system: `civitai`, `civitai-r2`, `huggingface`, `orchestrator`. |
| `id` | Required | Resource identifier within the source. For `civitai`, this is the **model ID**. |
| `version` | Optional | Specific version (for `civitai` this is the model version ID). If omitted, the resource's default/latest version is implied. |
| `fileId` | Optional | Specific `ModelFile` id, prefixed with `+`. Disambiguates between multiple files attached to the same version (e.g. a pruned vs. full-weight checkpoint, or a base model shipped alongside its text-encoder file). Omit to let the resolver pick the primary file. |
| `format` | Optional | Model file format, e.g. `safetensor`, `ckpt`, `diffuser`. |
## Real examples
From actual `GET /api/v1/model-versions/{id}` responses and internal workflow
templates:
```
urn:air:sdxl:checkpoint:civitai:827184@2514310
urn:air:sdxl:checkpoint:civitai:827184@2514310+2402203
urn:air:illustrious:checkpoint:civitai:795765@900661
urn:air:other:upscaler:civitai:147759@164821
urn:air:other:other:civitai-r2:civitai-worker-assets@sam_vit_b_01ec64.pth
```
The second example pins the AIR to a specific file on the version (e.g.
`waiIllustriousSDXL_v160.safetensors`, file id `2402203`) — useful when a
version ships multiple downloadable artifacts and you need to be explicit
about which one to load. The last one is a file asset (SAM ViT-B checkpoint)
stored on Civitai's R2 bucket rather than a model version.
## Type values
The `type` segment maps to Civitai's `ModelType` enum:
| AIR type | Civitai `ModelType` |
|----------|---------------------|
| `checkpoint` | `Checkpoint` |
| `lora` | `LORA` |
| `embedding` | `TextualInversion` |
| `vae` | `VAE` |
| `controlnet` | `Controlnet` |
| `upscaler` | `Upscaler` |
Resources that don't map to one of those (motion modules, detection models,
wildcards, etc.) use `other` as the type.
### Diffusion-model checkpoints
Some checkpoints ship their weights as a standalone **diffusion model** (the
denoiser / UNET only, with the VAE and text encoders supplied as separate
files) rather than an all-in-one checkpoint. These models still have
`ModelType = Checkpoint`, but their primary file is typed `Diffusion Model`
(or `UNet`). When that's the case, the AIR `type` segment reflects the **file**
kind instead of the model type:
| Primary `ModelFile.type` | AIR type |
|--------------------------|----------|
| `Diffusion Model` | `diffusionmodel` |
| `UNet` | `unet` |
```
urn:air:boogu:diffusionmodel:civitai:2714299@3049541
```
So a Flux / Wan / ZImage / Anima / Boogu checkpoint whose primary file is a
diffusion model resolves to `...:diffusionmodel:...`, not `...:checkpoint:...`.
The `air` field returned by the site API already applies this — prefer it over
hand-construction.
## Container image AIRs (`oci:image`)
Jobs that run in a worker-managed container can declare a custom container
image as a resource. The image is pulled (if not already cached on the worker)
and participates in the worker's LRU lifecycle alongside model files.
```
urn:air:oci:image:{registry}:{repo}@{tag-or-digest}
```
| Field | Value |
|-------|-------|
| `ecosystem` | `oci` (fixed) |
| `type` | `image` (fixed) |
| `source` | Registry alias: `dockerhub`, `ghcr`, etc. |
| `id` | Full repo path (may include `/`), e.g. `civitai/spine-comfy` |
| `version` | Tag like `v1.0.0`, or a digest like `sha256:abc123...` |
Examples:
```
urn:air:oci:image:dockerhub:civitai/spine-comfy@v1.0.0
urn:air:oci:image:dockerhub:library/python@3.12-slim
urn:air:oci:image:ghcr:civitai/training-toolkit@sha256:abc123...
```
Workers advertise support for `oci:image` via the `oci` ecosystem and the
`image` on-demand resource type, so the orchestrator only routes jobs with
OCI image dependencies to workers that can pull them. Built-in template
images (the worker's default ComfyUI/SigLIP2/ASR/AI-Toolkit images) are
reported as available resources but pinned — they participate in worker
registration without being subject to LRU eviction.
## Using AIR with the Orchestration API
The Orchestration API accepts AIR strings anywhere a resource is referenced.
Given a `modelVersionId` from the site API, the simplest way to get a valid
AIR is to call `GET /api/v1/model-versions/{id}` and forward the `air` field.
For example, to use `WAI-illustrious-SDXL v16.0` in a text-to-image workflow:
1. `curl https://civitai.com/api/v1/model-versions/2514310` →
`"air": "urn:air:sdxl:checkpoint:civitai:827184@2514310"`
2. Pass that string as the checkpoint reference in your
[Orchestration submission](/orchestration/guide/submitting-work).
## Building an AIR by hand
You can also construct an AIR directly from a Civitai model version:
```
urn:air:{baseModel}:{type}:civitai:{modelId}@{versionId}[+{fileId}]
```
Where `baseModel` comes from the model version's `baseModel` field
(`SDXL 1.0` → `sdxl`, `SD 1.5` → `sd15`, etc.) and `type` maps from the
parent model's `type` field as shown in the table above. Append
`+{fileId}` (using a `ModelFile.id` from `files[]` on the model version
response) only when you need to pin a specific file; otherwise the resolver
picks the primary file.
The site-generated `air` field already handles this mapping — prefer it over
hand-construction when you have the option.
---
---
url: /orchestration/recipes/anima.md
---
# Anima image generation
Anima is an anime-focused image generation ecosystem on Civitai's sdcpp workers. Single engine path, one operation (`createImage` — no img2img or edit support), optimized defaults for anime/illustration output:
* `engine: "sdcpp"`, `ecosystem: "anima"`
* **Only `createImage`** — Anima doesn't expose `createVariant` or `editImage`. Use [Flux 2 Klein](./flux2#klein-createvariant-img2img) or [Qwen](./qwen) if you need img2img or prompt-driven editing.
* Higher default `steps` (`30`) and lower default `cfgScale` (`4`) than the SD ecosystems — tuned for anime output
* Supports LoRAs for style/character injection
* No checkpoint URN needed — the ecosystem ships its own model; an optional `diffuserModel` override exists for advanced cases
## Prerequisites
* A Civitai orchestration token ([Quick start → Prerequisites](/orchestration/guide/getting-started#prerequisites))
* No checkpoint URN required — Anima uses a built-in diffuser
## Text-to-image
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=60
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "imageGen",
"input": {
"engine": "sdcpp",
"ecosystem": "anima",
"operation": "createImage",
"prompt": "masterpiece, best quality, 1girl, solo, portrait, looking at viewer, cinematic lighting",
"negativePrompt": "worst quality, low quality, blurry, bad anatomy, deformed hands",
"width": 1024,
"height": 1024,
"cfgScale": 4,
"steps": 30
}
}]
}
```
### Parameters
| Field | Default | Range | Notes |
|-------|---------|-------|-------|
| `prompt` | — ✅ | ≤ 10 000 chars | Booru-style tags work best. Lead with quality boosters (`masterpiece, best quality, …`). |
| `negativePrompt` | *(none)* | ≤ 10 000 chars | Recommended. `worst quality, low quality, blurry, bad anatomy, deformed hands` is a solid starting point. |
| `width` / `height` | `1024` | `64`–`2048`, divisible by 16 | Anima is trained around 1024² and well-behaved aspect ratios near that pixel count. |
| `cfgScale` | `4` | `0`–`30` | **Lower than SD1/SDXL's 7.** `3`–`5` is the sweet spot for Anima. |
| `steps` | `30` | `1`–`150` | **Higher than most sdcpp defaults.** `25`–`35` typical. |
| `sampleMethod` | `euler` | enum | [`SdCppSampleMethod`](/orchestration/reference/). |
| `schedule` | `simple` | enum | [`SdCppSchedule`](/orchestration/reference/). |
| `loras` | `{}` | `{ airUrn: strength }` | Stack multiple; `0.6`–`1.0` strengths typical. |
| `diffuserModel` | *(built-in)* | AIR URN | Optional override for the diffuser. The default built-in model is what you want in almost every case. |
| `quantity` | `1` | `1`–`12` | Number of images per call. |
| `seed` | random | int64 | Pin for reproducibility. |
### Aspect-ratio variants
Anima handles non-square aspect ratios well near ~1 megapixel total area — similar guidance to SDXL. Well-behaved dimensions include 1024², 1152×896, 1344×768, 1536×640, and their mirrors.
```json
{
"steps": [{
"$type": "imageGen",
"input": {
"engine": "sdcpp",
"ecosystem": "anima",
"operation": "createImage",
"prompt": "masterpiece, best quality, cyberpunk anime scene, neon city street at night",
"negativePrompt": "worst quality, low quality, blurry",
"width": 1344,
"height": 768,
"cfgScale": 4,
"steps": 30
}
}]
}
```
### With LoRAs
Anima LoRAs are a map of AIR URN → strength. Style LoRAs usually sit at `0.6`–`1.0`; character / concept LoRAs often higher:
```json
{
"steps": [{
"$type": "imageGen",
"input": {
"engine": "sdcpp",
"ecosystem": "anima",
"operation": "createImage",
"prompt": "masterpiece, best quality, detailed portrait of a magical girl in a forest",
"negativePrompt": "worst quality, low quality",
"width": 1024,
"height": 1024,
"cfgScale": 4,
"steps": 30,
"loras": {
"urn:air:anima:lora:civitai:123456@789012": 0.8
}
}
}]
}
```
Only Anima-tagged LoRAs work on the `anima` ecosystem.
## Reading the result
A successful `imageGen` step emits an `images[]` array — one entry per `quantity`:
```json
{
"status": "succeeded",
"steps": [{
"name": "0",
"$type": "imageGen",
"status": "succeeded",
"output": {
"images": [
{ "id": "blob_...", "url": "https://.../signed.jpeg" }
]
}
}]
}
```
Blob URLs are signed and expire — refetch the workflow or call [`GetBlob`](/orchestration/reference/operations/GetBlob) for a fresh URL.
## Runtime
Typical wall time per 1024×1024 image is 10–25 s. `wait=60` works comfortably for `quantity ≤ 2`. Higher `steps` counts and larger dimensions compound runtime; submit with `wait=0` and poll for large batches or atypical aspect ratios.
## Cost
Billed in Buzz on the workflow's `transactions`. Use `whatif=true` for an exact preview; see [Payments (Buzz)](/orchestration/guide/submitting-work#payments-buzz) for currency selection.
Per-pixel + per-step scaling against 1024² / 25 steps:
```
total = 8 × (width × height / 1024²) × (steps / 25) × quantity
```
| Shape | Buzz |
|-------|------|
| 1024²/`steps: 30`/`quantity: 1` (defaults) | **~9.6** |
| 1024²/`steps: 30`/`quantity: 4` | ~38 |
| 1344×768/`steps: 30` | ~7.5 × 1.2 ≈ **~9** |
| 1024²/`steps: 40` | ~12.8 |
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `400` with "operation must be createImage" | Passed `editImage` or `createVariant` | Anima only supports `createImage`. Use [Qwen](./qwen) or [Flux 2 Klein](./flux2#klein-createvariant-img2img) for img2img / edit on anime-style inputs. |
| `400` with "ecosystem must be anima" | Typo | Lowercase `"anima"`. |
| `400` with "model is not a valid property" | Sent `model` field | Anima has no checkpoint picker — delete the field, or if overriding, use `diffuserModel` instead. |
| Output looks flat or off-style | `cfgScale: 7` (SD default) on Anima | Drop to `cfgScale: 4`. Anima wants lower guidance than SD1/SDXL. |
| Output underbakes | `steps` too low for the prompt complexity | Bump to `steps: 30`–`40`. Anima's default is already `30` — don't go much below `20`. |
| LoRA has no effect | Wrong AIR URN, model private / not published, or ecosystem mismatch | Verify the URN on the LoRA's Civitai page; only Anima-tagged LoRAs work on the `anima` ecosystem. |
| Request timed out (`wait` expired) | Large `quantity`, atypical dimensions, or high `steps` | Resubmit with `wait=0` and poll. |
| Step `failed`, `reason = "blocked"` | Prompt hit content moderation | Don't retry the same input — see [Errors & retries → Step-level failures](/orchestration/guide/errors-and-retries#step-level-failures). |
## Related
* [`SubmitWorkflow`](/orchestration/reference/operations/SubmitWorkflow) — operation used by every example here
* [`GetWorkflow`](/orchestration/reference/operations/GetWorkflow) — for polling
* [Qwen image generation](./qwen) — alternative with edit + variant operations and LoRA support
* [SDXL image generation](./sdxl) — higher-fidelity general-purpose alternative
* [Flux 2](./flux2) / [Flux 1](./flux1) image generation — newer open-weights families
* [Image upscaling](./image-upscaler) — chain after `imageGen` for higher-res output
* [Prompt enhancement](./prompt-enhancement) — LLM-rewrite a prompt before feeding it in via `$ref`
* Full parameter catalog: the `AnimaCreateImageGenInput` schema in the [API reference](/orchestration/reference/)
* [`imageGen` endpoint OpenAPI spec](https://orchestration.civitai.com/v2/consumer/recipes/imageGen/openapi.yaml) — standalone OpenAPI 3.1 YAML covering the full `imageGen` surface; import into Postman / OpenAPI Generator
---
---
url: /orchestration/reference.md
---
# API Reference
Every consumer-facing operation, request schema, and response shape in the Civitai Orchestration API. Pages here are generated from the OpenAPI specification ([`v2-consumers.json`](https://orchestration.civitai.com/openapi/v2-consumers.json)) and stay in sync with the running API on every build.
## Conventions
* **Base URL**: `https://orchestration.civitai.com`
* **Auth**: `Authorization: Bearer ` on every request.
* **Content type**: `application/json` for bodies; blob upload endpoints accept `multipart/form-data` or presigned PUT.
* **IDs**: workflow IDs are ULIDs prefixed `wf_`; blob IDs are prefixed `blob_`.
* **Polymorphism**: workflow step bodies use a `$type` discriminator; request/response schemas list all valid subtypes under `oneOf`.
## Entry points
Most consumer integrations only touch three operations:
* [`SubmitWorkflow`](/orchestration/reference/operations/SubmitWorkflow) — create a workflow with one or more steps
* [`GetWorkflow`](/orchestration/reference/operations/GetWorkflow) — poll a single workflow
* [`QueryWorkflows`](/orchestration/reference/operations/QueryWorkflows) — list / filter workflows
The left sidebar is grouped by OpenAPI tag — **Workflows**, **WorkflowSteps**, **Recipes**, **Blobs**, **Resources**. Recipes have per-endpoint variants (one per job type) if you prefer the typed surface over the polymorphic `SubmitWorkflow` body.
## Rate limits & quotas
::: info Stub
Fill in once the per-tier rate limit scheme is finalized.
:::
---
---
url: /apps/reference.md
description: >-
Generated-from-source reference for Civitai Apps — scopes, manifest, message
bridge, React hooks, and the CLI.
---
# Reference
The pages in this section are **regenerated from pinned sources** on every build,
rather than hand-maintained. Each page states its exact source at the top. How
"live" a page is depends on its source (see [Keeping this current](#keeping-this-current)):
the manifest and the other SDK-derived pages track pinned package versions,
while the scope catalog tracks a committed snapshot in CI.
| Page | What it covers | Source of truth |
|------|----------------|-----------------|
| [Scopes](./scopes) | The scope catalog: what each scope authorizes, its OAuth bit, and its binding | `civitai` block-scope constants |
| [Manifest](./manifest) | Every `block.manifest.json` field, type, and constraint | the published JSON Schema |
| [Components](./components) | The `@civitai/components` design-system pack: each component's `data-civitai-ui` name, attributes, and ARIA markup | the `@civitai/components` `MARKUP.md` contract |
| [Messages](./messages) | The full `postMessage` bridge protocol (payloads, directions, page-only) | `@civitai/app-sdk` + host parity inventory |
| [Hooks](./hooks) | Every `@civitai/blocks-react` hook: signature + example | `@civitai/blocks-react` types + README |
| [CLI](./cli) | The `civitai` CLI's App-authoring commands and flags | the Go `civitai` CLI (`civitai/cli`) |
::: tip These pages regenerate on build
Reference artifacts are produced by `scripts/gen-appblocks-*.mjs` and land in a
gitignored `public/appblocks/`. `npm run gen:appblocks` (wired into `predev` /
`prebuild`) rebuilds them from the pinned SDK packages and the `civitai`
contract sources.
:::
## Keeping this current
Regeneration is automatic, but the **sources** the generators read are pinned —
so a real upstream change reaches these pages only after a maintainer refreshes
the relevant pin or snapshot. The refresh actions are:
| Page(s) | Source in CI | Refresh action |
|---------|--------------|----------------|
| [Manifest](./manifest) | the SDK-bundled canonical schema `@civitai/app-sdk/schemas/app-block/v1.json` (pinned devDep); committed `appblocks-snapshots/manifest-schema.json` is the CI-hermetic fallback, kept in lockstep with the pin | **Bump the `@civitai/app-sdk` version pin** (same one-line change as Messages/Hooks) — no live fetch. |
| [Messages](./messages) payload shapes, [Hooks](./hooks) | the pinned `@civitai/*` npm devDeps in `package.json` | **Bump the version pins** (`@civitai/app-sdk`, `@civitai/blocks-react`) — a one-line, reviewable change. |
| [CLI](./cli) | the committed `civitai app --help` snapshot (`appblocks-snapshots/civitai-cli-help.txt`) | **Re-capture** with a newer `civitai` binary: `node scripts/gen-appblocks-cli.mjs --write-snapshot`. |
| [Components](./components) | the committed `appblocks-snapshots/MARKUP.md` (the `@civitai/components` markup contract; CI has no `civitai-app-starters` checkout) | **Re-snapshot** `MARKUP.md` from `civitai-app-starters@main`, then re-run `node scripts/gen-appblocks-components.mjs` to regenerate the committed page. |
| [Scopes](./scopes) + the [Messages](./messages) page-only / request-reply flags | committed `appblocks-snapshots/` (CI has no `civitai` checkout) | **Re-copy the 3 snapshot files** from `civitai@origin/main` (`block-scope.constants.ts`, `scope-descriptions.constants.ts`, `hostHandlerParity.ts`). |
On a machine that has the `civitai` sibling repo checked out, the scopes /
messages-parity generators read `civitai@origin/main` directly and only fall
back to the snapshot when it's absent — so those snapshots are the CI-hermetic
copy, not the live source.
> Automated CI drift-guards now cover this. On the daily
> [`appblocks-drift`](https://github.com/civitai/civitai-developer-docs/blob/main/.github/workflows/appblocks-drift.yml)
> workflow: `npm run check:pins` fails when a pinned SDK devDep lags npm
> `latest`, `npm run check:snapshots` fails when a committed snapshot lags the
> upstream contract, and `npm run check:cli-snapshot` fails when the committed
> `civitai app --help` capture lags the latest published `civitai/cli` release.
> That last one needs its own probe because the CLI snapshot is `--help` *output*
> from a compiled Go binary, so there is no upstream file to diff. Separately,
> `npm run test:appblocks:cli` **blocks a PR** when the CLI gains an `app`
> subcommand the generator does not list — that one is a repo-local invariant, so
> it cannot false-fail on someone else's release. Refreshing is still a maintainer
> step, but a lagging pin or snapshot is now a visible, actionable signal rather
> than silent rot.
## Pinned versions
The SDK-derived pages are generated from these published, pinned packages:
* `@civitai/app-sdk@0.31.0` — the framework-agnostic contract (messages, scopes, manifest schema).
* `@civitai/blocks-react@0.39.0` — the React hooks.
When the SDK publishes a new version, bumping the pin in `package.json` is a
one-line, reviewable change that flows through to every generated page.
The [CLI](./cli) page is generated separately, from a committed `civitai app --help` snapshot of the canonical Go `civitai` CLI (`civitai/cli`) — not from an
npm package. Refresh it with `node scripts/gen-appblocks-cli.mjs --write-snapshot` against a newer binary.
## A note on authority
The generated tables describe the **shape** of the contract. The Civitai server
is the **enforcement boundary** — it validates more than these artifacts can
express (host allowlists, scope subsets, trust tiers). Where a generated page
and the server ever disagree, the server wins. Each page calls out the semantics
it can't capture.
---
---
url: /site/reference/articles.md
description: 'List, search, and fetch Civitai articles.'
---
# Articles
An **article** is a long-form post published on Civitai — a guide, workflow
write-up, changelog, or announcement. These endpoints expose the same public
article feed that powers the website.
::: tip Public, edge-cached, rate-limited
Both endpoints are **public** — they work anonymously and always evaluate the
request as anonymous, so a token is *optional* and never changes the data you
get back (the result is a pure function of the URL + your region). Responses are
edge-cached (`public, s-maxage=300`) and **conservatively rate-limited**; on a
`429` respect the `Retry-After` header. Only **published, scanned, non-private**
articles are ever returned; drafts and private articles are invisible (a private
article is indistinguishable from a missing one). Mature content is clamped to
the SFW ceiling in restricted regions regardless of the `nsfw` param.
:::
## List articles
```
GET /api/v1/articles
```
**Auth:** Public.
### Query parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `limit` | integer (1–100) | 100 | Number of items per page. |
| `cursor` | string | — | Opaque keyset cursor (`"\|"`). Use `metadata.nextCursor` from the previous response. Articles use **cursor-based** pagination only (no `page`). |
| `query` | string | — | Full-text search over the article title. |
| `tags` | comma-separated integers | — | Filter by **tag IDs** (not names), e.g. `tags=5,12`. |
| `username` | string | — | Filter by author username. |
| `sort` | `Newest` | `Recently Updated` | `Most Reactions` | `Most Comments` | `Most Bookmarks` | `Most Collected` | `Newest` | Sort order. |
| `nsfw` | boolean | `false` | If `true`, include mature content. Ignored (clamped to SFW) in restricted regions. |
A malformed `cursor` or an invalid param returns `400`.
### Response
Envelope: `{ items, metadata: { nextCursor, nextPage } }`. Each item mirrors the
article feed shape (key fields shown):
```json
{
"items": [
{
"id": 15342,
"title": "Getting started with ComfyUI",
"publishedAt": "2025-12-18T09:16:12.062Z",
"createdAt": "2025-12-17T20:02:00.000Z",
"updatedAt": "2025-12-18T09:16:12.062Z",
"nsfwLevel": 1,
"availability": "Public",
"status": "Published",
"stats": {
"favoriteCount": 812,
"collectedCount": 240,
"commentCount": 37,
"likeCount": 640,
"heartCount": 210,
"viewCount": 51200,
"tippedAmountCount": 1450
},
"user": {
"id": 4021,
"username": "some-creator",
"image": "https://image.civitai.com/.../avatar.jpeg"
},
"tags": [
{ "id": 5, "name": "comfyui", "isCategory": false }
],
"coverImage": {
"id": 88213,
"url": "https://image.civitai.com/.../cover.jpeg",
"nsfwLevel": 1,
"width": 1024,
"height": 1024
}
}
],
"metadata": {
"nextCursor": "1734512172|15320",
"nextPage": "https://civitai.com/api/v1/articles?limit=100&cursor=1734512172%7C15320"
}
}
```
`metadata.nextCursor` / `metadata.nextPage` are omitted on the last page.
### Example
```bash
curl "https://civitai.com/api/v1/articles?limit=5&sort=Most%20Reactions&query=comfyui"
```
## Get an article
```
GET /api/v1/articles/{id}
```
**Auth:** Public.
### Path parameters
| Name | Type | Description |
|------|------|-------------|
| `id` | integer (1–2147483647) | Article ID. |
### Response
Returns the full article object — the same fields as a list item plus the
article body/content — with the moderator-only `moderatorNsfwLevel` field
stripped and the `coverImage` clamped to the region's public browsing ceiling
(a cover above the ceiling is dropped).
Returns `404` if the article doesn't exist **or** is a draft / unpublished /
private article (the two cases are indistinguishable):
```json
{ "error": "No article with id 0" }
```
### Example
```bash
curl "https://civitai.com/api/v1/articles/4797"
```
---
---
url: /orchestration/guide/authentication.md
---
# Authentication
All consumer endpoints require `Authorization: Bearer ` on every request.
## Getting an API key
Manage your API keys from your Civitai account at **[civitai.com](https://civitai.com)** — generate new keys, revoke old ones, and copy tokens from there. Treat API keys like passwords: never commit them to source control, and rotate them if you suspect exposure.
## Using the token
```http
Authorization: Bearer
```
All requests go to `https://orchestration.civitai.com`.
## Try It in the docs
Most pages on this site have a **Run** widget under each example. Click the **Token** button in the top-right of the navbar to paste your Bearer token; it's stored in your browser's `localStorage` and used for every Run / Reference Try-It on the site. The token never leaves your browser except in the `Authorization` header it sends to `orchestration.civitai.com`.
The widget supports:
* **Preview cost** — submits with `whatif=true`, shows a per-currency Buzz breakdown.
* **Submit for real** — runs the workflow with `wait=90`, then polls [`GetWorkflow`](/orchestration/reference/operations/GetWorkflow) until terminal.
* **Inline preview** — generated images and videos render in the page once the workflow finishes.
Reference operation pages have their own playground panel from the OpenAPI viewer (with its own auth field — paste once, persists across reloads).
::: info Stub
Expand once finalized: token scopes, rate limits per tier, rotation policy, how to request elevated access.
:::
---
---
url: /site/guide/authentication.md
description: How to authenticate with the Civitai site API using bearer tokens.
---
# Authentication
The Civitai site API uses **bearer tokens** generated from your account
settings. A single token covers every endpoint that accepts authentication.
::: info Building a third-party app?
Use [OAuth](/site/oauth/) instead of personal API keys — users authorize
your app explicitly with the scopes it needs and can revoke it any time,
without rotating anything on your side.
:::
## How to pass the token
Two methods are supported. The header form is strongly preferred; the
query-param form exists mainly for download-tool compatibility and leaks the
token into access logs and caches.
### Authorization header (preferred)
```bash
curl -H "Authorization: Bearer $CIVITAI_TOKEN" \
"https://civitai.com/api/v1/me"
```
### Query parameter
```bash
curl "https://civitai.com/api/v1/me?token=$CIVITAI_TOKEN"
```
## Which endpoints require a token?
Endpoints fall into three categories:
| Category | Behavior without a token | Examples |
|---|---|---|
| **Public** | Full access. | `GET /creators`, `GET /tags`, `GET /images`, `GET /models/{id}`, `GET /model-versions/*` |
| **Mixed** | Accessible, but some filter params or fields may be unavailable. | `GET /models` (the `favorites` and `hidden` query params require auth) |
| **Authenticated** | `401 Unauthorized`. | `GET /me` |
Each page in the [Reference](../reference/) notes which category an endpoint falls into.
## What 401 looks like
Calling an authenticated endpoint without a token — or with an invalid one —
returns:
```
HTTP/2 401
Content-Type: application/json
{"error":"Unauthorized"}
```
Mixed endpoints silently degrade to anonymous access when no token is
provided; they only return 401 if you pass an auth-only filter (e.g.
`?favorites=true`) without a valid session.
## Caching and auth
Public endpoints set `Cache-Control: public, s-maxage=300, stale-while-revalidate=150` —
responses are cached for 5 minutes at the edge. When you call an endpoint *with*
a valid token, caching is skipped so personalized responses aren't shared.
CORS is open for public endpoints (`Access-Control-Allow-Origin: *`);
authenticated requests are restricted to Civitai-owned origins.
## Security tips
* Tokens are account-scoped. Rotating one means rotating everywhere it's used.
* If you suspect a leak, delete the key from your [account settings](https://civitai.com/user/account) and issue a new one.
* Prefer the `Authorization` header over `?token=`; query params end up in server logs, browser history, and proxy caches.
* Never embed a token in client-side code shipped to browsers or mobile apps.
---
---
url: /site/oauth/buzz-limits.md
description: >-
How Civitai users cap an OAuth app's buzz spending, and what your app should
expect at runtime.
---
# Buzz spend limits
OAuth tokens that include `AIServicesWrite` authorize your app to spend
the user's buzz on AI services (generation, training, scanning). To keep
that authorization sane, the consent flow lets users cap how much an app
can spend, and they can change the cap later from civitai.com.
Your app doesn't set or change the limit — the user does — but knowing
what they see and how it surfaces at runtime will save you a lot of debugging.
::: info Scope of the cap
Per-app buzz caps are enforced by the orchestrator, so they only apply to
**orchestrator-mediated spend** — every AI-services call your token makes.
Other buzz-spending scopes that an OAuth token can carry (notably
`BountiesWrite`, which lets the user create bounties) are gated by the
user's overall balance but are **not** subject to the per-app cap.
:::
## How users set a limit
When the user reaches the consent screen for a scope that includes
`AIServicesWrite`, Civitai shows a budget control alongside the scope list.
The current UI exposes a single "sliding window" budget — buzz limit + period
— but the underlying schema is more flexible.
After consent, users manage existing limits from **Account → Connected
Apps**. They can:
* Edit the limit per app.
* Remove the limit entirely (no cap).
* Revoke the app outright (which is a stronger action — invalidates all
the app's tokens).
## Budget shape
Limits are stored as an array of budgets. Each budget is one of:
| Type | Fields | Meaning |
|---|---|---|
| `absolute` | `limit`, optional `currencies` | Hard cap. Once hit, no more spending on those currencies until the user resets. |
| `sliding` | `limit`, `unit`, `window`, optional `currencies` | Rolling window — e.g. `unit: 7, window: "day"` is "no more than `limit` in any 7-day stretch." This is what the simple UI ships. |
| `rollover` | `limit`, `cron`, optional `currencies` | Calendar-based reset on a cron expression (e.g. monthly reset on the 1st). |
`currencies` (when set) restricts the budget to specific buzz pools — leave
it off and the budget covers every buzz currency.
Your app **doesn't read** this structure directly — it's stored per-user
and enforced server-side. You'll only ever see its effect: spend calls
succeed or fail.
## What your app sees at runtime
When the orchestrator blocks a spend — for either "user is broke" **or**
"user's per-app cap is hit" — Civitai surfaces it the same way:
```json
{
"code": "BAD_REQUEST",
"message": "Hey buddy, seems like you don't have enough funds to perform this action."
}
```
(The `message` may be replaced by an orchestrator-provided detail string
when a per-app limit is what tripped the call — but the response **code is
the same** either way.)
There's no separate error code that lets you distinguish "out of buzz"
from "capped by the user". If you need to give a precise message to the
user, parse `message` defensively, or check the user's per-app spend
state via [`GET /api/v1/me`](../reference/users) ahead of the call and
present a likely-cause hint based on whether a limit is set.
::: warning Don't rely on message text for programmatic decisions
The exact default message string above comes from
[`throwInsufficientFundsError`](https://github.com/civitai/civitai)'s
helper and may change. Treat anything beyond the HTTP/RPC code as
human-readable only.
:::
## Best practices for buzz-spending clients
* **Surface the user's balance.** Call
[`GET /api/v1/me`](../reference/users) periodically and show buzz in
your UI — users hate guessing whether their next click will be denied.
* **Use `whatif=true` for cost preview**, not for limit detection. The
orchestration `whatif` mechanism ([see the orchestration guide](../../orchestration/guide/submitting-work))
is designed to give you a per-currency cost breakdown before you submit
for real; treat it as a costing tool, not a "will this be denied?" oracle.
* **Don't retry on insufficient-funds errors.** Whether it's a real shortfall
or the user's per-app cap, retrying won't help until balance or limits
change. Show the user the error and let them resolve it.
* **Treat token revocation as expected.** A user who hits their cap may
decide to revoke your app entirely from civitai.com. Your refresh-token
call will return `invalid_grant`; handle that by sending the user back
through `/authorize` (with messaging that explains why).
* **Never persist budget assumptions across sessions.** Users can change
their cap any time; treat each spend call as the source of truth.
## When you don't need buzz scopes
If your app doesn't spend buzz on the user's behalf — e.g. a read-only
analytics dashboard, or one that submits work using **your own**
`client_credentials` token — don't request `AIServicesWrite`. Users won't
see the buzz-cap UI, and you skip a whole category of failure modes.
---
---
url: /orchestration/recipes/chat-completion.md
---
# Chat completion
`chatCompletion` routes text (and optionally image) inputs through large language models. Any model available on [OpenRouter](https://openrouter.ai/models) is supported, plus Civitai-hosted AIR models. The request and response shapes follow the OpenAI Chat Completions API.
## Access paths
Two ways to use chat completion, depending on your use case:
| Path | When to use |
|------|-------------|
| **`POST /v1/chat/completions`** | Drop-in replacement for the OpenAI API. Accepts `stream: true` for SSE streaming. |
| **`chatCompletion` workflow step** | Chain with other steps (`imageGen`, `convertImage`, etc.) in a multi-step workflow. |
Both paths share the same input schema and produce the same output format.
## Basic text completion
### Via the OpenAI-compatible endpoint
```http
POST https://orchestration.civitai.com/v1/chat/completions
Authorization: Bearer
Content-Type: application/json
{
"model": "openai/gpt-4o-mini",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What is the capital of France?" }
]
}
```
### Via SubmitWorkflow
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=60
Authorization: Bearer
Content-Type: application/json
{
"steps": [{
"$type": "chatCompletion",
"input": {
"model": "openai/gpt-4o-mini",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What is the capital of France?" }
]
}
}]
}
```
## Vision (image inputs)
Pass images in user message content parts. Any vision-capable model (e.g. `openai/gpt-4o`, `google/gemini-2.0-flash`) can process them.
```json
{
"model": "openai/gpt-4o",
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "Describe this image in detail." },
{
"type": "image_url",
"image_url": {
"url": "https://image.civitai.com/.../photo.jpeg",
"detail": "auto"
}
}
]
}],
"max_tokens": 300
}
```
`detail` can be `"auto"` (default), `"low"`, or `"high"`. The image source can be a public URL, a data URL (`data:image/jpeg;base64,...`), or raw Base64 — the orchestrator uploads it to blob storage before dispatching the job.
## Image generation
Set `"modalities": ["image", "text"]` on the request to generate images through `/v1/chat/completions`. The response carries an `images` array on the assistant message, where each entry is a base64 data URI — the same shape OpenRouter uses, so existing OpenRouter-style SDK code works unmodified.
```http
POST https://orchestration.civitai.com/v1/chat/completions
Authorization: Bearer
Content-Type: application/json
{
"model": "google/gemini-2.5-flash-image",
"messages": [
{ "role": "user", "content": "A cat in a teacup, soft window light" }
],
"modalities": ["image", "text"],
"image_config": {
"aspect_ratio": "1:1",
"image_size": "1K"
}
}
```
Response:
```json
{
"id": "chatcmpl-...",
"model": "google/gemini-2.5-flash-image",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"images": [{
"type": "image_url",
"image_url": { "url": "data:image/png;base64,iVBOR..." }
}]
},
"finish_reason": "stop"
}]
}
```
### Image editing (multi-turn)
Pass a prior generated image (or any image URL / data URI) as a content part on a user message and the request routes through the engine's edit operation:
```json
{
"model": "google/gemini-2.5-flash-image",
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "Make it a dog instead of a cat." },
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }
]
}],
"modalities": ["image", "text"]
}
```
### Supported models
| `model` | Engine | Operations |
|---------|--------|------------|
| `google/gemini-2.5-flash-image` | Gemini 2.5 Flash Image | create, edit |
| `openai/gpt-image-1` | OpenAI gpt-image-1 | create, edit |
| `openai/dall-e-3` | OpenAI DALL·E 3 | create |
| `openai/dall-e-2` | OpenAI DALL·E 2 | create, edit |
| `black-forest-labs/flux.2-dev` | Flux 2 Dev | create, edit |
| `black-forest-labs/flux.2-flex` | Flux 2 Flex | create, edit |
| `black-forest-labs/flux.2-pro` | Flux 2 Pro | create, edit |
| `black-forest-labs/flux.2-max` | Flux 2 Max | create, edit |
| `black-forest-labs/flux.2-klein` | Flux 2 Klein | create, edit |
The provider prefix (`google/`, `openai/`, `black-forest-labs/`) is optional — short names like `gemini-2.5-flash-image`, `gpt-image-1`, `flux-2-dev` are also accepted. Unknown model names with `modalities: ["image"]` return `400` with the supported list.
### Civitai AIR URNs
Pass a Civitai [AIR](/site/guide/air) URN as `model` to use a community checkpoint. The ecosystem segment of the AIR (`sd1`, `sdxl`, `flux1`, `anima`) selects the engine; the AIR is forwarded as the checkpoint:
```json
{
"model": "urn:air:sdxl:checkpoint:civitai:101055@128078",
"messages": [{ "role": "user", "content": "A cyberpunk samurai" }],
"modalities": ["image", "text"],
"image_config": { "aspect_ratio": "1:1", "image_size": "1K" }
}
```
| Ecosystem | Engine | Operations | Notes |
|-----------|--------|------------|-------|
| `sd1` | SD 1.5 (sd-cpp) | create, variant | Pass an input image to trigger img2img variant. |
| `sdxl` | SDXL (sd-cpp) | create, variant | Same — img2img variant when an input image is supplied. |
| `flux1` | Flux 1 (sd-cpp) | create, edit | Edit operation accepts up to 2 input images; width/height clamped to 832–1216. |
| `anima` | Anima (sd-cpp) | create | Anima checkpoints; no img2img path through chat-completions. |
Other ecosystems (`zimage`, `qwen`, `wan`, `flux2`) hardcode their checkpoints — pass the matching named model instead (e.g. `flux-2-dev`) and use the [`imageGen` workflow step](./flux2) directly when you need to override the checkpoint.
### `image_config`
| Field | Values | Effect |
|-------|--------|--------|
| `aspect_ratio` | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `21:9` | Sets width/height ratio. OpenAI engines snap to their nearest allowed size. Gemini ignores (always 1024×1024). |
| `image_size` | `0.5K`, `1K`, `2K`, `4K` | Approximate megapixel target. Engines clamp to their supported range. |
| `n` | 1–10 | Number of images. Falls back to the top-level `n`. Engines clamp to their supported max. |
For full per-engine knobs (samplers, LoRAs, guidance scales, advanced operations), use the [`imageGen` workflow step](./flux2) directly instead — chat-completions is a thin facade tuned for SDK compatibility, not a full passthrough of every engine parameter.
## Multi-turn conversations
Include prior turns as `assistant` messages to maintain context:
```json
{
"model": "openai/gpt-4o-mini",
"messages": [
{ "role": "system", "content": "You are a concise assistant." },
{ "role": "user", "content": "Write a haiku about the ocean." },
{ "role": "assistant", "content": "Waves crash endlessly,\nSalt..." },
{ "role": "user", "content": "Now write one about mountains." }
],
"temperature": 0.7
}
```
## Streaming
### Via `/v1/chat/completions`
Set `"stream": true` and handle Server-Sent Events (SSE). The response is a stream of `data: {...}` lines ending with `data: [DONE]`:
```http
POST https://orchestration.civitai.com/v1/chat/completions
Authorization: Bearer
Content-Type: application/json
{
"model": "openai/gpt-4o-mini",
"messages": [{ "role": "user", "content": "Tell me a short story." }],
"stream": true
}
```
### Via workflow step
Set `stream: true` in the step `metadata` field:
```json
{
"steps": [{
"$type": "chatCompletion",
"metadata": { "stream": true },
"input": {
"model": "openai/gpt-4o-mini",
"messages": [{ "role": "user", "content": "Tell me a short story." }]
}
}]
}
```
When streaming is enabled, the orchestrator stores the raw NDJSON chunks in a streaming blob and assembles them into the standard `ChatCompletionOutput` shape for the workflow output.
## Tool use (function calling)
Define tools as JSON Schema function definitions. The model decides when and how to call them:
```json
{
"model": "openai/gpt-4o",
"messages": [
{ "role": "user", "content": "What is the weather in Paris?" }
],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}
}
}],
"tool_choice": "auto"
}
```
When the model calls a tool, the assistant message in the response contains a `tool_calls` array instead of (or alongside) `content`. Submit the tool result back as a `tool` message:
```json
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"temperature\": 18, \"condition\": \"sunny\"}"
}
```
## Model selection
`model` accepts any string that identifies a model on OpenRouter or a Civitai AIR URI:
| Format | Example | Notes |
|--------|---------|-------|
| OpenRouter ID | `openai/gpt-4o-mini` | Any model from [openrouter.ai/models](https://openrouter.ai/models). |
| OpenAI shorthand | `gpt-4o`, `gpt-4o-mini` | OpenRouter also accepts bare OpenAI model names. |
| AIR URI | `urn:air:llm:model:civitai:@` | Routes to a Civitai-hosted model. |
## Parameters reference
| Field | Default | Notes |
|-------|---------|-------|
| `model` | — ✅ | Model ID (OpenRouter) or AIR URI. |
| `messages` | — ✅ | Array of role-discriminated messages (at least 1). |
| `temperature` | `1` | 0–2. Higher = more random output. |
| `topP` | `1` | 0–1. Nucleus sampling. Alternative to `temperature`; usually set one or the other. |
| `maxTokens` | `null` | Max output tokens, 1–128 000. Unlimited when omitted. |
| `n` | `1` | Number of completions to generate, 1–128. |
| `stop` | `null` | Up to 4 stop sequences. |
| `presencePenalty` | `0` | -2 to 2. Positive values discourage repeating topics. |
| `frequencyPenalty` | `0` | -2 to 2. Positive values discourage repeating exact tokens. |
| `seed` | `null` | Integer seed for deterministic output (beta). |
| `user` | `null` | End-user identifier for abuse monitoring. |
| `logprobs` | `null` | Return log probabilities for generated tokens. |
| `topLogprobs` | `null` | 0–20. Number of top log-prob candidates per token (requires `logprobs: true`). |
| `tools` | `null` | Function definitions available to the model. |
| `tool_choice` | `null` | `"auto"`, `"none"`, `"required"`, or `{ "type": "function", "function": { "name": "..." } }`. |
| `chatTemplateKwargs` | `null` | Extra kwargs passed to the model's chat template (vLLM-specific). |
| `modalities` | `null` | Output modalities. Include `"image"` to route the request through the image-generation pipeline. See [Image generation](#image-generation). |
| `imageConfig` | `null` | Image-generation parameters (`aspect_ratio`, `image_size`, `n`). Only consulted when `modalities` includes `"image"`. |
## Messages reference
Messages are discriminated by the `role` field:
### `system`
```json
{ "role": "system", "content": "You are a helpful assistant.", "name": "optional" }
```
### `user`
Content can be a plain string or an array of content parts:
```json
{ "role": "user", "content": "Plain text" }
```
```json
{
"role": "user",
"content": [
{ "type": "text", "text": "What's in this image?" },
{ "type": "image_url", "image_url": { "url": "https://...", "detail": "auto" } }
]
}
```
### `assistant`
```json
{ "role": "assistant", "content": "Prior response text." }
```
Or with tool calls (as returned by the model):
```json
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" }
}]
}
```
### `tool`
```json
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"temperature\": 18}"
}
```
## Reading the result
The output is an OpenAI-compatible `chat.completion` object:
```json
{
"status": "succeeded",
"steps": [{
"name": "0",
"$type": "chatCompletion",
"status": "succeeded",
"output": {
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1748000000,
"model": "openai/gpt-4o-mini",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "The capital of France is Paris."
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 24,
"completion_tokens": 9,
"total_tokens": 33
}
}
}]
}
```
The `/v1/chat/completions` endpoint returns the `output` object directly (not wrapped in a workflow envelope).
## Cost
Cost depends on whether the model routes through OpenRouter or is a Civitai AIR model.
### OpenRouter models
Cost is computed from actual token usage with a **30% margin**, converted to Buzz (1 000 Buzz = 1 USD):
```
buzzCost = actualCostUsd × 1000 × 1.3 (minimum 1 Buzz)
```
Before execution, the orchestrator estimates cost using OpenRouter's published per-token prices. After execution, the final Buzz charge is based on the tokens actually consumed by the model.
Different models have very different per-token prices — check [openrouter.ai/models](https://openrouter.ai/models) for current pricing. Representative examples:
| Model | Input (per 1M tokens) | Output (per 1M tokens) | Typical single call |
|-------|-----------------------|------------------------|---------------------|
| `openai/gpt-4o-mini` | $0.15 | $0.60 | < 1 Buzz |
| `openai/gpt-4o` | $2.50 | $10.00 | 2–15 Buzz |
| `anthropic/claude-3-5-sonnet` | $3.00 | $15.00 | 4–20 Buzz |
| `meta-llama/llama-3.3-70b-instruct` | $0.12 | $0.30 | < 1 Buzz |
Use `whatif=true` on your first request to get an exact preview before committing.
### AIR models (Civitai-hosted)
Flat-rate pricing based on image count and number of completions requested:
```
total = 1 × (imageCount × 2) × n
```
::: warning Known limitation
For text-only requests to AIR models (`imageCount = 0`), the `images` factor collapses the product to **0 Buzz**. This is a known bug — expect it to be corrected in a future release. For now, AIR model text-only calls cost 0 Buzz.
:::
## Runtime
Most chat completions finish in 5–30 seconds depending on model and output length. Use `wait=60` for simple requests; add `wait=0` + polling for long outputs, large `n`, or slow models. The `/v1/chat/completions` endpoint waits up to 60 seconds before timing out with `504`.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `400` with "messages must not be empty" | Empty `messages` array | Include at least one message. |
| `400` with "model is required" | Missing `model` field | `model` is always required. |
| `504 Gateway Timeout` (via `/v1`) | Slow model or long output | Retry with `wait=0` via `SubmitWorkflow` + polling. |
| `400` with "topLogprobs requires logprobs" | Sent `topLogprobs` without `logprobs: true` | Set `"logprobs": true` alongside `topLogprobs`. |
| Response truncated mid-sentence | `maxTokens` reached | Raise `maxTokens` or omit it to let the model decide. |
| Tool call in response instead of content | Expected behaviour | The model chose to call a tool — feed the `tool_calls` back as a `tool` message in the next turn. |
| Step `failed`, `reason = "no_provider_available"` | AIR model offline or no worker available | Retry shortly. |
## Related
* [`SubmitWorkflow`](/orchestration/reference/operations/SubmitWorkflow) — for the workflow-step path
* [`GetWorkflow`](/orchestration/reference/operations/GetWorkflow) — for polling
* [Results & webhooks](/orchestration/guide/results-and-webhooks) — production result handling
* [Prompt enhancement](./prompt-enhancement) — uses a `chatCompletion`-like step to rewrite image prompts
* [Image conversion](./convert-image) — 1-Buzz utility step to post-process generated images
---
---
url: /orchestration/recipes/training-other-image.md
---
# Chroma / ERNIE / Qwen / Z-Image LoRA training
Five smaller image-LoRA ecosystems share this page: each has its own `ecosystem` value and base checkpoint, but the request shape is otherwise the AI Toolkit standard.
| `ecosystem` | Base | Default price | Best for |
|-------------|------|---------------|----------|
| `chroma` | `lodestones/Chroma1-HD` | 2000 Buzz | Chroma community model fine-tunes |
| `ernie` | `baidu/ERNIE-Image` | 1000 Buzz | ERNIE Image LoRAs |
| `qwen` | Qwen-Image (versioned) | 2000 Buzz | Qwen Image / Qwen-Image-Edit LoRAs |
| `zimageturbo` | `ostris/Z-Image-De-Turbo` (+ Z-Image-Turbo extras) | 1000 Buzz | Z-Image Turbo LoRAs (cheap, fast inference) |
| `zimagebase` | `Tongyi-MAI/Z-Image` | 1000 Buzz | Z-Image base LoRAs |
Each ecosystem has its own subsection with a runnable example. The shared schema lives in [Common parameters](#common-parameters); ecosystem-specific quirks are in each subsection.
::: tip Long-running step
Always submit with `wait=0`. These ecosystems run anywhere from a fraction of a second per step (Z-Image Turbo) to ~1s/step (Chroma/Qwen). See [Results & webhooks](/orchestration/guide/results-and-webhooks).
:::
## The request shape
```json
{
"$type": "training",
"input": {
"engine": "ai-toolkit",
"ecosystem": "chroma" // chroma | ernie | qwen | zimageturbo | zimagebase
}
}
```
## Prerequisites
* A Civitai orchestration token ([Quick start → Prerequisites](/orchestration/guide/getting-started#prerequisites))
* A training-data zip (signed R2 URL, Civitai R2 AIR, or any HTTPS URL)
* An accurate `count` of images in the zip
## Chroma
Trains on the Chroma1-HD base. Uses [`TextToImageV2Job`](/orchestration/reference/) for sample renders; output LoRA is usable wherever Chroma is supported.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"tags": ["training"],
"steps": [{
"$type": "training",
"priority": "normal",
"retries": 2,
"input": {
"engine": "ai-toolkit",
"ecosystem": "chroma",
"steps": 2000,
"resolution": 1024,
"lr": 0.0001,
"trainTextEncoder": false,
"lrScheduler": "cosine",
"optimizerType": "adamw8bit",
"networkDim": 16,
"networkAlpha": 16,
"trainingData": {
"type": "zip",
"sourceUrl": "https://civitai-delivery-worker-prod.5ac0637cfd0766c97916cefa3764fbdf.r2.cloudflarestorage.com/training-images/5418/2382561TrainingData.B6Tr.zip",
"count": 10
},
"samples": {
"prompts": [
"woman with red hair, playing chess at the park, dramatic explosion in background",
"a woman holding a coffee cup, in a beanie, sitting at a cafe",
"a horse acting as a DJ at a night club, fisheye lens, smoke machine, laser lights"
]
}
}
}]
}
```
Chroma defaults: `networkDim: 16`, `optimizerType: adamw8bit`, `trainTextEncoder: false`, `lrScheduler: cosine`. Default price 2000 Buzz.
## ERNIE
Trains on Baidu's ERNIE-Image. Comfy-based ecosystem with built-in diffuser. Uses [`ComfyImageGenJob`](/orchestration/reference/) for sample renders.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"tags": ["training"],
"steps": [{
"$type": "training",
"priority": "normal",
"retries": 2,
"input": {
"engine": "ai-toolkit",
"ecosystem": "ernie",
"steps": 2000,
"lr": 0.0001,
"trainTextEncoder": false,
"lrScheduler": "cosine",
"optimizerType": "adamw8bit",
"networkDim": 32,
"networkAlpha": 32,
"trainingData": {
"type": "zip",
"sourceUrl": "urn:air:other:other:civitai-r2:civitai-delivery-worker-prod@training-images/7918795/2435272TrainingData.bJ7P.zip",
"count": 10
},
"samples": {
"prompts": ["a portrait of TOK", "TOK walking through a comic book city"]
}
}
}]
}
```
ERNIE defaults: `networkDim: 32`, `optimizerType: adamw8bit`, `trainTextEncoder: false`, `lrScheduler: cosine`. Default price 1000 Buzz.
## Qwen
Trains on Qwen-Image. The `version` field selects a specific Qwen-Image release:
| `version` | Base resolved to |
|-----------|------------------|
| `latest` (default) | `Qwen/Qwen-Image-Edit-2512` |
| `2509` | `urn:air:qwen:checkpoint:civitai:1864281@2110043` |
| `2512` | `Qwen/Qwen-Image-Edit-2512` (same as `latest`) |
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"tags": ["training"],
"steps": [{
"$type": "training",
"priority": "normal",
"retries": 2,
"input": {
"engine": "ai-toolkit",
"ecosystem": "qwen",
"version": "latest",
"steps": 2000,
"resolution": 1024,
"lr": 0.00011,
"trainTextEncoder": false,
"lrScheduler": "cosine",
"optimizerType": "adamw8bit",
"networkDim": 16,
"networkAlpha": 16,
"trainingData": {
"type": "zip",
"sourceUrl": "urn:air:other:other:civitai-r2:civitai-delivery-worker-prod@training-images/3315022/2526079TrainingData.o4S8.zip",
"count": 10
},
"samples": {
"prompts": [
"woman with red hair, playing chess at the park, dramatic explosion in background",
"a woman holding a coffee cup, in a beanie, sitting at a cafe"
]
}
}
}]
}
```
Qwen defaults: `networkDim: 16`, `optimizerType: adamw8bit`, `trainTextEncoder: false`, `lrScheduler: cosine`. Default price 2000 Buzz.
## Z-Image Turbo
Trains on `ostris/Z-Image-De-Turbo` and pulls in the original `Tongyi-MAI/Z-Image-Turbo` as an extras model. Output LoRA is usable in [Z-Image generation](./zimage) on the `turbo` model.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"tags": ["training"],
"steps": [{
"$type": "training",
"priority": "normal",
"retries": 2,
"input": {
"engine": "ai-toolkit",
"ecosystem": "zimageturbo",
"steps": 2000,
"resolution": 512,
"lr": 0.000611,
"trainTextEncoder": false,
"lrScheduler": "cosine",
"optimizerType": "adamw8bit",
"networkDim": 32,
"networkAlpha": 32,
"trainingData": {
"type": "zip",
"sourceUrl": "urn:air:other:other:civitai-r2:civitai-delivery-worker-prod@training-images/3315022/2526079TrainingData.o4S8.zip",
"count": 10
},
"samples": {
"prompts": ["a photo of TOK", "TOK in a garden", "TOK portrait"]
}
}
}]
}
```
Z-Image Turbo defaults: `networkDim: 32`, `optimizerType: adamw8bit`, `trainTextEncoder: false`. Default price 1000 Buzz.
## Z-Image Base
Trains on `Tongyi-MAI/Z-Image`. The orchestrator overrides `optimizerType` to `automagic` and `lr` to `0.000001` regardless of what you submit — the input fields are accepted but ignored. Use the [Z-Image Turbo](#z-image-turbo) recipe instead unless you specifically need a base-model LoRA.
```http
POST https://orchestration.civitai.com/v2/consumer/workflows?wait=0
Authorization: Bearer
Content-Type: application/json
{
"tags": ["training"],
"steps": [{
"$type": "training",
"priority": "normal",
"retries": 2,
"input": {
"engine": "ai-toolkit",
"ecosystem": "zimagebase",
"steps": 2000,
"resolution": 512,
"lr": 0.000611,
"trainTextEncoder": false,
"lrScheduler": "cosine",
"networkDim": 32,
"networkAlpha": 32,
"trainingData": {
"type": "zip",
"sourceUrl": "urn:air:other:other:civitai-r2:civitai-delivery-worker-prod@training-images/3315022/2526079TrainingData.o4S8.zip",
"count": 10
},
"samples": {
"prompts": ["a photo of TOK", "TOK in a garden", "TOK portrait"]
}
}
}]
}
```
Z-Image Base defaults: `networkDim: 32`, `optimizerType: automagic` (overridden), `lr: 0.000001` (overridden), `trainTextEncoder: false`. Default price 1000 Buzz.
## Common parameters {#common-parameters}
Defaults shown are the post-`ApplyDefaults` values; per-ecosystem deviations are noted above.
| Field | Required | Default | Notes |
|-------|----------|---------|-------|
| `engine` | ✅ | — | Always `ai-toolkit`. |
| `ecosystem` | ✅ | — | One of: `chroma`, `ernie`, `qwen`, `zimageturbo`, `zimagebase`. |
| `version` | (qwen only) | `latest` | `latest`, `2509`, `2512`. Selects the Qwen-Image base release. |
| `steps` | | `2000` | `1`–`10000`. Total training steps. Primary driver of training length and pricing. |
| `epochs` | | `10` | `1`–`20`. Number of saved checkpoints delivered, each separately downloadable. Each adds 10 Buzz of storage. |
| `batchSize` | | `1` | Defaults to 1. For Z-Image and ERNIE, raise it up to the ecosystem maximum (**2**) to train faster at the cost of more GPU memory; a larger batch needs fewer steps. For Chroma and Qwen it is fixed at 1. Values above the max are clamped. |
| `continueFrom` | | *(none)* | A previously-trained LoRA AIR (`urn:air::lora:...`) to resume from (see [Continue training](#continue-training)). Must be a LoRA of the same ecosystem. |
| `lr` | | `0.0001` | UNet learning rate. |
| `trainTextEncoder` | | `false` | All five ecosystems leave the text encoder frozen. |
| `lrScheduler` | | `cosine` | `constant`, `constant_with_warmup`, `cosine`, `linear`, `step`. |
| `optimizerType` | | `adamw8bit` (`automagic` for Z-Image Base) | Full enum on the [SDXL/SD1 page](./training-sdxl-sd1#common-parameters). |
| `networkDim` | | `32` (`16` for Chroma / Qwen) | `1`–`256`. |
| `networkAlpha` | | matches `networkDim` | `1`–`256`. |
| `noiseOffset` | | `0` | `0`–`1`. |
| `flipAugmentation` | | `false` | Random horizontal flips. |
| `shuffleTokens` / `keepTokens` | | `false` / `0` | Caption-tag shuffling. |
| `triggerWord` | | *(none)* | Activation token. Recommended for character / style LoRAs on Chroma, Z-Image. |
| `trainingData.{type, sourceUrl, count}` | ✅ | — | `type: "zip"`. |
| `samples.prompts[]` | | `[]` | Preview prompts rendered at each saved checkpoint with the trained LoRA. |
| `samples.negativePrompt` | | *(none)* | — |
| `samples.cfgScale` | | *(ecosystem default)* | Overrides the CFG / guidance scale used when rendering the preview samples. |
| `samples.strength` | | `1.0` | Trained-LoRA weight applied in the preview samples. |
## Continue training / train further {#continue-training}
To resume from a LoRA you already trained instead of starting from the base checkpoint, set `continueFrom` to that LoRA's AIR. The new run starts from those weights and the new epochs build on top:
```json
{
"$type": "training",
"input": {
"engine": "ai-toolkit",
"ecosystem": "chroma",
"continueFrom": "urn:air:chroma:lora:civitai:@",
"steps": 1000
}
}
```
`continueFrom` must point at a LoRA of the **same ecosystem** as the model being trained (a `chroma` LoRA for `ecosystem: "chroma"`, a `qwen` LoRA for `ecosystem: "qwen"`, and so on) — a mismatched ecosystem is rejected.
## Reading the result
Same envelope as the other training recipes — see [SDXL/SD1 → Reading the result](./training-sdxl-sd1#reading-the-result). Each saved checkpoint yields a `.safetensors` LoRA blob plus any sample images.
The trained LoRA is usable in the corresponding generation recipe — Chroma LoRAs in any Chroma workflow, ERNIE LoRAs in [ERNIE image generation](./ernie), Qwen LoRAs in [Qwen image generation](./qwen), Z-Image LoRAs in [Z-Image generation](./zimage).
## Runtime
Per-step wall time, default settings on a 10-image dataset:
| Ecosystem | Per-step | Typical full run |
|-----------|----------|-------------------|
| `chroma` | ~0.6–1.2 s | 10–30 min for 2000 steps |
| `ernie` | ~0.3–0.6 s | 6–16 min for 2000 steps |
| `qwen` | ~0.6–1.2 s | 10–30 min for 2000 steps |
| `zimageturbo` | ~0.1–0.25 s | 2–8 min for 2000 steps |
| `zimagebase` | ~0.1–0.25 s | 2–8 min for 2000 steps |
Always use `wait=0`.
## Cost
Training is billed per **step** plus a flat per-**epoch** storage surcharge, with a price floor:
```
price = steps × costPerStep + epochs × 10 (rounded)
floor: never less than 80% of the default-configuration price
```
`epochs` is the number of saved checkpoints delivered (default `10`, range `1`–`20`); each adds 10 Buzz of storage. The default run is **2000 steps / 10 epochs**, which lands each ecosystem on its default price. Lowering `steps` or `epochs` can save at most 20% (the floor).
| Ecosystem | `costPerStep` | Default price (2000 steps, 10 epochs) | Floor (80%) |
|-----------|---------------|----------------------------------------|-------------|
| `chroma` | 0.95 | 2000 | 1600 |
| `ernie` | 0.45 | 1000 | 800 |
| `qwen` | 0.95 | 2000 | 1600 |
| `zimageturbo` | 0.45 | 1000 | 800 |
| `zimagebase` | 0.45 | 1000 | 800 |
Sample-prompt rendering is billed separately at each ecosystem's image-generation rate. Use `whatif=true` (the **Preview cost** button on the widgets above) to confirm exact charges before submitting.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `400` with "ecosystem unknown" | Typo, or not one of `chroma` / `ernie` / `qwen` / `zimageturbo` / `zimagebase` | Check spelling. |
| `400` with "version not allowed" (Qwen only) | `version` not one of `latest` / `2509` / `2512` | Use one of the listed values. |
| Z-Image Base: `optimizerType` you set seems ignored | Intentional — `ApplyDefaults` overrides to `automagic` | Use Z-Image Turbo if you need full optimizer control. |
| Trained LoRA underbaked | Too few steps / too low `lr` | Raise `steps` (these ecosystems often need more steps than SDXL); keep `lr` ≤ `5e-4`. |
| Trained LoRA overcooked | Too many steps or `networkDim` too high | Drop `networkDim` to 16, lower `steps`. |
| Step `failed`, `moderationStatus: "Rejected"` | Dataset failed content moderation | Replace flagged images. |
## Related
* [SDXL & SD1 LoRA training](./training-sdxl-sd1) — classic Stable Diffusion ecosystems
* [Flux 1 LoRA training](./training-flux1) / [Flux 2 Klein LoRA training](./training-flux2-klein) — Flux family
* [Wan video LoRA training](./training-wan) / [LTX2 video LoRA training](./training-ltx2) — video LoRAs
* Generation recipes for these ecosystems: [Z-Image](./zimage), [Qwen](./qwen), [ERNIE](./ernie)
* [Results & webhooks](/orchestration/guide/results-and-webhooks)
* [`SubmitWorkflow`](/orchestration/reference/operations/SubmitWorkflow) / [`GetWorkflow`](/orchestration/reference/operations/GetWorkflow)
* [Endpoint OpenAPI spec](https://orchestration.civitai.com/v2/consumer/recipes/training/openapi.yaml)
---
---
url: /site.md
description: 'REST API for browsing models, images, creators, and tags on civitai.com.'
---
# Civitai Site API
The Civitai site exposes a public REST API at `https://civitai.com/api/v1/...` for
browsing models, model versions, images, creators, and tags. It's the same
surface that powers third-party tools like Stable Diffusion downloaders and
metadata lookup utilities.
This is **not** the Orchestration API. If you want to *submit* generation work,
see the [Orchestration docs](/orchestration/).
## Where to start
* **[Guide](./guide/)** — authentication, pagination, error handling, and the
AIR (AI Resource Identifier) format.
* **[Reference](./reference/)** — per-resource documentation for every public
endpoint (models, images, articles, collections, creators, tags, users),
sourced directly from the current Next.js handlers.
* **[CLI](./guide/cli)** — search and fetch these endpoints from the terminal
with the `civitai` CLI read commands.
## Quick example
```bash
# Public — no auth required
curl "https://civitai.com/api/v1/models?limit=1&types=LORA"
# Authenticated — pass a Civitai API token
curl -H "Authorization: Bearer $CIVITAI_TOKEN" \
"https://civitai.com/api/v1/me"
```
See [Getting started](./guide/getting-started) for a full walkthrough.
---
---
url: /site/guide/cli.md
description: >-
Browse, search, and download from Civitai in the terminal with the civitai CLI
— read commands plus the authenticated download command.
---
# CLI
The **`civitai` CLI** (Go, repo [`civitai/cli`](https://github.com/civitai/cli))
wraps the [public site API](../reference/) so you can search and fetch models,
images, articles, collections, and more straight from the terminal or a script —
and **download** a model version's file(s) with type-aware folder routing and
automatic SHA256 verification.
::: tip Read = anonymous, download = authenticated
The **read/search** commands (`models`, `model-versions`, `images`, `articles`,
`collections`, `tags`, `creators`, `users`) hit **public** endpoints and work
with no login; pass `--anon` to force a token-free request. The **`download`**
command is different: Civitai requires a token for **any** model-file download —
even a small public file — so `civitai download` needs [`civitai login`](#authentication).
:::
## Install
```bash
# npm (a thin wrapper that downloads the matching prebuilt binary)
npm install -g @civitai/cli
# or, without installing:
npx @civitai/cli --help
# Homebrew (macOS / Linux)
brew install civitai/tap/civitai
# Go install (from source, Go 1.25+)
go install github.com/civitai/cli/cmd/civitai@latest
# Nix flake — run without installing:
nix run github:civitai/cli -- --help
# …or install into your profile:
nix profile install github:civitai/cli
```
To pin the CLI as a flake input (reproducible builds/CI), reference it in your
`flake.nix`:
```nix
{
inputs.civitai-cli.url = "github:civitai/cli";
# …or pin a release tag from the Releases page:
# inputs.civitai-cli.url = "github:civitai/cli/";
outputs = { self, nixpkgs, civitai-cli, ... }: {
# add `civitai-cli.packages.${system}.default` to your devShell / packages
};
}
```
Prebuilt binaries for linux/macOS/windows × amd64/arm64 are on the
[GitHub Releases](https://github.com/civitai/cli/releases) page. Verify with
`civitai version`.
Logging in is **optional** for the read commands below and **required** for
[`download`](#download) (see [Authentication](#authentication)). Logging in is
also required for the App Blocks authoring commands (`civitai app …`, documented
in the [Apps CLI reference](/apps/reference/cli)).
## Shared flags
**Every** read subcommand accepts `--json` — print the raw API JSON response
(for scripting) instead of the formatted table — and `--anon`, which forces an
anonymous request, ignoring any stored login token. That holds across all 13
read commands with no exceptions, which is why it is stated once here instead of
repeated per command; the per-command flag lists live in the
[generated CLI reference](/apps/reference/cli#command-reference).
Search commands paginate with `--limit` and either `--page` (shallow) or
`--cursor` (deep paging — copy `metadata.nextCursor` from a previous response).
See [Pagination](./pagination).
## Read commands
### Models
```bash
civitai models search --query "pony" --limit 5
civitai models search --type LORA --sort "Most Downloaded" --period Month
civitai models search --base-model Pony --base-model "Illustrious"
civitai models get 827184
```
`models search` flags: `--query`, `--tag`, `--username`, `--type`
(e.g. `Checkpoint`, `LORA`, `TextualInversion`), `--base-model` (**repeatable**),
`--sort` (e.g. `"Highest Rated"`, `"Most Downloaded"`, `Newest`), `--period`
(`AllTime`, `Year`, `Month`, `Week`, `Day`), `--nsfw`, `--limit` (1–100),
`--page`, `--cursor`. See [Models](../reference/models).
::: tip Filtering by architecture / video models
`--base-model` filters by base-model family and is the way to disambiguate
models that share a `--type`. Video checkpoints all report `--type Checkpoint`,
so filter them with e.g. `--base-model "Wan Video 2.2 T2V-A14B"`.
:::
### Model versions
```bash
civitai model-versions get 2514310
civitai model-versions by-hash 5D8D26E2A6
```
Aliases: `model-version`, `mv`. `by-hash` looks a version up by any of its file
hashes (AutoV1, AutoV2, SHA256, CRC32, BLAKE3), matched case-insensitively.
See [Model versions](../reference/model-versions).
### Images
```bash
civitai images search --limit 5
civitai images search --model-version-id 128713 --sort Newest
civitai images search --username some-user --cursor
```
`images search` flags: `--limit` (1–200), `--page`, `--cursor`, `--post-id`,
`--model-id`, `--model-version-id`, `--username`, `--period`, `--sort`
(`"Most Reactions"`, `"Most Comments"`, `Newest`), `--nsfw`.
See [Images](../reference/images).
### Tags
```bash
civitai tags search --query anime --limit 10
```
`tags search` flags: `--query`, `--limit`, `--page`.
See [Tags](../reference/tags).
### Creators
```bash
civitai creators search --query artist --limit 10
```
`creators search` flags: `--query`, `--limit`, `--page`.
See [Creators](../reference/creators).
### Users
```bash
civitai users get some-username
```
`users get ` resolves a user through the public user search
(`GET /api/v1/users`) — the public route is a fuzzy search, so a lookup returns
the closest match. See [Users](../reference/users).
### Articles
```bash
civitai articles search --query "comfyui" --limit 5
civitai articles search --sort "Most Reactions" --nsfw
civitai articles get 15342
civitai articles get 15342 --content
```
`articles search` flags: `--query` (matches the title), `--tags` (comma-separated
tag **IDs**, e.g. `5,12`), `--username`, `--sort` (`Newest`,
`"Recently Updated"`, `"Most Reactions"`, `"Most Comments"`, `"Most Bookmarks"`,
`"Most Collected"`), `--nsfw`, `--limit` (1–100), `--cursor`.
`articles get ` prints the article's metadata by default. Pass `--content`
to also render the article **body** — the actual guide — as readable
text/markdown (headings, paragraphs, lists, links, code blocks; HTML stripped
and entities decoded). `--json` returns the raw API body and takes precedence
over `--content`. See [Articles](../reference/articles).
### Collections
```bash
civitai collections search --query "anime" --limit 5
civitai collections search --sort Newest --cursor
civitai collections get 1201
```
`collections search` flags: `--query` (matches the name), `--sort`
(`Newest`, `"Most Followers"`), `--nsfw`, `--limit` (1–100), `--cursor`
(`Newest` sort only). See [Collections](../reference/collections).
## Download
Download the file(s) of a model **version** from Civitai.
```bash
# by version id
civitai download 128713
# resolve a MODEL's default (first published) version instead
civitai download --model 4384 --out ./dreamshaper.safetensors
# preview the plan (files, sizes, SHA256, target paths, auth) without transferring
civitai download 128713 --dry-run
```
::: warning Downloads require authentication
Civitai requires a token to download **any** model file — even a small public
one (a 336 KB public embedding returns `401` just like a gated checkpoint). Run
[`civitai login`](#authentication) first; your stored token or `CIVITAI_API_KEY`
is sent automatically. `--anon` is meaningful for the read commands but **not**
for downloads (they still `401` without a token).
:::
By default the version's **primary** file is written into the current directory
under its server-provided name. Downloads stream to `.part` and are
renamed into place only on success, so an interrupted run never leaves a
truncated final file.
**Any file type downloads** — model weights, but also non-weights deliverables
like a `Workflows` model's Archive, training data, or other artifacts.
### Selecting files
```bash
civitai download 128713 --file vae --out-dir ./models
civitai download 128713 --all --out-dir ./models
```
Use `--file` to pick a specific file (exact match, else a unique
case-insensitive substring) or `--all` to download every file in the version.
### Folder routing for apps
```bash
civitai download 128713 --all --layout comfyui --root ~/ComfyUI
civitai download 128713 --layout a1111 --root ~/stable-diffusion-webui
```
`--layout ` routes each file into the correct subfolder for that
app, **by file/model type** — so `--all` fans a bundled VAE into the VAE folder
instead of polluting the checkpoint folder, and LoRAs/embeddings land in their
own directories. `--root ` (default `.`) is the base directory for routing.
`--layout` is mutually exclusive with `--out`/`--out-dir`.
### Base-model compatibility check
```bash
civitai download 128713 --layout a1111 --for-base "SDXL 1.0"
```
`--for-base ""` warns on stderr when the version's base model is in a
confidently different family than your target (e.g. an SD 1.5 embedding for an
SDXL model). The version's base model is always shown regardless.
### Integrity
The streamed bytes are **verified against the file's SHA256 by default**. A hash
mismatch deletes the partial file and fails the run. Pass `--no-verify` to skip
(a file with no published SHA256 is downloaded with a warning either way).
### Download flags
The complete, always-current flag list for `download` — and for every other
command — is in the [generated CLI reference](/apps/reference/cli#cli-download),
which is built from the binary's own help output. `civitai download --help`
prints the same thing locally.
## Authentication
The read commands don't need it. **`download` and the App Blocks authoring
commands do.** Authenticate once:
```bash
civitai login # browser device login (recommended)
civitai login --token # or a personal API key from civitai.com/user/account
civitai whoami # show the current identity and capabilities
```
`civitai login` runs a browser-based device login and stores short-lived OAuth
tokens that refresh automatically; the credential is saved owner-readable to
`~/.config/civitai/config.yaml`. The `CIVITAI_TOKEN` environment variable
overrides the stored credential. Running `civitai login` again switches the
active account (no separate logout).
For the read endpoints the result is identical to `--anon` whether or not you're
logged in.
## Scripting with `--json`
`--json` prints the **raw `/api/v1/...` REST response** — a stable passthrough,
not a shape the CLI invents. The fields are exactly the public Site API's, so
reach for the [REST reference](../reference/) (e.g. [Models](../reference/models),
[Model versions](../reference/model-versions)) for the schema instead of probing
with `jq keys`.
The output is pipe-safe by contract:
* **stdout is pure JSON** — `civitai … --json | jq -e .` always parses.
* **errors go to stderr with a non-zero exit** and **nothing on stdout**, so
`jq` never sees error prose. `civitai model-versions get 999999999 --json`
exits `1`, prints `Error: not found (404): Model not found` to stderr, and
emits an empty stdout.
### Cursor-pagination loop
Use `--cursor`, **not** `--page`, for deep paging (the API caps `page*limit` at
1000 and returns `429` beyond it — see [Pagination](./pagination)). Read
`.metadata.nextCursor` from each response, feed it back via `--cursor`, and stop
when it's absent:
```bash
export CIVITAI_NO_UPDATE_CHECK=1
cursor=""
while :; do
page=$(civitai models search --type LORA --base-model Illustrious \
--sort "Most Downloaded" --limit 5 ${cursor:+--cursor "$cursor"} --json) || break
echo "$page" | jq -r '.items[].id' # your work here
cursor=$(echo "$page" | jq -r '.metadata.nextCursor // empty')
[ -z "$cursor" ] && break # no more pages
done
```
### Clean output for pipelines
The CLI runs a background check for a newer release and prints a nag to
**stderr**. Suppress it in scripts with `CIVITAI_NO_UPDATE_CHECK=1` (env) or
`--no-update-check` (flag). The nag never touches stdout, so `--json` stays pure
regardless — silencing it just keeps your stderr/logs clean.
### Gotchas
* **SHA256 is UPPER-case** in the API/`--json` (e.g. `42BA94DF…`), whereas
`sha256sum` emits lowercase — case-fold before comparing if you verify
downloads yourself. (`civitai download`'s built-in verify is already
case-insensitive.)
* **`models search` already embeds `.modelVersions[]`** — each item carries its
versions with `files[].hashes.SHA256` and `trainedWords`, so a per-version
`model-versions get` is usually redundant when you started from a search.
* **Creator and model-level download counts are only in the search response.**
`model-versions get ` returns a version whose `.model` is just
`{name, type, nsfw, poi}` — no `creator`, no model `stats`. If you started from
a version id, fetch those from `models search`/`models get` and join on the
model id (`.modelId` on the version).
### Worked example — top LoRAs for a base model, then plan a download
Search → pick each version with `jq` → hand the id to `download` with app folder
routing. `--dry-run` prints the plan (files, sizes, hashes, targets) without
transferring, so this is safe to copy-paste:
```bash
export CIVITAI_NO_UPDATE_CHECK=1
civitai models search --type LORA --base-model Illustrious \
--sort "Most Downloaded" --limit 3 --json |
jq -r '.items[].modelVersions[0].id' |
while read -r vid; do
civitai download "$vid" --layout comfyui --root ~/ComfyUI --dry-run
done
```
Drop `--dry-run` (and run [`civitai login`](#authentication) first) to actually
fetch the files; `--layout comfyui` routes each into its ComfyUI type folder.
---
---
url: /apps/reference/cli.md
description: >-
The whole civitai CLI command tree — commands, flags, examples and the global
flags — generated from the canonical Go CLI (civitai/cli).
---
# CLI
The **`civitai` CLI** (Go, repo [`civitai/cli`](https://github.com/civitai/cli))
is the canonical command-line tool for Civitai — searching and downloading
models, running generations, and authoring Apps. Its `app` command group
scaffolds a correct project, validates it against the platform contract, and
packages + submits it for review.
::: warning This replaces the deprecated `@civitai/blocks-cli`
The old npm `@civitai/blocks-cli` (with `init` / `dev` / `deploy`) is
**deprecated**. Install the `civitai` binary instead — its authoring commands
below are the source of truth.
:::
## Install
Pick whichever fits — **npm** is the most convenient if you already have Node;
the others need no Node toolchain:
```bash
# npm (a thin wrapper that downloads the matching prebuilt binary)
npm install -g @civitai/cli
# or, without installing:
npx @civitai/cli --help
# Homebrew (macOS / Linux)
brew install civitai/tap/civitai
# Go install (from source, Go 1.25+)
go install github.com/civitai/cli/cmd/civitai@latest
# Nix flake — run without installing:
nix run github:civitai/cli -- --help
# …or install into your profile:
nix profile install github:civitai/cli
```
The repo is a [Nix flake](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html),
so `nix run` / `nix profile install` need no Go toolchain (`x86_64`/`aarch64`
Linux and macOS). To pin the CLI as a flake input — the reproducible option for
a team devShell or CI — reference it in your `flake.nix`:
```nix
{
inputs.civitai-cli.url = "github:civitai/cli";
# …or pin a release tag from the Releases page:
# inputs.civitai-cli.url = "github:civitai/cli/";
outputs = { self, nixpkgs, civitai-cli, ... }: {
# add `civitai-cli.packages.${system}.default` to your devShell / packages
};
}
```
Prebuilt binaries for linux/macOS/windows × amd64/arm64 are on the
[GitHub Releases](https://github.com/civitai/cli/releases) page. Verify with
`civitai version`.
## Authenticate
Most `app` commands that talk to the platform need a stored credential:
```bash
civitai login # browser device login
# or a full-scope personal API key (needed for real Buzz-spend in dev:live):
civitai login --token # create the key at civitai.com/user/account
```
## The App lifecycle
The authoring flow is **create → validate → submit**, then **review** on
civitai.com. The platform rebuilds your app from source — there is **no
client-side `deploy`**.
```bash
civitai app create my-app # scaffold a ready-to-build App
cd my-app
npm install && npm run dev:harness # iterate locally against the mock host
civitai app validate # local pre-check of block.manifest.json
civitai app submit # package the SOURCE tree + submit for review
civitai app status # track review / deploy state
```
`civitai app submit` enters your app into **moderator review** — it is not
published immediately. On approval the platform builds + deploys it and serves it
at `https://.civit.ai/`.
## Command reference
Every command below is generated from the binary's own help output — the whole
`civitai` command tree, not just `app`. Alongside App authoring it covers
catalog browsing and downloads (`models`, `images`, `articles`, `collections`,
`creators`, `tags`, `users`, `model-versions`, `download`), image generation
(`generate`, `workflows` — these **spend Buzz**), and account commands
(`login`, `whoami`, `buzz`, `upgrade`, `version`).
`civitai app` and its subcommands come first; the rest follow alphabetically.
Two `app` commands are badged **invite-only** — they are gated during the pre-GA
beta. The `completion` command (shell-completion scripts) is deliberately not
documented here; run `civitai completion --help` for it.
**Global flags**
Accepted by every `civitai` command, in addition to the flags listed with it.
| Flag | Description | Default |
|---|---|---|
| `--color` | force colored output even when stdout is not a TTY (also via CLICOLOR\_FORCE) | — |
| `--no-color` | disable colored/styled output (also via NO\_COLOR or CIVITAI\_NO\_COLOR) | — |
| `--no-update-check` | skip the background check for a newer release (also via CIVITAI\_NO\_UPDATE\_CHECK) | — |
| `-v, --version` | version for civitai | — |
**`civitai app`**
Browse, author, and ship Civitai Apps
```text
Browse, author, and ship Civitai Apps.
Browse the published App store with "civitai app list" (filter-based discovery)
and inspect one App with "civitai app view ".
An App is a sandboxed static web app served in an iframe. The platform
owns the build and the runtime; the only mandatory file is block.manifest.json.
The typical authoring lifecycle is create -> validate -> submit.
"civitai app create" is the friendly, batteries-included scaffolder (defaults to
the rich page-money SDK template); "civitai app init" is the same scaffolder
with a no-build static default (back-compat alias).
```
```bash
civitai app list
civitai app view my-block
civitai app create my-block
civitai app validate ./my-block
civitai app submit ./my-block
civitai app status
civitai app metrics my-block
civitai app withdraw pubreq_01H
civitai app dev-token my-block
```
**`civitai app create [name] [dir]`**
Create a ready-to-build App (batteries-included, SDK money-path)
```text
Create a ready-to-build App, batteries included.
This is the friendly happy path: a thin superset of "civitai app init" that
defaults to the rich page-money template — a Vite + React + TypeScript full-page
app wired to the published App SDK (estimate -> consent -> submit -> poll ->
Buzz spend), with a mock-host dev harness and a unit test. The scaffold is
immediately runnable (npm install && npm run dev:harness) and test-green.
"civitai app validate" passes once you have run "npm install" — until then it
correctly reports the package-lock.json the platform build installs from.
The default scaffold ships a runnable txt2img money path AND a Comfy on Civitai
(customComfy) sample that runs a server-registered recipe (invite-only beta) —
both share the estimate -> consent -> submit -> poll driver, switched by an
on-screen mode toggle, and both work end-to-end in "npm run dev:harness".
customComfy has TWO arms. Besides the recipe arm above, an app may also ship
its own ComfyUI graph inline (mode: 'inline', app developers only). src/comfy.ts
includes a complete, unit-tested buildInlineComfyBody for it: the graph, the
declared AIR resource manifest, and the maxBuzz ceiling (which is ALSO the step
timeout in seconds). See the generated README's "Comfy on Civitai samples" section.
Templates (override with --template):
static a no-build page app (index.html + a tiny JS, no build step)
page-vite a vite + React page app (config-as-code build: buildCommand + outputDir)
page-money a vite + React + TS full-page (W10) money-path app wired to the
published App SDK (estimate -> consent -> submit -> poll -> Buzz
spend); includes a txt2img + a Comfy on Civitai (customComfy) sample,
recipe and inline-graph body builders [default for create]
The display name can be free-form ("My Cool Block"); it is slugified for the
blockId. A slug-shaped name is used verbatim.
The blockId is your app's PERMANENT public identity — the hostname your app will
be served at once it is approved, and the argument every later command takes — so
derivation refuses rather than guesses when the name carries LETTERS a blockId
cannot hold ("Café Del Mar", "ÜberApp", any non-Latin name). Punctuation, symbols
and emoji still fold to a hyphen, as they always have ("Rocket 🚀 App" ->
rocket-app). Pass --slug to choose the blockId yourself; it bypasses
derivation entirely.
By default the project is created in ./. Override the output directory with
a positional [dir] or --dir ; override the display name independently with
--name (so name, slug, and directory can all differ).
Note: a DEFAULT `civitai login` (OAuth) grants submit but NOT Buzz-spend. To
run `dev:live` real generations, authenticate with a credential that carries
the AI Services scopes: `civitai login --scopes generate` (a browser login that opts into generation), or a full-scope personal API key (`civitai login --token `, created at https://civitai.com/user/account).
```
```bash
# A page-money app in ./my-block (the batteries-included default).
civitai app create my-block
# "My Cool Block" -> slug my-cool-block, dir ./my-cool-block.
civitai app create "My Cool Block"
# Same as init: a no-build static app.
civitai app create my-block --template static
# Custom output directory (slug stays my-block; created in ./apps/foo).
civitai app create my-block --dir ./apps/foo
# A name derivation cannot slugify: choose the blockId yourself.
civitai app create "Café Del Mar" --slug cafe-del-mar
```
| Flag | Description | Default |
|---|---|---|
| `--dir string` | output directory (default ./\) | — |
| `--from string` | fork from an existing published app slug (NOT AVAILABLE YET — the CLI cannot fetch app source) | — |
| `--name string` | display name (default derived from the name argument) | — |
| `--slug string` | explicit blockId (bypasses derivation from the name; 3-40 chars, starts with a letter, lowercase a-z/0-9/hyphens) | — |
| `-t, --template string` | project template: static | page-vite | page-money | `page-money` |
| `-y, --yes` | non-interactive: never prompt (use flags/defaults; fail if a name is missing) | — |
**`civitai app init [name] [dir]`**
Scaffold a ready-to-build App project
```text
Scaffold a correct, ready-to-build App project.
Templates:
static a no-build page app (index.html + a tiny JS, no build step)
page-vite a vite + React page app (config-as-code build: buildCommand + outputDir)
page-money a vite + React + TS full-page (W10) money-path app wired to the
published App SDK (estimate -> consent -> submit -> poll -> Buzz spend)
The display name can be free-form ("My Cool Block"); it is slugified for the
blockId. A slug-shaped name is used verbatim.
The blockId is your app's PERMANENT public identity — the hostname your app will
be served at once it is approved, and the argument every later command takes — so
derivation refuses rather than guesses when the name carries LETTERS a blockId
cannot hold ("Café Del Mar", "ÜberApp", any non-Latin name). Punctuation, symbols
and emoji still fold to a hyphen, as they always have ("Rocket 🚀 App" ->
rocket-app). Pass --slug to choose the blockId yourself; it bypasses
derivation entirely.
By default the project is created in ./. Override the output directory with
a positional [dir] or --dir ; override the display name independently with
--name (so name, slug, and directory can all differ).
```
```bash
# A no-build static app in ./my-block.
civitai app init my-block
# A page-money app; "My Cool Block" -> slug my-cool-block, dir ./my-cool-block.
civitai app init "My Cool Block" --template page-money
# Custom output directory (slug stays my-block; created in ./apps/foo).
civitai app init my-block --dir ./apps/foo
# A name derivation cannot slugify: choose the blockId yourself.
civitai app init "Café Del Mar" --slug cafe-del-mar
# Name, slug, and dir all independent.
civitai app init my-block ./apps/foo --name "My Block"
```
| Flag | Description | Default |
|---|---|---|
| `--dir string` | output directory (default ./\) | — |
| `--from string` | fork from an existing published app slug (NOT AVAILABLE YET — the CLI cannot fetch app source) | — |
| `--name string` | display name (default derived from the name argument) | — |
| `--slug string` | explicit blockId (bypasses derivation from the name; 3-40 chars, starts with a letter, lowercase a-z/0-9/hyphens) | — |
| `-t, --template string` | project template: static | page-vite | page-money | `static` |
| `-y, --yes` | non-interactive: never prompt (use flags/defaults; fail if a name is missing) | — |
**`civitai app validate [dir]`**
Validate block.manifest.json against the App schema
```text
Validate an App project.
This is a best-effort LOCAL pre-check that mirrors the platform's approve-time
validator (BlockManifestValidator). It catches most rejections before you
submit, but the SERVER remains the source of truth.
Checks block.manifest.json against the vendored JSON Schema (syntactic shape),
plus the ported semantic rules and structural checks:
- the manifest is present at the project root
- buildCommand and outputDir are coherent (outputDir set when buildCommand is);
outputDir must be a safe relative path (no leading "/", no ".." traversal)
- server-owned fields (iframe.src, trustTier) are REJECTED if set
- sandbox tokens are limited to the unverified-tier allowlist
(allow-scripts, allow-forms); allow-same-origin+allow-scripts is rejected
- a "page" manifest must declare an iframe block; renderMode=iframe needs one too
- iframe.minHeight and iframe.resizable are required when an iframe is present
- renderMode inline/hybrid is rejected (requires a verified tier the platform
only assigns post-submit)
- targets[].slotId must be a known registered slot
- the committed LOCKFILE matches the package manager the platform build
derives from buildCommand (its first word): pnpm -> pnpm-lock.yaml,
yarn -> yarn.lock, and npm/vite/npx/unset -> package-lock.json. The
platform installs strictly from the lockfile, so a mismatch or a missing
lockfile is a guaranteed build failure. Only applies when package.json
exists — a static app never installs.
The lockfile must also BE one, not merely exist: a package-lock.json has to
parse as JSON and declare a numeric "lockfileVersion" of 1 or more, and a
pnpm-lock.yaml / yarn.lock has to be non-empty. An empty lockfile fails the
platform build exactly like a missing one, so creating an empty one by hand
is not a fix — run the package manager and commit what it writes. A lockfile
that cannot be read (or is implausibly large) is left alone rather than
reported.
It also emits non-fatal WARNINGS the schema can't catch as hard errors:
- money-path footguns (e.g. a budgeted page with no page.buzzBudgetPerGen)
- a "page" app whose source never posts BLOCK_READY. The host will not reveal
a page app until it acks BLOCK_INIT, so such an app renders fine locally and
is replaced by a failure card in the real host — the shape of anything
scaffolded before that was fixed. Advisory ONLY: it infers runtime behaviour
from static text. A project depending on @civitai/* is never flagged (the
SDK transport acks internally), and it reads source only — never outputDir,
node_modules, markdown, or comments.
Warnings do NOT fail validation (exit 0) unless --strict is passed.
Defaults to the current directory.
```
```bash
civitai app validate # the current directory
civitai app validate ./my-block
civitai app validate --strict # treat warnings as failures
civitai app validate --json # raw JSON result (scriptable)
```
| Flag | Description | Default |
|---|---|---|
| `--json` | emit the validation result as JSON (scriptable) | — |
| `--strict` | treat warnings as failures (non-zero exit) | — |
**`civitai app submit [dir]`**
Package and submit an App for review
```text
Package the canonical App source tree and submit it for moderator
review.
The package is the SOURCE tree (manifest + src + build config) — NOT a
prebuilt dist. The platform rebuilds from source. These are excluded:
.cache, .git, .hg, .mypy_cache, .next, .pnpm-store, .pytest_cache, .ruff_cache, .svn, .turbo, .venv, .vite, build, coverage, dist, node_modules, out, venv, *.zip, .env, .env.local, .env.*.local, .env.development, .env.test
Submission path:
By default this uploads the bundle directly using your stored token to the
token-authenticated submit route (POST /api/v1/blocks/submit-version). OAuth
device-login tokens (`civitai login`) and personal API keys both work;
OAuth tokens refresh automatically. Set CIVITAI_SUBMIT_PATH to override the
route. With no token configured (and no --package-only), it writes the
canonical .zip and prints the manual next steps.
--package-only always just writes the .zip and stops.
Submitting creates a real "pending moderator review" request (undone only with
`civitai app withdraw`), so it is NOT fired blindly: before uploading you are
shown the app@version and asked to confirm. Pass --yes/-y to skip the prompt
(for scripts/CI). In a non-interactive shell (no TTY) submit REFUSES unless
--yes is given, rather than hang or submit silently. --package-only is the safe
preview — it never submits.
Defaults to the current directory.
```
```bash
civitai app submit # validate + package + confirm + submit
civitai app submit --yes # skip the confirmation prompt (scripts/CI)
civitai app submit --package-only # just write the .zip (safe preview, never submits)
civitai app submit -o my-block.zip ./my-block
```
| Flag | Description | Default |
|---|---|---|
| `-o, --out string` | output .zip path (default: \-\.zip) | — |
| `--package-only` | only write the .zip; do not attempt submission | — |
| `--skip-validate` | skip manifest validation before packaging | — |
| `-y, --yes` | skip the confirmation prompt and submit (for scripts/CI) | — |
**`civitai app status [blockId]`**
Check the review/deploy status of your App submissions
```text
Check the review and deploy status of your own App submissions.
Calls the token-authenticated, self-scoped status route
(GET /api/v1/blocks/submissions) with your stored credential — you only ever see
your OWN submissions. Both a personal API key and an OAuth login (civitai login)
work; the OAuth token must carry the Apps submit scope (the same gate the
submit route uses).
With no argument it lists all your submissions (newest first). Pass a blockId
(app slug) or --id to see a single submission in detail, including the
rejection reason (if rejected) and the live URL (if approved + deployed).
Note: a submission's .civit.ai surface only serves AFTER it is approved
and deployed (deployState 'live').
```
```bash
civitai app status # list all your submissions
civitai app status my-block # detail for the my-block app
civitai app status --id pubreq_01H # detail by publish-request id
civitai app status --json # raw JSON (scriptable)
```
| Flag | Description | Default |
|---|---|---|
| `--id string` | look up a single submission by publish-request id (pubreq\_...) | — |
| `--json` | emit raw JSON (scriptable) | — |
**`civitai app withdraw [pubreq-id]`**
Withdraw your own pending App submission
```text
Withdraw your own pending App submission so you can resubmit a new bundle
for the same slug.
Calls the token-authenticated, self-scoped withdraw route
(POST /api/v1/blocks/withdraw) with your stored credential — you can only ever
withdraw your OWN submissions. Both a personal API key and an OAuth login
(civitai login) work; the OAuth token must carry the Apps submit scope
(the same gate the submit route uses).
Only a submission still in the 'pending' review state can be withdrawn; an
already-approved/rejected (or already-withdrawn) request cannot. Withdrawing is
idempotent — withdrawing an already-withdrawn request still succeeds.
Pass the publish-request id as a positional argument or via --id (find it with
"civitai app status").
Withdraw is non-interactive (it never prompts); --yes/-y is accepted as a no-op
for symmetry with "civitai app submit" so the same scripted flag works on both.
```
```bash
civitai app withdraw pubreq_01H # withdraw by publish-request id
civitai app withdraw --id pubreq_01H # same, via the flag
```
| Flag | Description | Default |
|---|---|---|
| `--id string` | the publish-request id to withdraw (pubreq\_...) | — |
| `-y, --yes` | accepted for symmetry with 'app submit' (withdraw is non-interactive; no-op) | — |
**`civitai app listing`**
Manage your App store-listing media (icon, cover, screenshots)
```text
Manage your App store listing's MEDIA — the icon, cover, and screenshots
a listing needs before it can publish.
A store listing must have an ICON and a COVER before it can go live; screenshots
are optional. These commands ingest a local image, attach it to your listing,
and then wait for the content scan — the same pipeline the web submit form
uses. The platform validates dimensions, aspect and format at the ATTACH step,
so a wrongly-shaped image is refused in seconds rather than after the scan.
For a listing that is already LIVE (approved), attaching media opens a REVISION
that goes back to moderator review (the live listing is untouched until the
revision is approved); pass --changelog to describe the change.
The app is resolved from block.manifest.json in the current directory (or pass
--slug). Your store listing is created as a DRAFT when you run
`civitai app submit`, so you can set its media WHILE your app is pending review
— the media you attach carries forward when a moderator approves it. Set it
early to clear the publish floor before you go live.
Source files are checked locally BEFORE any upload — png, jpeg or webp, at most
2.0 MiB for an icon, 4.0 MiB for a cover, 2.0 MiB for a screenshot.
A file in the wrong format, or over its cap, is refused before anything is
uploaded.
```
```bash
civitai app listing status
civitai app listing set-icon ./assets/icon.png
civitai app listing set-cover ./assets/cover.png
civitai app listing add-screenshot ./shot.png --caption "Grid view"
civitai app listing rm-screenshot alsc_01H...
civitai app listing reorder alsc_02 alsc_01 alsc_03
```
**`civitai app listing set-icon `**
Set the listing icon (png, jpeg or webp, at most 2.0 MiB)
```text
Set your store listing's ICON — the small image shown beside your app's name.
An icon is MANDATORY: a listing cannot publish without one.
The source file is validated locally first (png, jpeg or webp, at most 2.0 MiB),
then ingested and attached, and the content scan is waited on afterwards.
Nothing is uploaded if the local check fails. The platform validates the
image's dimensions and aspect at the ATTACH step, so a wrongly-shaped image is
refused in seconds rather than after the scan.
See "Listing media requirements" in the README for the platform's bounds.
On a listing that is already LIVE this opens a REVISION for moderator re-review
instead of changing the live listing — pass --changelog to describe the change,
-y to skip the confirmation. On a DRAFT listing it attaches directly.
Run `civitai app listing status` to see what the publish floor still needs.
```
```bash
civitai app listing set-icon ./assets/icon.png
civitai app listing set-icon ./icon.png --slug my-app
civitai app listing set-icon ./icon.png --changelog "New brand mark" -y
```
| Flag | Description | Default |
|---|---|---|
| `--changelog string` | changelog for the moderator review (used only when the listing is already live) | — |
| `--dir string` | app directory holding block.manifest.json (when --slug is not given) | `.` |
| `--slug string` | app slug (defaults to block.manifest.json's blockId) | — |
| `-y, --yes` | skip the live-listing revision confirmation | — |
**`civitai app listing set-cover `**
Set the listing cover (png, jpeg or webp, at most 4.0 MiB)
```text
Set your store listing's COVER — the wide image at the top of the listing
page. A cover is MANDATORY: a listing cannot publish without one.
The source file is validated locally first (png, jpeg or webp, at most 4.0 MiB),
then ingested and attached, and the content scan is waited on afterwards.
Nothing is uploaded if the local check fails. The platform validates the
image's dimensions and aspect at the ATTACH step, so a wrongly-shaped image is
refused in seconds rather than after the scan.
See "Listing media requirements" in the README for the platform's bounds.
On a listing that is already LIVE this opens a REVISION for moderator re-review
instead of changing the live listing — pass --changelog to describe the change,
-y to skip the confirmation. On a DRAFT listing it attaches directly.
Run `civitai app listing status` to see what the publish floor still needs.
```
```bash
civitai app listing set-cover ./assets/cover.png
civitai app listing set-cover ./cover.jpg --slug my-app
civitai app listing set-cover ./cover.png --changelog "Updated hero" -y
```
| Flag | Description | Default |
|---|---|---|
| `--changelog string` | changelog for the moderator review (used only when the listing is already live) | — |
| `--dir string` | app directory holding block.manifest.json (when --slug is not given) | `.` |
| `--slug string` | app slug (defaults to block.manifest.json's blockId) | — |
| `-y, --yes` | skip the live-listing revision confirmation | — |
**`civitai app listing add-screenshot `**
Add a screenshot (up to 8) with an optional caption
```text
Add a SCREENSHOT to your store listing's gallery. Screenshots are OPTIONAL:
they are not part of the publish floor.
The source file is validated locally first (png, jpeg or webp, at most 2.0 MiB),
then ingested and appended to the gallery, and the content scan is waited on
afterwards. Nothing is uploaded if the local check fails. The platform
validates dimensions, aspect and format at the ATTACH step, so a bad image is
refused in seconds rather than after the scan. --caption adds a one-line
caption.
See "Listing media requirements" in the README for the platform's bounds.
Each run appends one screenshot; there is no bulk add. Use
`civitai app listing reorder` to change the order afterwards and
`civitai app listing rm-screenshot` to drop one — both take the screenshot ids
that `civitai app listing status` prints.
The gallery has a ceiling (8 at the time of writing) and it is the SERVER's, not
this CLI's: nothing here counts the gallery before uploading, so hitting the
ceiling surfaces as a server refusal after the ingest rather than as a local
usage error.
On a listing that is already LIVE this opens a REVISION for moderator re-review
instead of changing the live listing — pass --changelog to describe the change,
-y to skip the confirmation. On a DRAFT listing it attaches directly.
```
```bash
civitai app listing add-screenshot ./shot.png
civitai app listing add-screenshot ./grid.png --caption "Grid view"
civitai app listing add-screenshot ./shot.png --slug my-app
```
| Flag | Description | Default |
|---|---|---|
| `--caption string` | optional one-line caption for the screenshot | — |
| `--changelog string` | changelog for the moderator review (used only when the listing is already live) | — |
| `--dir string` | app directory holding block.manifest.json (when --slug is not given) | `.` |
| `--slug string` | app slug (defaults to block.manifest.json's blockId) | — |
| `-y, --yes` | skip the live-listing revision confirmation | — |
**`civitai app listing rm-screenshot `**
Remove a screenshot by its id (see `app listing status`)
```text
Remove a screenshot from your listing by its screenshot id (the id shown by
`civitai app listing status`, e.g. alsc_...). Note: for a LIVE listing,
direct screenshot edits are only possible while a revision is open.
```
```bash
civitai app listing rm-screenshot alsc_01H8XYZ
civitai app listing rm-screenshot alsc_01H8XYZ --slug my-app
```
| Flag | Description | Default |
|---|---|---|
| `--dir string` | app directory holding block.manifest.json (when --slug is not given) | `.` |
| `--slug string` | app slug (defaults to block.manifest.json's blockId) | — |
**`civitai app listing reorder `**
Reorder screenshots (pass ALL current screenshot ids in the new order)
```text
Reorder your listing's screenshots. Pass EXACTLY the current set of screenshot
ids (from `civitai app listing status`) in the desired order — a partial or
unknown set is rejected.
Ordering is positional: the first id becomes the first screenshot in the
gallery. There is no "move one" form — read the current order out of
`civitai app listing status` and pass the whole list back.
```
```bash
civitai app listing reorder alsc_02 alsc_01 alsc_03
civitai app listing reorder alsc_02 alsc_01 --slug my-app
```
| Flag | Description | Default |
|---|---|---|
| `--dir string` | app directory holding block.manifest.json (when --slug is not given) | `.` |
| `--slug string` | app slug (defaults to block.manifest.json's blockId) | — |
**`civitai app listing status`**
Show attached media and what's missing vs the publish floor
```text
Show your store listing's attached media (icon, cover, screenshots) and what
is still required before it can publish (an icon and a cover are mandatory).
Your store listing exists as a DRAFT from the moment you run
`civitai app submit`, so this works while your app is still pending review.
Note: on a LIVE (approved) listing this opens an in-progress revision draft and
reports ITS media (idempotent — it reuses any existing draft, and nothing is
submitted for moderator review until you run a set-/add- command and confirm).
```
```bash
civitai app listing status
civitai app listing status --slug my-app
civitai app listing status --dir ./my-app
```
| Flag | Description | Default |
|---|---|---|
| `--dir string` | app directory holding block.manifest.json (when --slug is not given) | `.` |
| `--slug string` | app slug (defaults to block.manifest.json's blockId) | — |
**`civitai app list`**
Discover published Apps in the store (GET /api/v1/apps)
```text
List published Apps from the Civitai store via GET /api/v1/apps.
This is filter-based discovery — filter by --kind / --category, order by --sort,
and page with --cursor. There is no free-text search yet (the store service
doesn't support it), so there is no `app search` command.
Login is required (`civitai login`): the endpoint keys the visible catalog
off your identity, so an anonymous call would see nothing. Pagination is keyset
cursor-based (no --page); the next cursor is printed after the results — pass it
back via --cursor.
The store is rate-limited per caller; a tight scripted loop may see 429s (the CLI
backs off and retries automatically).
NOTE: the store is gated by a launch flag — until it opens publicly you will only
see apps if your account is a moderator or app-dev-tester; a normal login may get
an empty list.
```
```bash
civitai app list
civitai app list --kind onsite --sort popular --limit 10
civitai app list --category generation --json
civitai app list --cursor ''
```
| Flag | Description | Default |
|---|---|---|
| `--category string` | filter by marketplace category (generation, games, utility, discovery, moderation, analytics, other) | — |
| `--cursor string` | pagination cursor from a previous response | — |
| `--json` | print the raw API JSON response (for scripting) | — |
| `--kind string` | filter by kind (all, onsite, offsite) | — |
| `--limit int` | results per page (1-50) | — |
| `--sort string` | sort order (top-rated, popular, newest, name) | — |
**`civitai app view `**
Show one App's detail (GET /api/v1/apps/{slug})
```text
Show the public detail for one published App by slug via
GET /api/v1/apps/{slug} — its description, category, rating, gallery, and the
kind-specific action target (an on-site app's live URL, or an off-site app's
external / connect target).
Login is required (`civitai login`). A missing or out-of-scope slug returns a
clean "not found" message.
This reads the PUBLIC STORE CATALOG, which is NOT the same thing as your
deployment: an app can be approved, deployed and serving at .civit.ai and
still not be in the store. When a 404 lands on a slug you own, the error says so
and points at `civitai app listing status` / `civitai app status`.
```
```bash
civitai app view my-cool-app
civitai app view my-cool-app --json
```
| Flag | Description | Default |
|---|---|---|
| `--json` | print the raw API JSON response (for scripting) | — |
**`civitai app metrics `**
Show your App's install / run / Buzz / engagement analytics
```text
Show the owner-only analytics for one of YOUR App Blocks: installs, runs and
the Buzz they spent, Buzz purchased through the app, and API engagement.
The slug is resolved to its appBlockId through your own submissions
(`civitai app status` reads the same route), so analytics are only available
once a version of the app has been APPROVED — an app that was never approved has
no App Block to report on yet.
WINDOW: the server defaults to the last 30 days and clamps any request to 366
days, so a zero is only meaningful together with the period it covers. This
command therefore always prints the window the SERVER served (echoed from the
response), not the one you asked for. Pass --from / --to as a plain YYYY-MM-DD
date (midnight UTC) or a full RFC3339 timestamp to widen it.
CREDENTIAL: the analytics query is full-scope, so it needs a full-scope personal API key (`civitai login --token `, created at https://civitai.com/user/account);
an OAuth browser login is refused with 403.
DATA CAVEAT: engagement counts only AUTHENTICATED, scope-gated API calls. An app
that ships no scoped API surface will show real installs and revenue with a flat
engagement section — that is expected, not a bug.
```
```bash
civitai app metrics my-block
civitai app metrics my-block --from 2026-05-01 --to 2026-08-03
civitai app metrics my-block --from 2026-05-01T00:00:00Z
civitai app metrics my-block --json
```
| Flag | Description | Default |
|---|---|---|
| `--from string` | window start: YYYY-MM-DD (midnight UTC) or RFC3339 (default: 30 days ago, server-side) | — |
| `--json` | emit the raw analytics payload (scriptable). Unlike the human view, --json does NOT refuse a not-entitled read: a notOwned:true payload is passed through with every counter zeroed and still exits 0, so a script MUST branch on the notOwned field rather than trusting the counts | — |
| `--to string` | window end: YYYY-MM-DD (midnight UTC) or RFC3339 (default: now, server-side) | — |
**`civitai app dev-token `** — invite-only
Mint a short-lived dev block token for `npm run dev:live`
```text
Mint a short-lived dev block token so a scaffolded page-money app can run
"npm run dev:live" against the REAL Civitai backend.
Calls the invite-gated mint route (POST /api/v1/blocks/dev-token) with your
stored credential and prints the token (a ~4-hour RS256 JWT). Paste it into
VITE_LIVE_BLOCK_TOKEN in .env.development.local, then restart "npm run dev:live".
The minted token's CAPABILITIES depend on the credential you mint with:
- REAL generation (spends real Buzz) needs a credential carrying AI Services:
a FULL-SCOPE PERSONAL API KEY (create one at https://civitai.com/user/account)
or an OAuth login that opted in with "civitai login --scopes generate".
Confirm yours can spend with "civitai whoami".
- A DEFAULT OAuth login ("civitai login", no --scopes) mints a
READ/IDENTITY-ONLY dev token (no spend) — dev:live shows your viewer +
catalog/storage, but estimate → submit → generation will NOT spend.
--spend is the explicit affirmation that this token may spend real Buzz. It does
two things to the request: it adds ai:write:budgeted to the scopes REQUESTED from
the mint route, and it sets the request's spend-intent field (requestBudgetedSpend)
to true. The server still clamps against your credential, so --spend cannot grant
what your credential lacks.
WITHOUT --spend the CLI never requests budgeted spend implicitly: if your
block.manifest.json declares ai:write:budgeted it is FILTERED OUT of the request
(the command tells you when that happens). The scaffolded money app declares it,
so a live run that used to generate now needs --spend — otherwise dev:live
refuses with "block lacks ai:write:budgeted scope". Every other manifest scope is
requested unchanged. With no local manifest the CLI sends no scopes at all —
independently of that, EVERY mint now states its spend intent explicitly, since
requestBudgetedSpend is always present on the request and is true only with
--spend.
Pre-GA the mint route is invite-only. You do NOT need to submit the app
first — for a brand-new slug with no app row yet, the token is minted from the
scopes in your local block.manifest.json (clamped server-side), so
"create → dev-token → dev:live" works directly. The token is short-lived —
never commit it; re-mint when it expires.
BUDGET (--budget, 1-250 Buzz). The token carries a per-generation Buzz budget.
Omit --budget and the server picks one — 50 for a slug with no submitted app.
Your LOCAL block.manifest.json page.buzzBudgetPerGen does NOT raise it: until
the app is submitted there is no server-side manifest to read, so the 50 is a
flat default, not a clamp of your file. --budget is the only way to move it.
A generation is REFUSED outright when the recipe's Buzz ceiling exceeds that
budget, so a shipped recipe with a ceiling of 90 dead-ends on the default:
insufficient buzz budget: recipe ceiling 90 exceeds budget 50
Raise it (--budget 250) rather than editing the recipe.
The trap: for an inline customComfy graph your maxBuzz is BOTH the Buzz ceiling
and the step timeout in SECONDS. An over-thrifty budget therefore does not fail
as a billing error — the step runs out of wall clock and comes back "expired",
which reads like a broken graph. Budget for the seconds the graph needs.
```
```bash
civitai app dev-token my-block # print the token to stdout
civitai app dev-token my-block --spend # also REQUEST ai:write:budgeted (real Buzz)
civitai app dev-token my-block --budget 250 # max budget (and 250s of customComfy wall clock)
civitai app dev-token my-block --env # print VITE_LIVE_BLOCK_TOKEN=
civitai app dev-token my-block --env >> .env.development.local
```
| Flag | Description | Default |
|---|---|---|
| `--budget int` | per-generation Buzz budget the token may spend (1-250; omit to let the server decide — 50 for an unsubmitted app). Must clear your recipe's ceiling; for inline customComfy it is ALSO the step timeout in seconds | — |
| `--env` | print VITE\_LIVE\_BLOCK\_TOKEN=\ (paste-ready into .env.development.local) | — |
| `--spend` | explicitly REQUEST the ai:write:budgeted scope so npm run dev:live can spend REAL Buzz. Omit it and that scope is FILTERED OUT of the request and the request's spend-intent field states false — the CLI never asks for budgeted spend implicitly, even when your block.manifest.json declares it | — |
**`civitai app dev-tunnel [blockId]`** — invite-only
Preview your LOCAL dev server inside the real Civitai host via a hardened tunnel
```text
Run your app locally (`npm run dev:tunnel`) and see it rendered INSIDE the
real production host at civitai.com/apps/dev/ — the actual page host
bridge, your real Buzz, real pickers, real session — but with the iframe pointing
at YOUR local code instead of a deployed bundle. A prod-fidelity inner-dev-loop.
How it works: this mints an EPHEMERAL ssh keypair (in memory — never written to
~/.ssh), calls blocks.startDevTunnel with the PUBLIC key, opens a reverse tunnel
(ssh -R) from your local dev-server port to the Civitai tunnel endpoint, and
prints the /apps/dev/ URL to open in your browser. On Ctrl-C (or an idle
timeout) it tears the tunnel down and revokes the session server-side.
Start your dev server first, in another terminal:
npm run dev:tunnel # serves your app on 127.0.0.1:5186, embeddable
then run this against the SAME port. Authentication uses your stored credential
(`civitai login` or a personal API key); you can only tunnel your OWN app.
The blockId is resolved from (in order): the `--block` flag, the positional
argument, then the `blockId` in `block.manifest.json` in the current
directory. Run it from your App project dir and you can omit the blockId entirely.
⚠️ GATED: dev tunnels are limited to invited Apps authors / moderators and are
guarded by a server kill-switch flag. The tunnel endpoint is live; if you are not
enrolled the mint reports "not available" — ask to be added to the cohort.
```
```bash
# In terminal 1: start the embeddable dev server.
npm run dev:tunnel
# In terminal 2: open the tunnel (Ctrl-C to tear down).
civitai app dev-tunnel # blockId from block.manifest.json in the CWD
civitai app dev-tunnel my-block
civitai app dev-tunnel my-block --port 5173
# Dev server NOT on the CLI's loopback (a container/pod, VM, or bound interface):
civitai app dev-tunnel my-block --local-host 10.42.0.100
civitai app dev-tunnel --block my-block --idle-timeout 15m
```
| Flag | Description | Default |
|---|---|---|
| `--block string` | the blockId (app slug) to tunnel (or pass it positionally; defaults to the blockId in block.manifest.json in the CWD) | — |
| `--idle-timeout duration` | tear the tunnel down after this much inactivity | `30m0s` |
| `--local-host string` | host your local dev server is bound to. Default localhost (loopback) — the scaffold's dev:tunnel binds localhost, so most users need nothing. Set this for a dev server NOT on the CLI's loopback: a container/pod (e.g. --local-host 10.42.0.100), a VM, or a specific bound interface | `localhost` |
| `--no-wait` | skip the readiness wait and print the URL immediately (it may 404/NXDOMAIN for a few minutes while DNS/route propagate) | — |
| `--port int` | local dev-server port to tunnel (matches the scaffold's dev:tunnel) | `5186` |
| `--ready-timeout duration` | cap the wait for the public host to start serving (0 = wait indefinitely until ready or Ctrl-C; a positive value warns + prints the URL anyway on expiry) | — |
| `--tunnel-endpoint string` | sish SSH endpoint host:port (default sish.civitai.com:2224, or $CIVITAI\_DEV\_TUNNEL\_ENDPOINT) | — |
**`civitai app pull [dir]`**
Clone or sync your app's repository from Civitai
```text
Clone (or, if [dir] is already a checkout, pull) the canonical repository
backing one of YOUR approved Apps. This is the read side of git authoring:
it fetches the current block.manifest.json + source so you can edit locally and
then submit (`civitai app submit`) or push.
Authentication uses your stored credential (`civitai login` or a personal
API key). The command calls an owner-only endpoint that lazily provisions a
scoped, read-only Forgejo identity for you and returns a clone URL with a push/
pull token embedded.
⚠ SECURITY — TOKEN-IN-URL LEAKAGE: the clone URL embeds your access token as
HTTP-Basic credentials (https://:@...). On a fresh CLONE, git
stores the remote URL in .git/config, so the token lands on disk in the clone;
treat the checkout as sensitive: do NOT commit .git/config or share the
directory, and consider clearing the remote URL (or replacing it with the
credential-less HTTPS URL `git remote set-url origin `) after the
clone if you rely on a git credential helper. On a SYNC (pull into an existing
checkout) the URL is passed explicitly and is NOT persisted to .git/config, but
the token still transiently appears in the git child process's arguments, so it
is briefly visible to other processes via `ps` / /proc//cmdline.
The repo only exists once your FIRST version has been submitted as a ZIP and
approved; before then the command tells you so instead of failing obscurely.
```
```bash
civitai app pull --app my-block # clone into ./my-block
civitai app pull ./my-block --app my-block # clone/sync into ./my-block
civitai app pull . --app my-block # sync the current directory
```
| Flag | Description | Default |
|---|---|---|
| `--app string` | the app slug (repo name) or appBlockId to pull (required) | — |
**`civitai articles`**
Search and inspect articles on Civitai
```text
Read-only access to Civitai articles through the public REST API
(GET /api/v1/articles, GET /api/v1/articles/{id}).
No login is needed — these are public read routes (unlike `civitai app list`
and `app view`, which are refused without a token). The CLI still sends your
stored token when you have one; --anon forces an anonymous request.
Articles are the site's long-form guides. `articles search` finds them;
`articles get --content` renders the BODY as readable text/markdown —
headings, paragraphs, lists, links and code blocks, with the HTML stripped and
entities decoded — so a guide is readable in the terminal without a browser.
--json returns the raw API body, including the UNTOUCHED HTML content, and
takes precedence over --content.
--json writes the API response to stdout and nothing else — notes and errors go
to stderr, so `… --json | jq -e .` always parses. The document is the API's,
the bytes are not: it is re-indented on the way out, so do not diff or hash it
against the wire.
```
```bash
civitai articles search --query "workflow" --limit 5
civitai articles get 1234
civitai articles get 1234 --content
```
**`civitai articles get `**
Get an article by id (GET /api/v1/articles/{id})
```text
Get one article by id (GET /api/v1/articles/{id}).
By default it prints the article's metadata (title, author, stats, tags). Pass
--content to also render the article BODY — the actual guide — as readable plain
text / lightweight markdown (headings, paragraphs, lists, links, code blocks;
HTML tags stripped and entities decoded). --json returns the raw API body
(including the untouched HTML content) and takes precedence over --content.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai articles get 1234
civitai articles get 1234 --content
civitai articles get 1234 --json
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--content` | also render the article body as readable text/markdown (ignored with --json, which returns raw) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
**`civitai articles search`**
Search articles (GET /api/v1/articles)
```text
Search articles via GET /api/v1/articles.
Filters: --query (matches the TITLE, not the body), --tags, --username and
--nsfw. --tags takes numeric tag IDS, comma-separated (e.g. --tags 5,12), not
tag names — and `civitai tags search` renders names, not ids, so it cannot
supply this filter for you.
--sort takes a server-owned value set.
The CLI does not check those value sets — it passes the value through and the
server rejects an unknown one with HTTP 400, reported as a usage mistake.
Paging is cursor-only: the article feed is a keyset feed, so there is no --page
here. --limit takes 1–100; omit it for the server's default page size.
The next cursor is printed under the results; pass it back via --cursor.
The REACTIONS column is likes plus favourites, from the list endpoint's own
stats. `articles get ` reports all-time view / like / favourite /
comment / collected counts instead — a different stats block, so the two need
not agree — and --content renders the guide itself.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai articles search --query "comfyui" --limit 5
civitai articles search --sort "Most Reactions" --nsfw
civitai articles search --username some-creator --cursor ''
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--cursor string` | pagination cursor from a previous response | — |
| `--json` | print the raw API JSON response (for scripting) | — |
| `--limit int` | results per page (1-100) | — |
| `--nsfw` | include NSFW results | — |
| `--query string` | text search query (matches the article title) | — |
| `--sort string` | sort order (Newest, "Recently Updated", "Most Reactions", "Most Comments", "Most Bookmarks", "Most Collected") | — |
| `--tags string` | filter by tag ids (comma-separated, e.g. 5,12) | — |
| `--username string` | filter by author username | — |
**`civitai buzz`**
Show your spendable Buzz balance
```text
Show your spendable Buzz balance (blue / green / yellow, plus a total) using
your stored credential.
Reads buzz.getBuzzAccount with the same credential as `whoami` / `app status`.
A full-scope personal API key can read your balance, as can a browser login that
opted into the generate scope set (`civitai login --scopes generate`). A
DEFAULT OAuth login (`civitai login`) can read neither balance nor spend Buzz —
in that case this prints both ways to fix it.
```
```bash
civitai buzz
civitai buzz --json # raw JSON (scriptable)
```
| Flag | Description | Default |
|---|---|---|
| `--json` | emit raw JSON (scriptable) | — |
**`civitai collections`**
Search and inspect collections on Civitai
```text
Read-only access to Civitai collections through the public REST API
(GET /api/v1/collections, GET /api/v1/collections/{id}).
No login is needed — these are public read routes (unlike `civitai app list`
and `app view`, which are refused without a token). The CLI still sends your
stored token when you have one; --anon forces an anonymous request.
Only PUBLIC collections are discoverable. Logging in does not widen this
surface to your own private collections, and there is no create, edit or
add-to-collection path in the CLI — this group is read-only.
`collections search` finds collections by name; `collections get `
shows one collection's owner, type, read permission, description and tags.
Neither command pages through a collection's CONTENTS: the public route
answers with collection metadata (`search` adds an item COUNT), not with the
models or images inside.
--json writes the API response to stdout and nothing else — notes and errors go
to stderr, so `… --json | jq -e .` always parses. The document is the API's,
the bytes are not: it is re-indented on the way out, so do not diff or hash it
against the wire.
```
```bash
civitai collections search --query "favorites" --limit 5
civitai collections get 1234
civitai collections get 1234 --json
```
**`civitai collections get `**
Get a collection by id (GET /api/v1/collections/{id})
```text
Get one collection by id: GET /api/v1/collections/{id}.
The id is the number in a civitai.com/collections/ URL. A non-integer or
non-positive argument is refused locally, as a usage mistake, before any
request is made.
Prints the collection's name, owner, type, read permission, public flag,
description (truncated) and tags. Only PUBLIC collections are readable here.
Two differences from the `search` row for the same collection, both of them
the API's shape rather than the CLI's: the detail body drops the item COUNT and
adds the TAGS. Neither shape lists the collection's items.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai collections get 1234
civitai collections get 1234 --json
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
**`civitai collections search`**
Search collections (GET /api/v1/collections)
```text
Search public collections via GET /api/v1/collections.
Paging is cursor-only: a keyset cursor on the collection id, so there is
no --page here. --limit takes 1–100; omit it for the server's default page size.
The next cursor is printed under the results; pass it back via --cursor.
Cursor paging is only supported for the default (Newest) sort. This is a server
constraint: for any other --sort (e.g. "Most Followers") the API returns a
nextCursor that it then rejects — a dead cursor that yields no further pages. So
for a non-Newest sort the CLI shows the first page only and does NOT print a
next-page hint; deep paging requires --sort Newest.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai collections search --query "anime" --limit 5
civitai collections search --sort Newest --cursor
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--cursor string` | pagination cursor from a previous response (Newest sort only) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
| `--limit int` | results per page (1-100) | — |
| `--nsfw` | include NSFW results | — |
| `--query string` | text search query (matches the collection name) | — |
| `--sort string` | sort order (Newest, "Most Followers") | — |
**`civitai creators`**
Search creators on Civitai
```text
Read-only access to Civitai creators through the public REST API
(GET /api/v1/creators).
No login is needed — these are public read routes (unlike `civitai app list`
and `app view`, which are refused without a token). The CLI still sends your
stored token when you have one; --anon forces an anonymous request.
A creator row is a USERNAME, a published-model COUNT and a LINK. The group is
search-only: the public API has no per-creator profile route, so there is no
`creators get` to add.
The follow-up is `civitai models search --username `, which is what
actually lists a creator's models. Use `civitai users get ` if you want
the user record (id, avatar) behind the name.
--json writes the API response to stdout and nothing else — notes and errors go
to stderr, so `… --json | jq -e .` always parses. The document is the API's,
the bytes are not: it is re-indented on the way out, so do not diff or hash it
against the wire.
```
```bash
civitai creators search --query artist --limit 10
civitai creators search --query artist --json
```
**`civitai creators search`**
Search creators (GET /api/v1/creators)
```text
Search creators via GET /api/v1/creators.
--query matches the username; omit it to page the whole creator list.
--limit takes 1–200; omit it for the server's default page size.
Paging is --page only. This endpoint answers with the classic page envelope
(total items, current page, total pages) and no cursor, so there is no --cursor
here. The footer prints the next --page while the response says there is one.
Each row is USERNAME, MODELS (that creator's published model count) and LINK
(the equivalent models query on the website). To list the models themselves,
run `civitai models search --username `.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai creators search --query artist --limit 10
civitai creators search --limit 50 --page 2
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
| `--limit int` | results per page (1-200) | — |
| `--page int` | page number | — |
| `--query string` | text search query | — |
**`civitai download [version-id]`**
Download a model version's file(s)
```text
Download the file(s) of a model VERSION from Civitai.
Identify the version deterministically by its numeric version id:
civitai download 691639
…or resolve a model's default (first published) version with --model:
civitai download --model 4384
The positional id is normally a model-VERSION id, but 'civitai models search'
and 'civitai models get' list MODEL ids — so handing a model id as the positional
(e.g. 'civitai download 4384') just works: the CLI recognizes it's a model id and
downloads that model's default version (printing a note that it did). When a
pasted number is BOTH a valid model id and a valid version id (common for low/mid
numbers), the CLI STOPS and asks you to disambiguate rather than silently
downloading an unrelated model's version — re-run with --model (the model's
default version) or --version (that version as-is). Use --version to name a
version id explicitly and skip that stop; --yes proceeds on the version
interpretation and echoes exactly which version it is downloading.
Use --dry-run to print the resolved plan (files, sizes, SHA256, target paths,
and whether auth is required) without transferring anything.
By default the version's PRIMARY file is downloaded into the current directory
under its server-provided name. Any file type downloads — model weights, but
also non-weights deliverables like a "Workflows" model's Archive, training data,
or other artifacts. Use --file to pick a specific file, or --all to download
every file. Downloads stream to ".part" and are renamed into place only
on success, so an interrupted run never leaves a truncated final file.
Selecting one of two same-named files: a version can ship two files with the
SAME name (e.g. an fp16 and an fp8 both named flux_dev.safetensors). --file
accepts a numeric FILE ID (the version's files[].id) as well as a name, so you
can pick exactly one — the ids are shown by --dry-run and in the error you get
if a name is ambiguous. --all refuses to run when two selected files would
resolve to the same on-disk path (which would silently overwrite one), listing
the colliding files with their ids so you can --file the one you want.
Authentication: most model files require a token to download — a gated file
requires authentication (it 401s without a token), but some public files
download with no token at all. Run 'civitai login' if a download 401s. Your
stored login token or CIVITAI_TOKEN is sent automatically. The read/search
commands (models, model-versions, articles, images, …) always work anonymously.
Folder routing: pass --layout (with an optional --root ,
default ".") to write each file into the correct subfolder for that app, routed
by the file/model type — so --all fans a bundled VAE into the VAE folder
instead of polluting the checkpoint folder. --layout is mutually exclusive with
--out/--out-dir.
Compatibility: --for-base "" warns on stderr when the version's base
model is in a confidently different family than your target (e.g. an SD 1.5
embedding for an SDXL model). The version's base model is always shown.
Integrity: the streamed bytes are verified against the file's SHA256 by default
(--no-verify to skip; a file with no published SHA256 is downloaded with a
warning). A hash mismatch deletes the partial file and fails. SHA256 verifies
INTEGRITY (the bytes match what the API advertised), NOT authenticity — a
compromised source that advertises a matching hash for malicious bytes can't be
detected by the hash alone. Pickle/executable (.ckpt/.pt/.pth/.bin/.pickle/.pkl)
and archive (.zip/.tar/.tar.gz/.tgz/.rar/.7z) files can execute code when loaded;
the CLI notes this on stderr. Only download models from creators you trust.
```
```bash
civitai download 691639
civitai download --version 128713 # force a version id (skips the ambiguous-id stop)
civitai download --model 4384 --out ./dreamshaper.safetensors
civitai download 290640 --file vae --out-dir ./models
civitai download 691639 --file 1234567 # pick one of two same-named files by id
civitai download 290640 --all --out-dir ./models
civitai download 290640 --all --layout comfyui --root ~/ComfyUI
civitai download 691639 --layout a1111 --for-base "SDXL 1.0"
```
| Flag | Description | Default |
|---|---|---|
| `--all` | download every file in the version (refuses if two files would overwrite the same path — pick one with --file \) | — |
| `--anon` | force an anonymous request (ignore any stored login token); NOTE: most downloads 401 without a token — --anon is meaningful for read commands, not downloads | — |
| `--dry-run` | print the resolved download plan (files, sizes, hashes, targets) and exit without downloading anything | — |
| `--file string` | select one file by numeric file id, or by name (exact, else a unique case-insensitive substring); use the id to pick one of two same-named files | — |
| `--for-base string` | warn if the version's base model is a confidently different family than this target (e.g. "SDXL 1.0") | — |
| `--force` | re-download even if the target file already exists | — |
| `--layout string` | route each file into its type's subfolder for an app (a1111|comfyui); mutually exclusive with --out/--out-dir | — |
| `--model string` | resolve+download a MODEL's default (first published) version instead of a version id | — |
| `--no-verify` | skip SHA256 verification of the downloaded bytes | — |
| `--out string` | target file path (single-file only; mutually exclusive with --all/--out-dir) | — |
| `--out-dir string` | directory to write server-named file(s) into (created if needed) | — |
| `--root string` | base directory for --layout routing (default "."; only applies with --layout) | — |
| `--version string` | download this model-VERSION id explicitly (skips the ambiguous model-id safety stop the bare positional id triggers) | — |
| `--yes` | proceed past the ambiguous-id safety stop (a bare id that is BOTH a model id and a version id): download the version as typed | — |
**`civitai generate [prompt]`**
Generate images from a text prompt (SPENDS BUZZ)
```text
Generate images from a text prompt on Civitai's generator.
🔴 THIS SPENDS REAL BUZZ AND CANNOT BE UNDONE. A submitted generation is charged
the moment the orchestrator accepts it, and nothing local can call that back —
not --timeout, not Ctrl-C, not `civitai workflows cancel`. Preview the
price with --dry-run first; it calls the server's cost estimator and spends
nothing.
What the LEDGER then does with that charge — if the run fails, expires, or you
cancel it — is decided server-side, and this CLI cannot see your Buzz ledger — `civitai buzz` reports a balance, not a history, so settle it against your Buzz transaction history (/user/transactions).
CREDENTIAL: generation needs the AI Services scopes. Two credentials carry them:
`civitai login --scopes generate` (a browser login that opts into generation), or a full-scope personal API key (`civitai login --token `, created at https://civitai.com/user/account). A DEFAULT OAuth browser login
(`civitai login` with no --scopes) does NOT carry them and is refused — and
re-running plain `civitai login` will not fix that. Check yours with
`civitai whoami`.
The ONE exception is --print-input: it assembles the graph and exits before the
estimator, the submit and the balance read, so it needs no credential. Two
caveats, and they are NOT the same caveat. With --image it does need one, because
it uploads each local file first and that upload is authenticated. With
--checkpoint or --lora it still needs none — the model-version lookup is a public
read — but it is not OFFLINE: that lookup is a real request and fails without a
network. Only a bare --print-input needs neither a credential nor a network.
--max-cost IS AN ESTIMATE CHECK, NOT A SPENDING CAP. The cost this command shows
is an estimate, not a quote: the server's estimator returns no quote id, no
signed price and no expiry — there is nothing to hand back at submit time, and
no server-side ceiling is reachable from an API key at all. The realized charge
can exceed the estimate, and --max-cost cannot claw the difference back — it
never reaches the server. --max-cost compares the
ESTIMATE against your number and refuses locally before submitting; it catches a
--quantity typo, and that is all it can do. Do not run an unattended loop
believing it caps spend.
CONFIRMATION: an interactive run prints the estimate and your balance and asks
before spending. A non-interactive shell (pipe/CI) without --yes is REFUSED
rather than charged silently.
WHAT THE SERVER DOES NOT TELL YOU: the generator is permissive, not a validator.
An out-of-range --quantity is clamped with no error, and a checkpoint id that
does not exist is accepted with the ecosystem default silently substituted and
billed. This command therefore resolves every --checkpoint / --lora id against
the public model-version API BEFORE submitting, so a bad id is a hard local
error instead of a wrong charge, and it echoes the resolved model NAME in the
confirmation so you approve a name rather than an integer.
🔴 --dry-run's "Resources ready" line is NOT A PROMISE OF OUTPUT. It echoes the
server's `ready` flag, which reports only that the resources this job needs are
currently available — nothing about moderation, and nothing about whether the
job will actually produce an image. A run that reports resources ready can still
be charged and return nothing. Treat `ready: false` as "do not submit"; do not
read `ready: true` as a green light.
WAITING AND DOWNLOADING: by default the command waits for the job to finish and
writes every deliverable output into --out-dir as -.. Pass
--no-wait to print the workflow id and exit immediately, and pick the results up
later with `civitai workflows get `. Output URLs are PRESIGNED AND
EXPIRE, so download promptly; re-read the workflow for fresh links.
🔴 --timeout STOPS WAITING. IT DOES NOT STOP PAYING. The generation keeps
running server-side after the CLI gives up, and cancelling it does not stop the
cost already accrued — a mid-run cancel bills that. The same is true of Ctrl-C.
Both print the workflow id and the exact command to re-attach.
CRASH SAFETY: the idempotency key is written to a local file BEFORE the request
is sent, because the money moves server-side even if this process dies mid-POST.
If a submit's reply never arrives, re-run with --external-id :
the orchestrator dedupes on it and returns the PRE-EXISTING workflow instead of
charging a second time.
IMAGE-TO-IMAGE: --image attaches a reference image (repeatable).
A local png/jpeg is uploaded to Civitai first and the stored blob is referenced;
an https URL is passed through as-is, but must be publicly reachable, because the
generator downloads it server-side too. Either way the CLI reads the image's
width and height from its header and sends them — the server requires both and
rejects an entry without them.
🔴 --image REQUIRES --ecosystem, and the reason is money. The server turns a
text-to-image job into image-to-image only when the request names an ecosystem;
without one it ignores the images, generates from the prompt alone, and charges
you the full amount with no error. Worse, only SOME ecosystems accept reference
images at all (Qwen, Flux1Kontext, NanoBanana, Seedream, OpenAI, Grok and a few
more do; the SD family and the default do not) — and the cost estimate cannot
tell you which case you are in, because several edit-capable ecosystems price
identically with and without images. Name an ecosystem you know supports editing.
🔴 The server SILENTLY TRUNCATES too many reference images. Per-ecosystem limits
run from 1 to 7 and are not knowable from here; over the limit the extras are
dropped with no error and the truncated job is billed. The CLI refuses more than
7 (no ecosystem accepts more) and warns for anything above 1.
RAW GRAPHS: --input (or --input -) sends a generation-graph JSON document
exactly as written, instead of building one from the flags above. It is how you
reach graph parameters this CLI has no flag for. Get a valid starting point with
--print-input, which assembles the graph, prints it, and exits without
submitting or even pricing anything.
--input is txt2img only in this release. It cannot be combined with the content
flags (--negative-prompt, --quantity, --aspect-ratio, --checkpoint, --lora) or
with a prompt argument; the execution flags all still apply. Keys that belong to
the request ENVELOPE rather than the graph — civitaiTip, creatorTip, buzzType,
tags, externalId — are REFUSED in an input file: they are this CLI's to set, and
a tip in particular is real Buzz that --dry-run structurally cannot see. Keys
this CLI does not recognise are passed through with a warning, because the
server silently drops what it does not declare rather than reporting an error.
🔴 --input DOES NOT get the model-id safety net. --checkpoint and --lora are
resolved against the public API before submitting, so a bad id fails locally
instead of being billed with a substituted model; a raw graph is not
interpreted, so nothing in it is checked before you pay for it.
```
```bash
# Preview the price — spends nothing
civitai generate "a cat wearing sunglasses" --dry-run
# The same estimate as JSON, for scripts
civitai generate "a cat wearing sunglasses" --dry-run --json
# Generate 4 images, refusing if the estimate exceeds 50 Buzz
civitai generate "a cat wearing sunglasses" --quantity 4 --max-cost 50
# A specific checkpoint plus a LoRA at 0.8 strength
civitai generate "a cat" --checkpoint 128713 --lora 250712:0.8
# Image-to-image from a local file — --ecosystem is required
civitai generate "make it winter" --ecosystem Flux1Kontext --image ./cat.png --dry-run
# …or from a public URL, with two reference images
civitai generate "combine these" --ecosystem Seedream \
--image https://example.com/a.jpg --image ./b.png --yes
# Wait, and write the images into ./out
civitai generate "a cat" --yes --out-dir ./out
# Fire and forget; collect the results later
civitai generate "a cat" --yes --no-wait
civitai workflows get
# Non-interactive (CI): --yes is required, or the run is refused
civitai generate "a cat" --yes --max-cost 20
# Graduate from flags to a raw graph: print, edit, send back
civitai generate "a cat" --quantity 2 --print-input > graph.json
civitai generate --input graph.json --dry-run
civitai generate --input graph.json --yes
# …or pipe it straight through
jq '.prompt = "a dog"' graph.json | civitai generate --input - --dry-run
```
| Flag | Description | Default |
|---|---|---|
| `--aspect-ratio string` | aspect ratio bucket, e.g. 1:1 (width/height derive from it) | — |
| `--checkpoint int` | checkpoint model-VERSION id (not a model id) — resolved before submitting | — |
| `--dry-run` | print the cost estimate and exit without submitting (spends nothing) | — |
| `--ecosystem string` | model family to generate with, e.g. Qwen or Flux1Kontext. Sent to the server verbatim and NOT checked locally; required with --image because the server only promotes a job to image-to-image when the ecosystem is stated | — |
| `--external-id string` | re-attach to an earlier submit by reusing its idempotency key (the orchestrator dedupes on it and returns the PRE-EXISTING workflow rather than charging again). Use the key recorded before the lost submit | — |
| `--fail-on-substitution` | refuse to submit if the server REPORTS it substituted a different checkpoint for the one you asked for. Checked against the ESTIMATE, so nothing is spent when it refuses. Off by default: the server substitutes deliberately so that a script pinned to a retired version keeps working. NOT A GUARANTEE: a server that does not report substitutions makes this flag silently inert, so it cannot be relied on as a spend guard against an older deployment | — |
| `--force` | overwrite existing output files instead of refusing | — |
| `--image stringArray` | reference image for image-to-image: a local file (png or jpeg, uploaded) or an https URL (passed through). Repeatable. Requires --ecosystem, and only some ecosystems accept reference images at all | — |
| `--input string` | read the generation graph from a JSON file ('-' for stdin) and send it as-is, instead of building one from flags. txt2img only. Cannot be combined with the content flags above | — |
| `--json` | emit the raw server payload on stdout (scriptable) | — |
| `--lora stringArray` | LoRA model-version id, optionally :strength (e.g. 250712:0.8). Repeatable | — |
| `--max-cost int` | refuse to submit if the ESTIMATE exceeds this many Buzz. This is an estimate check, NOT a spending cap: the estimate is not binding, the server enforces no ceiling, and the realized charge can be higher — this flag cannot claw that back | — |
| `--negative-prompt string` | negative prompt | — |
| `--no-download` | wait for the result and print the output URLs, but write no files | — |
| `--no-wait` | submit, print the workflow id and exit without waiting; collect the results later with 'civitai workflows get \' | — |
| `--out-dir string` | directory to write the generated files into (created if needed); named \-\.\ | `.` |
| `--print-input` | print the exact generation graph that would be sent and exit without submitting. Redirect it to a file, edit it, and feed it back with --input | — |
| `--quantity int` | number of images to generate (server default when unset; no -n shorthand, it reads as "no") | — |
| `--timeout duration` | how long to WAIT for the generation to finish (e.g. 5m, 0 waits indefinitely). This stops the CLI waiting; it does NOT stop the generation and does NOT stop the charge — the job continues server-side to completion | `10m0s` |
| `-y, --yes` | skip the confirmation and submit (required in a non-interactive shell) | — |
**`civitai images`**
Search images on Civitai
```text
Read-only access to Civitai images through the public REST API
(GET /api/v1/images). Videos and audio posts ride the same route — pick one
with --type image|video|audio.
No login is needed — these are public read routes (unlike `civitai app list`
and `app view`, which are refused without a token). The CLI still sends your
stored token when you have one; --anon forces an anonymous request.
`images search` is the feed; `images get ` is one image, by the id in
a civitai.com/images/ URL.
Both can render the GENERATION METADATA — prompt, negative prompt, sampler,
cfg, steps, seed, and the resources "recipe" of checkpoint plus LoRAs — which
is what makes this a reproduction tool rather than a gallery. `search` omits
it unless you pass --meta, matching the API (which leaves meta out by default
to keep pages small); `get` always asks for it.
An uploader can hide their generation data. Those images print
"meta: (hidden by uploader)" rather than failing.
--json writes the API response to stdout and nothing else — notes and errors go
to stderr, so `… --json | jq -e .` always parses. The document is the API's,
the bytes are not: it is re-indented on the way out, so do not diff or hash it
against the wire.
```
```bash
civitai images search --limit 5
civitai images search --model-id 4384 --period Month
civitai images search --base-model "Krea 2" --sort "Most Reactions" --period Week
civitai images search --nsfw --sort "Most Reactions" --period Month --meta
civitai images get 136456589
```
**`civitai images get `**
Get a single image by id (GET /api/v1/images?imageId=\)
```text
Fetch one image by its numeric id — the id in a civitai.com/images/ URL —
and render its generation metadata: prompt, negative prompt, sampler, cfg,
steps, seed, and the resources recipe (checkpoint plus LoRAs, with weights and
hashes). Metadata is requested implicitly, so this is the same detail block
`images search --meta` prints.
There is no per-id REST route. This is GET /api/v1/images?imageId= — the
search endpoint keyed to a single id — which is why `--json` hands back a
one-item search envelope rather than a bare image object.
The id is validated locally: a non-integer, non-positive, or beyond-32-bit
value is refused as a usage mistake before any request, so an oversized id
comes back as a clear refusal instead of a server error.
A resource line falls back to meta.hashes when the generator inlined no hash;
those hashes are exactly what `model-versions by-hash` resolves.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai images get 136456589
civitai images get 136456589 --json
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
**`civitai images search`**
Search images (GET /api/v1/images)
```text
Search images via GET /api/v1/images.
Filters: --model-id, --model-version-id, --post-id, --username, --nsfw and
--base-model (repeatable — the API ORs the values). --base-model is matched
LITERALLY, so a misspelling returns zero results rather than an error; the CLI
says so on stderr.
--type, --sort and --period take server-owned value sets.
The CLI does not check those value sets — it passes the value through and the
server rejects an unknown one with HTTP 400, reported as a usage mistake.
--sort is IGNORED when --model-id is set — the API returns that model's images
in its own order whatever you ask, so the CLI notes it on stderr rather than
let you believe the sort took. --model-version-id does honour --sort.
--meta adds each image's prompt, sampler, cfg, steps, seed, model and resource
list, rendered as a per-image block instead of the table (a table cannot hold a
prompt). With --json it adds the raw meta object to every item.
Paging: --limit takes 1–200; omit it for the server's default page size.
--page is shallow paging, --cursor is deep paging, and the next cursor is
printed under the results. The API caps page × limit at 1000 and answers 429
past it — the CLI reports that cap as a usage mistake, not a rate limit, so a
retry loop does not spin on it.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai images search --limit 5
civitai images search --model-version-id 128713 --sort Newest
civitai images search --base-model "Krea 2" --sort "Most Reactions" --period Week
civitai images search --type video --sort "Most Reactions"
civitai images search --nsfw --sort "Most Reactions" --period Month --meta
civitai images search --username some-user --cursor
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--base-model strings` | filter by base model; repeatable (e.g. --base-model "Krea 2" --base-model Flux). The API OR-combines the given values | — |
| `--cursor string` | pagination cursor from a previous response | — |
| `--json` | print the raw API JSON response (for scripting) | — |
| `--limit int` | results per page (1-200) | — |
| `--meta` | include generation metadata (prompt, sampler, seed, etc.) | — |
| `--model-id int` | filter by model id | — |
| `--model-version-id int` | filter by model version id | — |
| `--nsfw` | include NSFW results | — |
| `--page int` | page number (shallow paging; prefer --cursor) | — |
| `--period string` | time period (AllTime, Year, Month, Week, Day) | — |
| `--post-id int` | filter by post id | — |
| `--sort string` | sort order ("Most Reactions", "Most Comments", Newest) | — |
| `--type string` | filter by media type (image, video, audio) | — |
| `--username string` | filter by uploader username | — |
**`civitai login`**
Authenticate with Civitai
```text
Authenticate the CLI with Civitai for authenticated commands (whoami,
app submit).
By default `civitai login` runs a browser-based device login: it prints a
URL and a code, you approve in your browser, and the CLI stores short-lived
OAuth tokens that refresh automatically.
Alternatively, pass --token to store a personal API key created at
https://civitai.com/user/account (API Keys). Passing --token with NO value prints
where to create that key and how to re-run (it does not log in). Either way the
credential is saved
to your config file (~/.config/civitai/config.yaml, owner-readable only). The
CIVITAI_TOKEN environment variable still overrides the stored credential.
SCOPES. By DEFAULT `civitai login` grants identity + Apps submit +
dev-tunnel, and deliberately NOT Buzz-spend — a plain login must never silently
hand the CLI authority to spend your Buzz. Opt in per named scope set with
--scopes (additive — you keep everything the default grants):
--scopes generate
run `civitai generate` and read your Buzz balance (AIServicesRead|AIServicesWrite|BuzzRead) — this login WILL be able to SPEND your Buzz
So `civitai login --scopes generate` yields ONE credential that can both
submit apps and run `civitai generate`. Without it, generation is refused
and `dev:live` cannot spend; the other way to get spend authority is a
full-scope personal API key (`civitai login --token `, created at
https://civitai.com/user/account).
--scopes applies only to the browser device login; it is rejected with --token.
Switching accounts: running `civitai login` again overwrites the stored
credential with the new account — no separate logout needed. (Check the active
account with `civitai whoami`.)
```
```bash
civitai login # browser device login (recommended); no Buzz-spend
civitai login --scopes generate # ALSO grant generation + Buzz SPEND (civitai generate)
civitai login --no-browser # device login without auto-opening a browser
civitai login --token # store a personal API key instead
civitai login --token # no value: print where to create a personal key
civitai login # run again to SWITCH the active account (overwrites the stored credential)
```
| Flag | Description | Default |
|---|---|---|
| `--no-browser` | do not attempt to open a browser for device login | — |
| `--scopes strings` | extra scope sets to request on a browser device login, additive on top of the default (valid: generate). --scopes generate grants generation AND Buzz-SPEND authority; omit it and this login cannot spend your Buzz. Not valid with --token | — |
| `--token string[="(no value)"]` | store a personal API key instead of the browser device login (pass with no value to print where to create one) | — |
**`civitai model-versions`**
Inspect model versions on Civitai
```text
Read-only access to Civitai model versions through the public REST API
(GET /api/v1/model-versions/{id} and .../by-hash/{hash}).
Aliases: model-version, mv.
No login is needed — these are public read routes (unlike `civitai app list`
and `app view`, which are refused without a token). The CLI still sends your
stored token when you have one; --anon forces an anonymous request.
A model VERSION is the downloadable unit, and it is what most of the rest of
this CLI wants: `civitai download --version `,
`civitai generate --checkpoint ` and `--lora ` all take a version
id, never a model id. `civitai models get ` is where you read those
version ids off a model.
`by-hash` runs that lookup backwards: it identifies a file you already have
on disk, which is how you put a name to an unlabelled .safetensors.
--json writes the API response to stdout and nothing else — notes and errors go
to stderr, so `… --json | jq -e .` always parses. The document is the API's,
the bytes are not: it is re-indented on the way out, so do not diff or hash it
against the wire.
```
```bash
civitai model-versions get 128713
civitai mv get 128713 --json
civitai model-versions by-hash 5D8D26E2A6
```
**`civitai model-versions by-hash `**
Get a model version by file hash (GET /api/v1/model-versions/by-hash/{hash})
```text
Look up a model version by any of its file hashes:
GET /api/v1/model-versions/by-hash/{hash}.
AutoV1, AutoV2, SHA256, CRC32 and BLAKE3 hashes all work — the server
upper-cases the value, so case does not matter here. This is the "what IS this
file?" lookup for a .safetensors you already have on disk.
Note the API reports SHA256 in UPPER case while sha256sum prints lower case, so
case-fold before comparing if you hash a file yourself. (`civitai download`'s
own verification is already case-insensitive.)
On a hit the CLI prints a ready-to-run download line for the resolved version.
It uses --version rather than a bare positional id deliberately: a bare id is
ambiguous between a model id and a version id, and the printed command must
never be the one that trips that stop.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai model-versions by-hash 5D8D26E2A6
civitai mv by-hash 5D8D26E2A6 --json
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
**`civitai model-versions get `**
Get a model version by id (GET /api/v1/model-versions/{id})
```text
Get one model version by its version id: GET /api/v1/model-versions/{id}.
This is the id `civitai download --version` and
`civitai generate --checkpoint / --lora` take. A non-integer argument is
refused locally, as a usage mistake, before any request is made.
The output carries the base model, the trigger words, the AIR identifier and
every file with its size and type. A version whose primary file is not model
weights is tagged with that type ([Archive], [Training Data], [Other]).
What a version does NOT carry is model-level data. Its .model is a stub — the
CLI reads a name, a type and an nsfw flag out of it — and there is no creator
and no model-level download count on this response at all. If you started from
a version and need those, read them from `models get` / `models search`
and join on the version's .modelId.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai model-versions get 128713
civitai mv get 128713 --json
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
**`civitai models`**
Search and inspect models on Civitai
```text
Read-only access to Civitai models through the public REST API
(GET /api/v1/models, GET /api/v1/models/{id}).
No login is needed — these are public read routes (unlike `civitai app list`
and `app view`, which are refused without a token). The CLI still sends your
stored token when you have one; --anon forces an anonymous request.
`models search` is the discovery surface — filter by --query / --tag /
--username / --type / --base-model, order with --sort / --period, and page with
--limit / --page / --cursor. `models get ` returns one model with its
full version list.
What lives one level down: a model is the PAGE, a model VERSION is the
downloadable unit. `civitai download` and `civitai generate --checkpoint`
both take a version id, which `models get` lists.
--json writes the API response to stdout and nothing else — notes and errors go
to stderr, so `… --json | jq -e .` always parses. The document is the API's,
the bytes are not: it is re-indented on the way out, so do not diff or hash it
against the wire.
```
```bash
civitai models search --query "pony" --limit 5
civitai models search --type LORA --base-model Illustrious --limit 20
civitai models get 4384
civitai models get 4384 --json
```
**`civitai models get `**
Get a model by id (GET /api/v1/models/{id})
```text
Get one model by id: GET /api/v1/models/{id}.
The id is the number in a civitai.com/models/ URL. A non-integer argument
is refused locally, as a usage mistake, before any request is made.
The human output lists every published VERSION (id, name, base model) — that
version id is what `civitai download --version` and
`civitai generate --checkpoint` take. A version whose primary file is not
model weights is tagged with its actual file type ([Archive], [Training Data],
[Other]); it still downloads, the tag just says it is not a .safetensors.
--json carries much more than the human view — the description HTML, per-file
hashes and download URLs, and the full stats block.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai models get 4384
civitai models get 4384 --json
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
**`civitai models search`**
Search models (GET /api/v1/models)
```text
Search models via GET /api/v1/models.
Filters: --query (free text), --tag, --username, --base-model (repeatable — the
API ORs the given values) and --nsfw. --base-model is matched LITERALLY, so a
misspelling returns zero results rather than an error; the CLI says so on
stderr.
--type, --sort and --period take server-owned value sets.
The CLI does not check those value sets — it passes the value through and the
server rejects an unknown one with HTTP 400, reported as a usage mistake.
Paging: --limit takes 1–100; omit it for the server's default page size.
--page is shallow paging, --cursor is deep paging, and the next cursor is
printed under the results. The API caps page × limit at 1000 and answers 429
past it — the CLI reports that cap as a usage mistake, not a rate limit, so a
retry loop does not spin on it.
--period changes the SORT, not the DOWNLOADS column: the API returns only the
all-time download count, so a later row can legitimately show more downloads
than an earlier one. The column is labelled DL(all-time) for that reason.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai models search --query "pony" --limit 5
civitai models search --type LORA --sort "Most Downloaded" --period Month
civitai models search --base-model Pony --base-model Illustrious --limit 20
civitai models search --username some-creator --cursor ''
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--base-model strings` | filter by base model; repeatable (e.g. --base-model Pony --base-model "Illustrious"). Distinguishes video checkpoints ("Wan Video 2.2 T2V-A14B") that all share --type Checkpoint | — |
| `--cursor string` | pagination cursor from a previous response | — |
| `--json` | print the raw API JSON response (for scripting) | — |
| `--limit int` | results per page (1-100) | — |
| `--nsfw` | include NSFW results | — |
| `--page int` | page number (shallow paging; prefer --cursor for deep paging) | — |
| `--period string` | time period (AllTime, Year, Month, Week, Day) | — |
| `--query string` | text search query | — |
| `--sort string` | sort order (e.g. "Highest Rated", "Most Downloaded", Newest) | — |
| `--tag string` | filter by tag name | — |
| `--type string` | filter by model type (e.g. Checkpoint, LORA, TextualInversion) | — |
| `--username string` | filter by creator username | — |
**`civitai tags`**
Search model tags on Civitai
```text
Read-only access to Civitai MODEL tags through the public REST API
(GET /api/v1/tags).
No login is needed — these are public read routes (unlike `civitai app list`
and `app view`, which are refused without a token). The CLI still sends your
stored token when you have one; --anon forces an anonymous request.
These are the model taxonomy — the same names `civitai models search --tag`
filters on, which is what this group is for: find the tag, then search with it.
The group is search-only, and deliberately so: the public route answers with a
tag NAME and a LINK per tag and nothing else, so there is nothing for a
`tags get` to fetch.
Not to be confused with `civitai articles search --tags`, which takes numeric
tag IDS rather than these names — a different filter on a different endpoint.
--json writes the API response to stdout and nothing else — notes and errors go
to stderr, so `… --json | jq -e .` always parses. The document is the API's,
the bytes are not: it is re-indented on the way out, so do not diff or hash it
against the wire.
```
```bash
civitai tags search --query anime --limit 10
civitai tags search --query anime --json
```
**`civitai tags search`**
Search tags (GET /api/v1/tags)
```text
Search model tags via GET /api/v1/tags.
--query matches the tag name; omit it to page the whole tag list.
--limit takes 1–200; omit it for the server's default page size.
Paging is --page only. This endpoint answers with the classic page envelope
(total items, current page, total pages) and no cursor, so there is no --cursor
here. The footer prints the next --page while the response says there is one.
Each row is a tag NAME and a LINK. The name is what
`civitai models search --tag ` takes — that is the follow-up this
command exists to set up. The link is the equivalent models query on the
website.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai tags search --query anime --limit 10
civitai tags search --limit 50 --page 2
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
| `--limit int` | results per page (1-200) | — |
| `--page int` | page number | — |
| `--query string` | text search query | — |
**`civitai upgrade`**
Update the civitai CLI to the latest release
```text
Download and install the latest civitai release, replacing this binary.
The latest release is resolved from the public GitHub releases API (no token is
ever sent). The downloaded tarball is verified against its SHA-256 checksum
before anything is replaced — a mismatch aborts the upgrade and leaves the
current binary untouched.
If this binary was installed via Homebrew, upgrade delegates to:
brew upgrade civitai/tap/civitai
(use --force to self-replace anyway).
```
```bash
civitai upgrade
civitai upgrade --force
```
| Flag | Description | Default |
|---|---|---|
| `--force` | reinstall even if already up to date, and self-replace a Homebrew install | — |
**`civitai users`**
Look up users on Civitai
```text
Read-only access to Civitai users through the public REST API.
No login is needed — these are public read routes (unlike `civitai app list`
and `app view`, which are refused without a token). The CLI still sends your
stored token when you have one; --anon forces an anonymous request.
NOTE: the only public users route is the SEARCH endpoint GET /api/v1/users,
keyed by ?query= or ?ids=. The per-id route /api/v1/users/{userId} is an
INTERNAL webhook (POST plus a system token) and is not usable from the CLI, so
`users get` resolves a user through that public search.
That is also why there is no `users search`: the search endpoint is already
what `users get` calls, and it answers with a handful of fuzzy neighbours
rather than a browsable, pageable list — it has no pagination envelope at all.
What comes back is identity only: id, username and avatar URL. For a user's
models use `civitai models search --username `; for their published
model COUNT use `civitai creators search --query `.
--json writes the API response to stdout and nothing else — notes and errors go
to stderr, so `… --json | jq -e .` always parses. The document is the API's,
the bytes are not: it is re-indented on the way out, so do not diff or hash it
against the wire.
```
```bash
civitai users get some-username
civitai users get 5
civitai users get 5 --json
```
**`civitai users get `**
Look up a user by username or id (public search: GET /api/v1/users)
```text
Look up a user by username or numeric id through the public user search
(GET /api/v1/users). A numeric argument is sent as ?ids= and returns exactly
that user; anything else is sent as ?query=.
A NAME lookup is FUZZY on the server side — the endpoint answers with the
closest-matching users, not with your user. So the CLI requires an exact
(case-insensitive) username match before it prints anybody: a typo lists the
near misses and fails as not-found rather than confidently printing the wrong
person. When several users match, the exact one is printed and the rest are
listed under "other matches". Pass the numeric id when you need certainty.
An unknown user comes back from this endpoint as an empty HTTP 200 rather than
a 404. The CLI still reports it as NOT FOUND, so a script gets the same signal
it would from a real 404.
The result is identity only — id, username, avatar URL. There are no stats and
no model list on this route.
Works anonymously (no login needed); --anon forces an anonymous request even
when you are logged in.
```
```bash
civitai users get some-username
civitai users get 5 --json
```
| Flag | Description | Default |
|---|---|---|
| `--anon` | force an anonymous request (ignore any stored login token) | — |
| `--json` | print the raw API JSON response (for scripting) | — |
**`civitai version`**
Print the CLI version, commit, and build date
```text
Print detailed build information for this civitai binary.
The version, commit, and date are stamped in at release time. For a plain
"go install" or source build they fall back to the embedded Go build info
(module version + VCS revision/time).
After printing build info, this command makes a single unauthenticated call to
the GitHub releases API to tell you if a newer release is available. The check
is best-effort (short timeout, fails silently offline) and never sends your API
token. Skip it with --no-update-check or by setting CIVITAI_NO_UPDATE_CHECK.
```
```bash
civitai version
civitai version --no-update-check
```
**`civitai whoami`**
Verify your stored API token and its capabilities
```text
Verify the stored API token by calling the Civitai API and printing the
authenticated user PLUS a short capability summary: the credential type (OAuth
login vs personal API key), whether it can read your Buzz balance, and whether
it can spend Buzz. The money-path dead end — an OAuth `civitai login` token
can submit/withdraw but cannot spend Buzz — is surfaced here, before dev:live.
Reads the token from config or CIVITAI_TOKEN.
```
```bash
civitai whoami
civitai whoami --scopes # also list every granted scope
civitai whoami --json # raw JSON (scriptable)
```
| Flag | Description | Default |
|---|---|---|
| `--json` | emit raw JSON (scriptable) | — |
| `--scopes` | also print the full decoded scope list | — |
**`civitai workflows`**
List, inspect and cancel generation workflows
```text
Work with the generation workflows your account has submitted.
A workflow is one submitted generation job. `civitai generate` prints a
workflow id; `list` and `get` are how you find one afterwards — which is what
makes `--no-wait`, a --timeout expiry and a Ctrl-C recoverable rather than a
dead end.
`list` and `get` are reads and SPEND NOTHING. 🔴 `cancel` stops a job but does
not undo its cost — a mid-run cancel bills what has already accrued.
```
```bash
civitai workflows list
civitai workflows get 01JABCXYZ
civitai workflows get 01JABCXYZ --json
civitai workflows cancel 01JABCXYZ
```
**`civitai workflows cancel `**
Cancel a running generation workflow (DOES NOT UNDO THE CHARGE)
```text
Cancel a generation workflow that is still running.
🔴 CANCELLING IS NOT A WAY TO SAVE MONEY. A mid-run cancel BILLS THE ACCRUED
COST, orchestrator-side: by the time a workflow is running the money has already
moved, and stopping it does not call that back. Cancel a job because you no
longer want its OUTPUT — never to save money, and never to undo a submit you
regret. What the ledger does afterwards is decided server-side, and this CLI cannot see your Buzz ledger — `civitai buzz` reports a balance, not a history, so settle it against your Buzz transaction history (/user/transactions).
That is also why `civitai generate --timeout` and Ctrl-C do not cancel anything:
stopping the wait costs nothing, while stopping the job would cost the same as
letting it finish and would throw away the result you already paid for.
Cancelling an already-finished workflow is harmless — the outputs of a succeeded
workflow are not deleted by it (use the website to delete results).
This needs the same AI Services scopes that `civitai generate` needs:
`civitai login --scopes generate` (a browser login that opts into generation), or a full-scope personal API key (`civitai login --token `, created at https://civitai.com/user/account).
CONFIRMATION: cancelling is IRREVERSIBLE and destroys a job you have already paid
for, so an interactive run asks first. Pass `--yes` to skip the prompt in a
script; a non-interactive shell without `--yes` REFUSES rather than cancelling
silently.
```
```bash
civitai workflows cancel 01JABCXYZ
civitai workflows cancel 01JABCXYZ --yes
civitai workflows cancel 01JABCXYZ --json --yes
```
| Flag | Description | Default |
|---|---|---|
| `--json` | emit the raw server reply on stdout (scriptable) | — |
| `-y, --yes` | skip the confirmation prompt and cancel (for scripts/CI) | — |
**`civitai workflows get `**
Show one generation workflow and its outputs
```text
Show one generation workflow: its status, its steps, and its outputs.
Use it to re-attach to a job you did not wait for — after `--no-wait`, after a
--timeout expiry, or after Ctrl-C. The workflow id is printed by
`civitai generate` in all three cases.
OUTPUT URLS ARE PRESIGNED AND EXPIRE. The links this prints are short-lived;
fetch them promptly or re-run this command for fresh ones.
Outputs that are blocked by moderation, not available, or hidden are listed with
the reason rather than omitted — a finished workflow can legitimately contain
fewer usable results than it was charged for, and silently dropping them would
make that invisible.
Reading a workflow SPENDS NOTHING. It needs the same AI Services scopes that
`civitai generate` needs: `civitai login --scopes generate` (a browser login that opts into generation), or a full-scope personal API key (`civitai login --token `, created at https://civitai.com/user/account).
```
```bash
civitai workflows get 01JABCXYZ
civitai workflows get 01JABCXYZ --json
```
| Flag | Description | Default |
|---|---|---|
| `--json` | emit the raw server payload on stdout (scriptable) | — |
**`civitai workflows list`**
List the generation workflows you have submitted
```text
List your own generation workflows, newest first.
This is the feed behind the website's generator queue: one entry per submitted
job, with its status, when it was created, what it cost and how many outputs it
produced.
PAGING is by cursor, not page number. --limit sets the page size; when more
results exist the command prints the next cursor, which you pass back as
--cursor to fetch the following page. Deep pages are not cached server-side, so
walk them at a civil pace.
Each row reports outputs as "/". They differ when an output
was blocked by moderation, never landed, or was hidden on the website — a
workflow you were charged for can legitimately have fewer usable results than it
produced, and collapsing the two numbers would hide that. Use
`civitai workflows get ` for the per-output reasons and the URLs.
Reading SPENDS NOTHING. It needs the same AI Services scopes that
`civitai generate` needs: `civitai login --scopes generate` (a browser login that opts into generation), or a full-scope personal API key (`civitai login --token `, created at https://civitai.com/user/account).
```
```bash
civitai workflows list
civitai workflows list --limit 5
civitai workflows list --limit 50 --cursor
civitai workflows list --json
```
| Flag | Description | Default |
|---|---|---|
| `--cursor string` | opaque cursor from a previous page's next-cursor line | — |
| `--json` | emit the raw server payload on stdout (scriptable) | — |
| `--limit int` | how many workflows to fetch in this page (server default when unset) | — |
| `--tag stringArray` | only list workflows carrying this orchestrator tag. Repeatable | — |
## Local dev against the real backend
The scaffold ships npm scripts for the inner dev loop (`npm run dev:harness` for
the mock host, `npm run dev:live` for the real backend). Two `app` subcommands
support the live loop and are **invite-gated during the pre-GA beta**:
* **`civitai app dev-token `** mints a short-lived dev block token for
`npm run dev:live` (real backend). Real generation that spends Buzz needs a
full-scope personal API key.
* **`civitai app dev-tunnel`** previews your **local** dev server rendered inside
the real Civitai host (real session, real pickers, real Buzz) via a hardened
reverse SSH tunnel.
See the [Quickstart](../guide/quickstart) for the end-to-end flow.
---
---
url: /site/reference/collections.md
description: 'List, search, and fetch public Civitai collections.'
---
# Collections
A **collection** is a curated set of resources (models, images, articles, or
posts) grouped by a user on Civitai. These endpoints expose **public**
collections only.
::: tip Public, edge-cached, rate-limited
Both endpoints are **public** — they work anonymously and always evaluate the
request as anonymous, so a token is *optional* and never changes the data you
get back (the result is a pure function of the URL + your region). Only
**public** collections are ever returned — private collections are unreachable,
and a private collection is indistinguishable from a missing one. Responses are
edge-cached (`public, s-maxage=300`) and **conservatively rate-limited**; on a
`429` respect the `Retry-After` header. Mature covers/collections are clamped to
the SFW ceiling in restricted regions regardless of the `nsfw` param. There is
no "my collections" mode here — own-collection discovery is a per-user
(authoring) surface, not public discovery.
:::
## List collections
```
GET /api/v1/collections
```
**Auth:** Public.
### Query parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `limit` | integer (1–100) | 100 | Number of items per page. |
| `cursor` | integer | — | Keyset cursor (a collection ID). Use `metadata.nextCursor` from the previous response. **Only supported with the default `Newest` sort** — combining a cursor with `sort=Most Followers` returns `400`. |
| `query` | string (≤ 100 chars) | — | Full-text search over the collection name. |
| `sort` | `Newest` | `Most Followers` | `Newest` | Sort order. |
| `nsfw` | boolean | `false` | If `true`, include mature content. Ignored (clamped to SFW) in restricted regions. |
An invalid param — or a cursor combined with `sort=Most Followers` — returns `400`.
### Response
Envelope: `{ items, metadata: { nextCursor, nextPage } }`.
```json
{
"items": [
{
"id": 1201,
"name": "Favorite anime LoRAs",
"description": "A running list of the best anime LoRAs.",
"type": "Model",
"nsfwLevel": 1,
"read": "Public",
"isPublic": true,
"itemCount": 42,
"coverImageUrl": "https://image.civitai.com/.../cover.jpeg",
"user": { "id": 4021, "username": "some-curator" }
}
],
"metadata": {
"nextCursor": 1180,
"nextPage": "https://civitai.com/api/v1/collections?limit=100&cursor=1180"
}
}
```
* `itemCount` counts only **accepted** items in the collection.
* `coverImageUrl` is a ready-to-use CDN URL (or `null` when there is no viewable
cover, e.g. a mature cover clamped out in a restricted region).
* `metadata.nextCursor` / `metadata.nextPage` are omitted on the last page.
### Example
```bash
curl "https://civitai.com/api/v1/collections?limit=5&query=anime&sort=Newest"
```
## Get a collection
```
GET /api/v1/collections/{id}
```
**Auth:** Public.
### Path parameters
| Name | Type | Description |
|------|------|-------------|
| `id` | integer (1–2147483647) | Collection ID. |
### Response
```json
{
"id": 1201,
"name": "Favorite anime LoRAs",
"description": "A running list of the best anime LoRAs.",
"type": "Model",
"nsfwLevel": 1,
"read": "Public",
"isPublic": true,
"coverImageUrl": "https://image.civitai.com/.../cover.jpeg",
"user": { "id": 4021, "username": "some-curator" },
"tags": [ { "id": 5, "name": "anime" } ]
}
```
Returns `404` if the collection doesn't exist **or** is private (the two cases
are indistinguishable):
```json
{ "error": "No collection with id 0" }
```
### Example
```bash
curl "https://civitai.com/api/v1/collections/104"
```
---
---
url: /apps/guide/comfy-cloud.md
description: >-
Drive ComfyUI from an App Block — either by naming a server-registered recipe,
or by shipping your own graph inline. The two arms, the gates on each, the
budget rules, and how to try it in the local harness.
---
# Comfy on Civitai (customComfy)
Most generation from an App Block goes through a bounded
[**text-to-image**](./text-to-image) body: you send a prompt, model, and a few
params, and the host builds the generation graph for you. **Comfy on Civitai**
(`customComfy`) is the other path — it drives ComfyUI, for effects a simple
txt2img body can't express (a panorama stitch, a multi-stage pipeline, a
custom-node graph).
`customComfy` has **two arms**, selected by `mode`:
| Arm | `mode` | Your block sends | Who can run it |
| --- | --- | --- | --- |
| **[recipe](#the-recipe-arm)** | omitted, or `'recipe'` | a registered recipe id + bounded `params` | closed-beta builders |
| **[inline](#the-inline-arm-ship-your-own-graph)** | `'inline'` (required) | **the ComfyUI graph itself**, a declared resource manifest, and a `maxBuzz` ceiling | app developers |
::: danger This page used to say you could not do the second one
Earlier revisions stated flatly that a block never ships a ComfyUI graph and
could not bring its own. That was true when the recipe arm was the only one; the
inline arm shipped afterwards and this page was not updated. A developer working
against the live feature read the old sentence, believed it over their own
testing, and concluded the capability did not exist. It does.
:::
::: warning Closed beta — access is limited
Comfy on Civitai is part of the [closed-beta](./) Apps platform and is
**mod-gated**, and the inline arm additionally requires an **app-developer**
account. You can scaffold and run the recipe sample against the local mock host
today (see [Try it locally](#try-it-locally)).
:::
## The recipe arm
The block sends a tiny body that *names* a workflow the platform already owns:
```ts
import type { WorkflowBodyCustomComfy } from '@civitai/app-sdk/blocks';
// The block picks a registered recipe id + a small, per-recipe-validated params
// object. The server owns the workflow in full.
const body: WorkflowBodyCustomComfy = {
kind: 'customComfy',
recipe: 'starter-comfy-txt2img', // a SERVER-registered, code-reviewed id
params: {
prompt: 'a serene alpine lake at golden hour',
// seed?: number | null — omit to let the orchestrator pick
},
};
```
`recipe` selects the workflow; `params` is a small, bounded object the recipe
validates. That's the entire wire surface for this arm.
Note that `mode` is **omitted**, not set to `'recipe'`. The server declares it an
optional literal precisely so that a body without the key parses as a recipe —
which is what keeps every block written before the inline arm existed working
unchanged.
### The recipe-gated model
A **recipe** is a fixed, **server-registered, code-reviewed** ComfyUI workflow,
identified by a stable id (for example `starter-comfy-txt2img`). The recipe —
not your block — owns:
* the **ComfyUI graph** itself,
* the **resource allowlist** (which checkpoints / LoRAs the graph may use),
* the **checkpoint policy**, and
* a hard per-generation **Buzz ceiling** (`maxBuzz`) backed by an aggressive
step timeout.
Your block influences none of that beyond **choosing the recipe id and its
`params`**. An unknown or unregistered `recipe` is rejected **fail-closed** at
the server boundary, and any `params` field the recipe's schema doesn't accept
is stripped.
::: tip Why the recipe arm works this way — the security model
A block runs in an untrusted sandboxed iframe. Code review is what makes a recipe
trustworthy: a human checked the graph, the resources it pins, and the ceiling it
declares. Pinning a generation to a reviewed, in-repo artifact keeps all three on
Civitai's side of the boundary. It's the same "the host brokers, you don't"
principle as the rest of the [bridge](./concepts#the-host-block-bridge), applied
to ComfyUI.
The [inline arm](#the-inline-arm-ship-your-own-graph) has no such review, so it
replaces it with three mechanical gates rather than dropping the requirement.
:::
The trade-off is deliberate: **within this arm** you choose the recipe, not the
graph. If the registered recipes don't cover what you need, you have two options
— request a new recipe (below), or ship the graph yourself with the
[inline arm](#the-inline-arm-ship-your-own-graph).
### Requesting a new recipe
Because a recipe is a reviewed, in-repo artifact, adding one isn't self-serve —
it's a change the Civitai team makes. Request one through the **same channel as
beta access**: open a [Request access](https://github.com/civitai/cli/issues/new?template=request-access.yml)
issue on the `civitai/cli` repo describing the workflow you need (what the graph
should do, the models involved, rough runtime), or reach out to the Civitai team
if you already have builder access.
Ask for a recipe when you want a graph available to **every viewer** of your
block. If you just want to run a graph now, that's what the inline arm is for.
## The inline arm (ship your own graph)
Set `mode: 'inline'` and the body carries the ComfyUI graph itself, plus a
declared manifest of every resource it needs:
```ts
// The inline arm's body shape, written out here rather than imported so the
// shape is visible. It mirrors the server's schema field-for-field.
type InlineComfyBody = {
kind: 'customComfy';
mode: 'inline';
workflow: Record }>;
resources: string[];
prompt?: string;
negativePrompt?: string;
maxBuzz: number;
};
const CHECKPOINT = 'urn:air:sdxl:checkpoint:civitai:101055@128078';
const body: InlineComfyBody = {
kind: 'customComfy',
mode: 'inline',
workflow: {
'1': { class_type: 'CheckpointLoaderSimple', inputs: { ckpt_name: CHECKPOINT } },
'2': { class_type: 'CLIPTextEncode', inputs: { text: 'a mountain at dawn', clip: ['1', 1] } },
'3': { class_type: 'CLIPTextEncode', inputs: { text: '', clip: ['1', 1] } },
'4': { class_type: 'EmptyLatentImage', inputs: { width: 1024, height: 1024, batch_size: 1 } },
'5': {
class_type: 'KSampler',
inputs: {
seed: 0, steps: 20, cfg: 7, sampler_name: 'euler', scheduler: 'normal', denoise: 1,
model: ['1', 0], positive: ['2', 0], negative: ['3', 0], latent_image: ['4', 0],
},
},
'6': { class_type: 'VAEDecode', inputs: { samples: ['5', 0], vae: ['1', 2] } },
'7': { class_type: 'SaveImage', inputs: { images: ['6', 0], filename_prefix: 'civitai' } },
},
// EVERY AIR the graph names must also be declared here.
resources: [CHECKPOINT],
prompt: 'a mountain at dawn',
maxBuzz: 90, // ALSO the step timeout, in seconds — see below
};
```
::: warning The published SDK does not type the inline arm yet
That is why the shape above is written out by hand rather than imported. In the
pinned `@civitai/app-sdk@0.31.0` — also the newest published version —
`WorkflowBodyCustomComfy` is the **recipe** shape only
(`{ kind, recipe, params }`): there is no `mode` field on it, and no
`WorkflowBodyCustomComfyInline` or `InlineComfyNode` export to import. The
server accepts an inline body; the published types have not caught up.
So declare the shape locally, as above. When you narrow, narrow on the **value**
of `body.mode === 'inline'` — never on whether the `mode` key is present,
because a recipe body may legitimately carry `mode: 'recipe'` or even
`mode: undefined`.
:::
### `mode: 'inline'` is required
Including a `workflow` key does not select this arm. A body without `mode` routes
to the **recipe** arm and is then rejected for a missing `recipe` — an error that
reads as though something unrelated went wrong. This is the single most common
first-try mistake.
### `resources` is a declared manifest, not an inference
Every AIR URN that appears anywhere in your graph — including as an **object
key** — must also appear in `resources`, or the submit is rejected. The server
does not extract them for you, and the match is **whole-string** (trimmed,
case-insensitive): an AIR embedded inside a longer string does not count as
declared.
That flat array is the entire surface the entitlement check runs over, and the
containment rule is what makes checking it sufficient. It is also what the
orchestrator downloads — anything your graph references that is missing from the
list would fail at load time inside ComfyUI regardless.
Rules that reject a body outright:
* **A `civitai`-sourced AIR must carry a model version id** —
`urn:air:::civitai:@`. Without one there
is no version to check entitlement against. The AIR grammar is parsed strictly
here: all four segments are required.
* **Permitted AIR types are model weights only** — `checkpoint`,
`diffusion_model`, `unet`, `lora`, `lycoris`, `dora`, `embedding`, `hypernet`,
`controlnet`, `vae`, `upscaler`, `clip`, `clipvision`, `text_encoders`,
`motion` and a few siblings. An `oci:image` container AIR is **not** permitted.
Anything the allowlist does not name is rejected, not passed through.
* **At most 24 entries**, each at most 512 characters. **Duplicates are
rejected**, not silently deduped.
### Graph limits
All of these **reject** the body; none truncates it.
| Limit | Value |
| --- | --- |
| Nodes | 1 – 300 |
| Serialized graph size | ≤ 262144 bytes (256 KB) |
| Nesting depth | ≤ 128 levels |
| `prompt` / `negativePrompt` | ≤ 1500 characters each |
Each node is exactly `{ class_type, inputs }` and the node schema is strict, so a
raw ComfyUI **Save (API Format)** export needs its per-node `_meta` key stripped
— otherwise the whole body bounces rather than the key being dropped.
An `inputs` value is either a literal or a `[nodeId, outputIndex]` pair wiring
that input to another node's output, exactly as in ComfyUI.
### What replaced code review
A recipe is trustworthy because a human read it. An inline graph has no review,
so three fail-closed gates stand in — all of which run **before** any spend
reservation or orchestrator call, so a rejection costs nothing:
1. **AIR containment** — the rule above. Without it, gating `resources` would
prove nothing about what the graph actually loads.
2. **Entitlement**, over the declared `resources`, and **stricter than the
on-site generator**: early-access `hasAccess` and Private/epoch subscription
are both folded in, an unresolvable version id is a hard rejection rather than
a silent drop, and a resource the site would normally **substitute** with a
sibling version is rejected instead — your graph names one exact AIR string
and nothing rewrites it.
3. **A moderation sweep** over every distinct string leaf in the graph, not just
your declared `prompt`. A real graph carries its prompts inside
`CLIPTextEncode` nodes, so auditing a declared field alone would leave
moderation reading a value the generation never uses. A clean declared
`prompt` therefore cannot launder a graph prompt.
Fields the orchestrator's own step input accepts but a block must never set —
a session API token, a container image, a VRAM tier — are unreachable: the step
is constructed server-side from an allowlisted set, and the body schema is strict
so naming one of them is a rejection rather than a silently ignored key.
### Access
The inline arm is **app-developer-only** and **page-token-only**. The host runs a
developer check on every `customComfy` estimate *and* submit, so a non-developer
viewing your published block cannot submit one. Treat inline as a build-and-
iterate primitive; to serve a graph to every viewer, get it registered as a
recipe.
An inline body also carries **no account preference** — its schema has no
`accountType` field anywhere, so the host funds it from the default order.
## Submitting a generation
You submit a `customComfy` body through the **same** `useBuzzWorkflow()` hook you
use for text-to-image generation — the hook takes a full `WorkflowBody`
discriminated union and forwards the body verbatim, so switching to Comfy on
Civitai (either arm) is just a different `body`:
```tsx
import { useBuzzWorkflow } from '@civitai/blocks-react';
import type { WorkflowBodyCustomComfy } from '@civitai/app-sdk/blocks';
export function RunButton({ prompt }: { prompt: string }) {
const { estimate, submit, watch, status, result } = useBuzzWorkflow();
const run = async () => {
const body: WorkflowBodyCustomComfy = {
kind: 'customComfy',
recipe: 'starter-comfy-txt2img',
params: { prompt },
};
await estimate(body); // display estimate → result.cost.total
const snap = await submit(body);
await watch(snap.workflowId); // owns the loop; resolves on the terminal snapshot
};
return Generate ;
}
```
The host runs the estimate and submit server-side against your block token,
re-checking scopes and budget every time — your block never talks to the
orchestrator directly. `estimate()` returns a **display estimate**, not a firm
quote; the exact charge is known only when the workflow reaches a terminal
state (see [How generation is billed](#how-generation-is-billed)).
## Requirements
To use Comfy on Civitai, your app must be a **page app** (Comfy on Civitai is
not offered to slot apps) and its manifest must:
1. **Request the `ai:write:budgeted` scope.** This is the budgeted-generation
capability — the same scope text-to-image generation uses.
2. **Set `page.buzzBudgetPerGen` ≥ the per-generation Buzz ceiling.** On the
recipe arm that ceiling comes from the registry; on the inline arm it is the
`maxBuzz` you declare. The host mints each generation token with
`buzzBudgetPerGen` as its budget and
**gates every submit on `maxBuzz ≤ token.buzzBudget`**. If your
per-gen budget is below the recipe's ceiling, **every submit is rejected**
before it runs. But that makes the recipe's ceiling a **floor**, and a floor
is not a sizing method — these are two different quantities. The recipe's
`maxBuzz` is what the *server* enforces on one job: the step runs under a
timeout that physically bounds GPU-seconds, and you settle down to the real
runtime cost regardless. `buzzBudgetPerGen` is what *you* choose — the largest
single generation your app may request at all, i.e. the blast radius if the
app is exploited. Size it from how much damage you are willing to absorb, then
check it clears the floor; sizing it as *the recipe's price plus a margin* is
the classic mistake, and it re-breaks the app the day you call a pricier
recipe. See [Sizing the budget](../reference/manifest) in the manifest
reference.
```json
{
"$schema": "https://civitai.com/schemas/app-block/v1.json",
"blockId": "my-comfy-app",
"version": "0.1.0",
"name": "My Comfy App",
"type": "block",
"scopes": ["ai:write:budgeted"],
"page": {
"path": "/",
"title": "My Comfy App",
"buzzBudgetPerGen": 300
},
"contentRating": "g",
"minApiVersion": "1.0",
"buildCommand": "npm run build",
"outputDir": "dist"
}
```
The scaffold's Comfy on Civitai sample pairs `buzzBudgetPerGen: 300` with the
`starter-comfy-txt2img` recipe (per-generation ceiling **30** Buzz) — roughly 10×
the ceiling. That headroom is never spent: it bounds what the app is allowed to
*ask for*, while the charge is the real runtime cost. Read the 300 as a
blast-radius limit, not as 30 rounded up.
## How generation is billed
Comfy on Civitai is **post-paid with a hard ceiling** — you don't pay a fixed price up
front:
1. **Reserve the ceiling.** On submit, the host reserves the recipe's declared
`maxBuzz` against the token budget (this is why `buzzBudgetPerGen` must cover
it).
2. **Cap the runtime.** The recipe runs under an aggressive step **timeout** that
physically bounds GPU-seconds, so the worst-case charge can't exceed the
ceiling no matter what the graph does (roughly ~1 Buzz per GPU-second).
3. **Settle to actual.** When the workflow reaches a terminal state, the charge
**settles down to the real runtime** — a fast job costs less than the ceiling;
the reservation only bounds the maximum.
So a recipe's ceiling is a **worst case the host reserves against**, not a flat
price — and not a target to size `buzzBudgetPerGen` from.
Surface the `estimate()` value as an estimate in your UI, and read the final
`result.cost.total` on completion.
### 🔴 On the inline arm, `maxBuzz` is also the timeout in seconds
The recipe arm gets its ceiling from the registry. On the inline arm **you
declare it**, and the server derives the step timeout from that same number:
> `stepTimeoutSeconds = maxBuzz`
One number, both roles. That is not an implementation detail — it is what makes
the ceiling *physically* enforceable instead of merely asserted: there is only
one value, so the job cannot outrun the budget it was reserved against.
The consequence is the thing to internalise:
::: danger Setting `maxBuzz` low to be thrifty does not buy a cheap generation
It buys a job that is **killed** after that many seconds and comes back
`expired`, with nothing to show for it and no obvious explanation. `maxBuzz: 10`
is a 10-second timeout.
:::
You are billed the **real** cost either way — post-paid against measured GPU
seconds, settling to actual and refunding the unused remainder of the ceiling.
**Headroom is free.** Size `maxBuzz` from the wall-clock time your graph needs,
not from what you hope to pay. It must be an integer in **1 – 250**, and the host
still requires `maxBuzz ≤ token.buzzBudget`; over-budget comes back as a failed
snapshot naming both numbers.
For the same reason, `estimate()` on an inline body simply **echoes your
`maxBuzz` back** as `cost.total`. The orchestrator forwards the graph opaquely
and cannot price it, so that number is an **upper bound, not a quote** — surface
it as "up to N Buzz", not as a price.
## Try it locally
The `civitai` CLI's generation scaffold (`civitai app create`, the page-app
template that wires up Buzz + generation) ships a **Comfy on Civitai sample** — a
ready-made `src/comfy.ts` with body builders for **both arms**:
`buildComfyBody` for a `starter-comfy-txt2img` recipe body, and
`buildInlineComfyBody` for a complete inline SDXL graph like the one above. Both
are unit-tested, including the AIR-containment rule.
The **mock host** in `npm run dev:harness` implements the `customComfy` message
handlers, so the **recipe** sample runs **with no backend** — you can wire up the
body, click generate, and see the mocked estimate/submit/poll round-trip locally
before you ever have beta access:
```bash
civitai app create my-app # generation template ships src/comfy.ts
cd my-app && npm install
npm run dev:harness # mock host — recipe sample runs, no backend
npm test # exercises BOTH body builders
```
The inline builder is not on the scaffold's mode toggle yet: the mock host in the
pinned `@civitai/blocks-react` reads `body.params.accountType` unconditionally
and an inline body has no `params`, so driving it through the harness throws.
That is a mock-host bug being fixed, and it does **not** affect live
civitai.com — the scaffold's README tracks the state and spells out the wiring.
Real generation needs closed-beta access (plus an app-developer account for the
inline arm) and `npm run dev:live` / a submitted app; see the
[Quickstart](./quickstart#submitting-closed-beta).
## Not to be confused with orchestration recipes
Civitai uses the word "recipe" in two unrelated places:
* **App Blocks `customComfy` recipes** (this page) — server-registered ComfyUI
workflows a *block* invokes by id through the host bridge. The block sends
`{ kind: 'customComfy', recipe, params }`; it never holds a token for, or calls,
the orchestrator directly. (The [inline arm](#the-inline-arm-ship-your-own-graph)
goes through the same host bridge — shipping your own graph does **not** mean
talking to the orchestrator yourself.)
* **[Orchestration recipes](/orchestration/recipes/)** — task-oriented examples
for the public **Orchestration REST API** (WAN, Flux, upscalers, TTS, …), where
*you* hold a Bearer token and POST workflows to
`orchestration.civitai.com` yourself.
They're different products with different auth models. If you're building an App
Block, you want customComfy recipes; if you're calling the orchestration API
directly, you want [Orchestration recipes](/orchestration/recipes/).
## Next
* [Quickstart](./quickstart) — scaffold, run the harness, submit for review.
* [Concepts](./concepts) — the block / trust-frame / bridge model.
* [Scopes reference](../reference/scopes) — `ai:write:budgeted` and the full scope set.
* [Manifest reference](../reference/manifest) — `page.buzzBudgetPerGen` and every manifest field.
---
---
url: /apps/showcase.md
---
# Component showcase
A live gallery of every component in **`@civitai/components`** — the
framework-agnostic, dual-consumption design system that powers Civitai App
Blocks. Each demo renders **live and fully themed** (painted by the *published*
`@civitai/theme` + `@civitai/components` CSS a real consumer installs), above a
source panel you can toggle between the **framework-agnostic HTML** and the
**`@civitai/components-react`** binding.
* **HTML** authors follow the `data-civitai-ui` markup contract (see the
[Components reference](/apps/reference/components)); any HTML that follows it
renders identically to the React bindings.
* **React** authors use `@civitai/components-react` — thin `forwardRef` wrappers
that emit exactly that markup and auto-inject the stylesheet.
The previews re-theme with the site: toggle the header's light/dark switch and
every `--civitai-*` token re-resolves in place. The React snippets below are
type-checked against the pinned `@civitai/components-react@0.3.0` declarations on
every build, so they can't drift from the shipped API.
::: tip Setup
Load the tokens **and** the component CSS (order-independent):
```html
```
Then set `data-theme="light"` or `data-theme="dark"` on any ancestor. From JS,
`import { injectStyles } from '@civitai/components'; injectStyles()` injects both
idempotently.
:::
## Button
Native `` (ref-forwarded in React). `data-variant`:
`filled` · `light` · `outline` · `subtle`. `data-size`: `sm` · `md` · `lg`.
Loading sets `aria-busy` + `disabled` and prepends a `sm` loader.
\
```html
Filled
Light
Outline
Subtle
Small
Medium
Large
Saving…
Disabled
```
\
```tsx
import { Button, Group, Stack } from '@civitai/components-react';
Filled
Light
Outline
Subtle
Small
Medium
Large
Saving…
Disabled
;
```
## Badge
Presentational ``. `data-variant`: `filled` · `light` · `outline`.
`data-size`: `sm` · `md` · `lg`. `data-color` (new in 0.1.2): `info` ·
`success` · `warning` · `error` — omit it for the default primary accent.
\
```html
Filled
Light
Outline
Info
Success
Warning
Error
Small
Medium
Large
```
\
```tsx
import { Badge, Group, Stack } from '@civitai/components-react';
Filled
Light
Outline
Info
Success
Warning
Error
Small
Medium
Large
;
```
## Alert
`data-civitai-ui="alert"` with **`role="alert"`** (use `role="status"` for
non-urgent). `data-color`: `info` (default) · `success` · `warning` · `error`.
The body lives in `data-civitai-ui-alert-body`, with an optional
`data-civitai-ui-alert-title` and an optional dismiss button.
\
```html
Heads up
A new model version is available.
Saved
Your changes are live.
×
Approaching your Buzz limit.
Generation failed
The provider rejected the request.
```
\
```tsx
import { Alert, Stack } from '@civitai/components-react';
A new model version is available.
{}}>
Your changes are live.
Approaching your Buzz limit.
The provider rejected the request.
;
```
## TextInput
Labeled ` `. The wrapper carries `data-civitai-ui="text-input"`; the label
is wired via `for`/`id`, help text via `aria-describedby`, and the invalid state
via `aria-invalid="true"` + `data-invalid="true"` on the wrapper.
\
```html
```
\
```tsx
import { TextInput, Stack } from '@civitai/components-react';
;
```
## Textarea
Identical to TextInput, but the control is a resizable `