{
  "openapi": "3.1.0",
  "info": {
    "title": "SteadyShot API",
    "description": "Generate consistent character illustrations and scenes. Create characters, generate reference sheets, produce scene images, multi-character scenes, and full storybooks — all with built-in consistency checking.",
    "version": "1.0.0",
    "contact": {
      "name": "SteadyShot",
      "url": "https://steadyshot.ai"
    }
  },
  "servers": [
    {
      "url": "https://steadyshot.ai",
      "description": "Production"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "paths": {
    "/api/characters": {
      "get": {
        "operationId": "listCharacters",
        "summary": "List all characters",
        "description": "Returns all characters belonging to the authenticated user, ordered by creation date (newest first).",
        "responses": {
          "200": {
            "description": "Array of characters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/Character" }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createCharacter",
        "summary": "Create a new character",
        "description": "Create a character with a name, description, and optional source image. If an image is provided, it will be analyzed for multiple people (returns a warning if detected).",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["name", "description"],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Character name"
                  },
                  "description": {
                    "type": "string",
                    "description": "Detailed character description (appearance, clothing, features)"
                  },
                  "style_notes": {
                    "type": "string",
                    "description": "Art style notes, e.g. 'watercolor', 'Pixar 3D', 'anime'"
                  },
                  "image": {
                    "type": "string",
                    "format": "binary",
                    "description": "Optional source/reference image of the character"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Character created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Character" },
                    {
                      "type": "object",
                      "properties": {
                        "warning": {
                          "type": "string",
                          "description": "Warning if multiple people detected in the source image"
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/characters/{id}": {
      "get": {
        "operationId": "getCharacter",
        "summary": "Get character details",
        "description": "Returns a character's details including all reference images.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Character UUID"
          }
        ],
        "responses": {
          "200": {
            "description": "Character with references",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Character" },
                    {
                      "type": "object",
                      "properties": {
                        "references": {
                          "type": "array",
                          "items": { "$ref": "#/components/schemas/ReferenceImage" }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Character not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteCharacter",
        "summary": "Delete a character",
        "description": "Permanently deletes a character and all its data (reference images, generated images, storage files).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Character UUID"
          }
        ],
        "responses": {
          "200": {
            "description": "Character deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": { "type": "boolean" },
                    "id": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/brand-kits": {
      "post": {
        "operationId": "createBrandKit",
        "summary": "Create a brand kit",
        "description": "Create a new brand kit. Supply name, optional logo, optional palette (JSON array of {name, hex}), and optional brand_voice text. Returns the created brand kit.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string" },
                  "logo": { "type": "string", "format": "binary", "description": "Logo image (PNG with transparency preferred)" },
                  "palette": { "type": "string", "description": "JSON-encoded array of {name, hex}" },
                  "brand_voice": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Brand kit created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BrandKit" } } }
          }
        }
      },
      "get": {
        "operationId": "listBrandKits",
        "summary": "List brand kits",
        "responses": {
          "200": {
            "description": "Array of brand kits",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/BrandKit" } }
              }
            }
          }
        }
      }
    },
    "/api/brand-kits/{id}": {
      "get": {
        "operationId": "getBrandKit",
        "summary": "Get brand kit with assets",
        "parameters": [
          { "in": "path", "name": "id", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Brand kit with embedded assets",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BrandKitWithAssets" } } }
          }
        }
      },
      "patch": {
        "operationId": "updateBrandKit",
        "summary": "Update brand kit metadata",
        "parameters": [
          { "in": "path", "name": "id", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "brand_voice": { "type": "string", "nullable": true },
                  "palette": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": { "name": { "type": "string" }, "hex": { "type": "string" } }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated brand kit" }
        }
      },
      "delete": {
        "operationId": "deleteBrandKit",
        "summary": "Delete a brand kit",
        "parameters": [
          { "in": "path", "name": "id", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Brand kit deleted" }
        }
      }
    },
    "/api/brand-kits/{id}/assets": {
      "post": {
        "operationId": "uploadBrandAsset",
        "summary": "Upload a brand asset (logo variant, product, packaging)",
        "parameters": [
          { "in": "path", "name": "id", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["kind", "label", "image"],
                "properties": {
                  "kind": { "type": "string", "enum": ["logo", "product", "packaging", "reference"] },
                  "label": { "type": "string", "description": "Short human label, e.g. 'hero-product-front'" },
                  "image": { "type": "string", "format": "binary" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Asset uploaded" }
        }
      },
      "delete": {
        "operationId": "deleteBrandAsset",
        "summary": "Delete a brand asset",
        "parameters": [
          { "in": "path", "name": "id", "required": true, "schema": { "type": "string" } },
          { "in": "query", "name": "asset_id", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Asset deleted" }
        }
      }
    },
    "/api/generate": {
      "post": {
        "operationId": "generateScene",
        "summary": "Generate a scene image",
        "description": "Generate a scene image featuring a character. Uses multiple reference images for consistency. Includes automatic retry with consistency checking and scene accuracy validation. Costs 1 credit.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["prompt"],
                "properties": {
                  "character_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Character UUID. Required unless brand_kit_id is provided."
                  },
                  "brand_kit_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Brand kit UUID. When set (alone or with character_id), the brand kit's logo, palette, and product references are locked into the generation. Required unless character_id is provided."
                  },
                  "brand_asset_ids": {
                    "type": "array",
                    "items": { "type": "string", "format": "uuid" },
                    "maxItems": 4,
                    "description": "Optional subset of brand_asset IDs to lock into the scene as visual references. If omitted, the backend auto-picks (logo + products + packaging, capped at 4)."
                  },
                  "previous_image_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Optional id of a previously generated image for the SAME character_id (returned as `image_id` from a prior /api/generate call). When set, the prior image is threaded as the highest-priority visual reference, anchoring the new scene against the prior generation rather than only the canonical reference sheet. Use this to chain scenes in storybook flows — cross-page outfit/lighting drift drops significantly. Must belong to the auth user and the same character_id (400 otherwise)."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "Scene description, e.g. 'reading a book under a cherry blossom tree'"
                  },
                  "art_style": {
                    "type": "string",
                    "description": "Optional art style override, e.g. 'Pixar 3D', 'anime', 'watercolor'"
                  },
                  "max_retries": {
                    "type": "integer",
                    "default": 3,
                    "description": "Maximum generation attempts (1-5)"
                  },
                  "min_score": {
                    "type": "number",
                    "default": 0.95,
                    "description": "Minimum combined consistency score to accept (0-1)"
                  },
                  "no_cache": {
                    "type": "boolean",
                    "default": false,
                    "description": "Force regeneration even if a cached result exists"
                  }
                },
                "description": "Note: the request body may also include reserved internal-only fields (prefixed with `_`) which are set by other SteadyShot endpoints when they call /api/generate internally. These are not part of the public API and should not be set by external clients — their shape may change without notice."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated scene image",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GenerateResult" }
              }
            }
          },
          "400": {
            "description": "Missing parameters or no reference images",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "402": {
            "description": "Insufficient credits",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "Character not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/scenes/{image_id}/vary": {
      "post": {
        "operationId": "varyScene",
        "summary": "Generate single-attribute variations of an existing scene",
        "description": "Take an existing generated scene as the source and produce N near-clones that differ in ONE specified dimension (expression, outfit, lighting, angle, pose, or background) while preserving everything else. Internally fans out N parallel /api/generate calls anchored on the source image. Costs 1 credit per generated variant (count). Only supported for character-anchored scenes (brand-only generations have no chain anchor).",
        "parameters": [
          {
            "name": "image_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "UUID of the source generated_image to vary"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["vary", "target"],
                "properties": {
                  "vary": {
                    "type": "string",
                    "enum": ["expression", "outfit", "lighting", "angle", "pose", "background"],
                    "description": "The single dimension to change. Everything else is held constant."
                  },
                  "target": {
                    "type": "string",
                    "description": "The new value for the chosen dimension, e.g. 'frowning' for expression or 'golden hour' for lighting."
                  },
                  "count": {
                    "type": "integer",
                    "default": 4,
                    "minimum": 1,
                    "maximum": 4,
                    "description": "How many variations to generate (1-4)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Variations generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source_image_id": { "type": "string", "format": "uuid" },
                    "vary": { "type": "string" },
                    "target": { "type": "string" },
                    "requested": { "type": "integer" },
                    "generated": { "type": "integer" },
                    "failed": { "type": "integer" },
                    "variants": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "image_id": { "type": "string", "format": "uuid" },
                          "image_url": { "type": "string" },
                          "consistency_score": { "type": "integer" }
                        }
                      }
                    },
                    "failures": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "status": { "type": "integer" },
                          "error": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid dimension, missing target, or source is brand-only",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "Source scene not found or not owned by caller",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/generate-multi": {
      "post": {
        "operationId": "generateMultiCharacterScene",
        "summary": "Generate a multi-character scene",
        "description": "Generate a scene image featuring 2-4 characters together. Includes character count verification and per-character consistency checking. Costs 2 credits.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["character_ids", "prompt"],
                "properties": {
                  "character_ids": {
                    "type": "array",
                    "items": { "type": "string", "format": "uuid" },
                    "minItems": 2,
                    "maxItems": 4,
                    "description": "Array of 2-4 character UUIDs"
                  },
                  "prompt": {
                    "type": "string",
                    "description": "Scene description, e.g. 'Milo and Aria playing chess in a garden'"
                  },
                  "art_style": {
                    "type": "string",
                    "description": "Optional art style override"
                  },
                  "max_retries": {
                    "type": "integer",
                    "default": 3
                  },
                  "min_score": {
                    "type": "number",
                    "default": 0.90
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated multi-character scene",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MultiGenerateResult" }
              }
            }
          },
          "400": {
            "description": "Invalid parameters",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "402": {
            "description": "Insufficient credits",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/storybook": {
      "post": {
        "operationId": "generateStorybook",
        "summary": "Generate a storybook",
        "description": "Generate a full storybook with multiple pages. Supports single or multiple characters, optional consistency checking per page, style continuity between pages, and PDF export. Costs 1 credit per page.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["pages"],
                "properties": {
                  "character_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Single character UUID (use this OR character_ids)"
                  },
                  "character_ids": {
                    "type": "array",
                    "items": { "type": "string", "format": "uuid" },
                    "maxItems": 4,
                    "description": "Array of 2-4 character UUIDs for multi-character storybook"
                  },
                  "pages": {
                    "type": "array",
                    "items": { "type": "string" },
                    "maxItems": 20,
                    "description": "Array of scene descriptions, one per page"
                  },
                  "art_style": {
                    "type": "string",
                    "description": "Art style override for the entire storybook"
                  },
                  "consistency_check": {
                    "type": "boolean",
                    "default": false,
                    "description": "Check consistency on each page and retry low-scoring ones"
                  },
                  "export_pdf": {
                    "type": "boolean",
                    "default": false,
                    "description": "Assemble all pages into a PDF and return the download URL"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated storybook",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/StorybookResult" }
              }
            }
          }
        }
      }
    },
    "/api/comics": {
      "get": {
        "operationId": "listComics",
        "summary": "List your comics",
        "description": "Returns the caller's comics newest-first, with the page-1 thumbnail attached for list rendering.",
        "responses": {
          "200": {
            "description": "List of comics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comics": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Comic" }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createComic",
        "summary": "Create an empty comic shell",
        "description": "Spec §4 primitive. Creates a comic project with cast + style + optional setting and outfit locks. No panels are generated — call POST /api/comics/{id}/panels to add panels one at a time, or use POST /api/comics/generate for one-shot plan+generate.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title", "character_ids"],
                "properties": {
                  "title": { "type": "string", "maxLength": 200 },
                  "character_ids": {
                    "type": "array",
                    "items": { "type": "string", "format": "uuid" },
                    "minItems": 1,
                    "maxItems": 3,
                    "description": "Locked SteadyShot character IDs that may appear in panels"
                  },
                  "style_preset": { "$ref": "#/components/schemas/ComicStyleKey" },
                  "story_prompt": { "type": "string", "maxLength": 2000 },
                  "setting": {
                    "type": "string",
                    "description": "Single shared location text injected into every panel prompt"
                  },
                  "outfit_locks": {
                    "type": "array",
                    "items": { "$ref": "#/components/schemas/OutfitLock" }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created comic",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Comic" }
              }
            }
          }
        }
      }
    },
    "/api/comics/{id}": {
      "get": {
        "operationId": "getComic",
        "summary": "Fetch a comic + its pages",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Comic + ordered pages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comic": { "$ref": "#/components/schemas/Comic" },
                    "pages": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/ComicPanel" }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteComic",
        "summary": "Delete a comic (cascades to pages)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "deleted": { "type": "boolean" } }
                }
              }
            }
          }
        }
      }
    },
    "/api/comics/{id}/panels": {
      "post": {
        "operationId": "generateComicPanel",
        "summary": "Generate one comic panel",
        "description": "Spec §4 primitive. Generates a single panel image using the comic's locked setting + outfit locks + style. The bubble overlay is derived from caption/caption_type/speaker. Costs 1 credit; refunded on failure.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["page_number", "action"],
                "properties": {
                  "page_number": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 12,
                    "description": "1-indexed slot. If a panel already exists at this slot it is regenerated; otherwise inserted."
                  },
                  "action": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "What HAPPENS in this panel — poses, motion, expressions. Do not re-describe setting or outfits (locked on the comic)."
                  },
                  "shot": { "$ref": "#/components/schemas/ComicShotKey" },
                  "caption": {
                    "type": "string",
                    "description": "Text rendered in the overlay bubble. Hard limit ~10 words for legibility."
                  },
                  "caption_type": {
                    "type": "string",
                    "enum": ["narration", "dialogue"],
                    "default": "narration"
                  },
                  "speaker": {
                    "type": "string",
                    "description": "Character name; required when caption_type='dialogue'"
                  },
                  "continue_from": {
                    "type": "integer",
                    "description": "page_number of a prior panel to use as a style continuity anchor"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated panel",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "panel": { "$ref": "#/components/schemas/ComicPanel" },
                    "image_url": { "type": "string", "format": "uri" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/comics/{id}/render": {
      "post": {
        "operationId": "renderComic",
        "summary": "Assemble a comic for compositing",
        "description": "Spec §4 primitive. Returns the comic + its panels (with bubble specs + shot + image URLs) so the caller can composite layout client-side. A future iteration may return a server-rendered PNG/PDF.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "page_number": {
                    "type": "integer",
                    "description": "Render just one page; omit for all"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Assembled comic",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comic": { "$ref": "#/components/schemas/Comic" },
                    "pages": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/ComicPanel" }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/comics/generate": {
      "post": {
        "operationId": "generateComic",
        "summary": "One-shot: plan + create + generate a full comic",
        "description": "Convenience macro that wraps the spec-aligned primitives. Plans the setting + outfit locks + N panels (caption, action, shot) from a story idea using Claude, then runs panel generation in the background. Returns immediately with the comic id; the caller polls GET /api/comics/{id} to watch panels fill in. Costs 1 credit per panel; refunded per failed panel.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title", "story_prompt", "character_ids"],
                "properties": {
                  "title": { "type": "string", "maxLength": 200 },
                  "story_prompt": { "type": "string", "maxLength": 2000 },
                  "character_ids": {
                    "type": "array",
                    "items": { "type": "string", "format": "uuid" },
                    "minItems": 1,
                    "maxItems": 3
                  },
                  "page_count": {
                    "type": "integer",
                    "minimum": 3,
                    "maximum": 8,
                    "default": 5
                  },
                  "style_preset": { "$ref": "#/components/schemas/ComicStyleKey" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comic created; panels generating in the background",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid" },
                    "status": { "type": "string", "enum": ["generating"] },
                    "page_count": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/library": {
      "get": {
        "operationId": "listLibraryCharacters",
        "summary": "Browse the character library",
        "description": "Browse pre-built characters available for import. No auth required.",
        "security": [],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Search by name or appearance"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 100, "maximum": 200 },
            "description": "Max results to return"
          }
        ],
        "responses": {
          "200": {
            "description": "Library characters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "characters": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/LibraryCharacter" }
                    },
                    "total": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/library/import": {
      "post": {
        "operationId": "importLibraryCharacter",
        "summary": "Import a library character",
        "description": "Import a pre-built character from the library into your account. Creates a copy with reference images ready for scene generation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["library_character_id"],
                "properties": {
                  "library_character_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The library character's UUID"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Character imported",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "character_id": { "type": "string", "format": "uuid" },
                    "name": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/credits": {
      "get": {
        "operationId": "getCredits",
        "summary": "Get credit balance",
        "description": "Returns the authenticated user's current credit balance and plan.",
        "responses": {
          "200": {
            "description": "Credit balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "credits": { "type": "integer" },
                    "plan": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/api-keys": {
      "get": {
        "operationId": "listApiKeys",
        "summary": "List your API keys",
        "description": "Returns all API keys for the authenticated user (key values are not returned, only prefixes).",
        "responses": {
          "200": {
            "description": "API keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/ApiKey" }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createApiKey",
        "summary": "Create a new API key",
        "description": "Generate a new API key. The full key is only returned once — save it immediately. Maximum 5 active keys per user.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "default": "Default",
                    "description": "A label for the key, e.g. 'ChatGPT' or 'Discord Bot'"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "API key created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/ApiKey" },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "description": "The full API key (only shown once)"
                        },
                        "warning": { "type": "string" }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "revokeApiKey",
        "summary": "Revoke an API key",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["key_id"],
                "properties": {
                  "key_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The key's UUID to revoke"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": { "type": "boolean" }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from https://steadyshot.ai — prefix: ss_live_"
      }
    },
    "schemas": {
      "ComicStyleKey": {
        "type": "string",
        "enum": [
          "classic_american",
          "manga_bw",
          "european_tintin",
          "indie_webcomic",
          "newspaper_sunday"
        ],
        "default": "classic_american",
        "description": "Locked visual language for the whole comic. Stays consistent across every panel."
      },
      "ComicShotKey": {
        "type": "string",
        "enum": [
          "establishing",
          "wide",
          "two_shot",
          "medium",
          "closeup",
          "reaction_closeup",
          "over_shoulder",
          "splash"
        ],
        "default": "wide",
        "description": "Camera framing for a single panel."
      },
      "OutfitLock": {
        "type": "object",
        "required": ["character_name", "outfit"],
        "properties": {
          "character_name": { "type": "string" },
          "outfit": {
            "type": "string",
            "description": "Detailed outfit description injected verbatim into every panel prompt the character appears in. Prevents clothing drift across panels."
          }
        }
      },
      "ComicBubble": {
        "type": "object",
        "required": ["type", "text", "anchor"],
        "properties": {
          "type": {
            "type": "string",
            "enum": ["speech", "narration", "thought", "sfx"]
          },
          "text": { "type": "string" },
          "speaker_name": { "type": "string" },
          "anchor": {
            "type": "object",
            "required": ["x", "y"],
            "properties": {
              "x": { "type": "number", "minimum": 0, "maximum": 100 },
              "y": { "type": "number", "minimum": 0, "maximum": 100 }
            },
            "description": "Panel-relative percentages so the bubble layout survives any rendered image size."
          }
        }
      },
      "ComicPanel": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "comic_id": { "type": "string", "format": "uuid" },
          "page_number": { "type": "integer" },
          "image_url": { "type": "string", "format": "uri", "nullable": true },
          "caption": { "type": "string" },
          "scene_prompt": { "type": "string" },
          "shot": { "$ref": "#/components/schemas/ComicShotKey" },
          "bubbles": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ComicBubble" }
          },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "Comic": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "title": { "type": "string" },
          "story_prompt": { "type": "string" },
          "character_ids": {
            "type": "array",
            "items": { "type": "string", "format": "uuid" }
          },
          "status": {
            "type": "string",
            "enum": ["generating", "ready", "failed"]
          },
          "page_count": { "type": "integer" },
          "setting": { "type": "string" },
          "outfit_locks": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/OutfitLock" }
          },
          "style_preset": { "$ref": "#/components/schemas/ComicStyleKey" },
          "style_lock": { "type": "string" },
          "setting_anchor_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Internal establishing-shot reference. Used as a style+setting anchor for every panel."
          },
          "created_at": { "type": "string", "format": "date-time" },
          "cover_image_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Page-1 image_url; populated by list endpoint for thumbnails."
          }
        }
      },
      "Character": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "style_notes": { "type": "string", "nullable": true },
          "source_image_url": { "type": "string", "nullable": true },
          "face_mode": { "type": "string", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "ReferenceImage": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "character_id": { "type": "string", "format": "uuid" },
          "label": { "type": "string", "description": "e.g. 'front', 'left_side', 'happy'" },
          "image_url": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "LibraryCharacter": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "appearance_summary": { "type": "string" },
          "clothing_description": { "type": "string", "nullable": true },
          "personality_traits": { "type": "array", "items": { "type": "string" } },
          "reference_image_url": { "type": "string", "nullable": true },
          "usage_count": { "type": "integer" }
        }
      },
      "GenerateResult": {
        "type": "object",
        "properties": {
          "image_id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID of the new generated_images row. Pass this back as `previous_image_id` on the next /api/generate call to chain scenes for cross-page coherence."
          },
          "image_url": { "type": "string", "description": "URL of the generated image" },
          "consistency_score": { "type": "integer", "description": "0-100 combined score" },
          "character_score": { "type": "integer", "description": "0-100, present when character_id was supplied" },
          "brand_score": { "type": "integer", "description": "0-100, present when brand_kit_id was supplied" },
          "attempts": { "type": "integer" },
          "passed": { "type": "boolean", "description": "Whether the score met the minimum threshold" },
          "reasoning": { "type": "string" },
          "issues": { "type": "array", "items": { "type": "string" } },
          "cached": { "type": "boolean", "description": "True if returned from cache" }
        }
      },
      "PaletteColor": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "hex": { "type": "string", "example": "#0055FF" }
        }
      },
      "BrandKit": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "logo_image_url": { "type": "string", "nullable": true },
          "palette": { "type": "array", "items": { "$ref": "#/components/schemas/PaletteColor" } },
          "brand_voice": { "type": "string", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "BrandAsset": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "brand_kit_id": { "type": "string", "format": "uuid" },
          "image_url": { "type": "string" },
          "kind": { "type": "string", "enum": ["logo", "product", "packaging", "reference"] },
          "label": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "BrandKitWithAssets": {
        "allOf": [
          { "$ref": "#/components/schemas/BrandKit" },
          {
            "type": "object",
            "properties": {
              "assets": { "type": "array", "items": { "$ref": "#/components/schemas/BrandAsset" } }
            }
          }
        ]
      },
      "MultiGenerateResult": {
        "type": "object",
        "properties": {
          "image_url": { "type": "string" },
          "characters": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "name": { "type": "string" },
                "score": { "type": "integer" }
              }
            }
          },
          "average_score": { "type": "integer" },
          "attempts": { "type": "integer" },
          "passed": { "type": "boolean" },
          "reasoning": { "type": "string" },
          "issues": { "type": "array", "items": { "type": "string" } },
          "mode": { "type": "string", "enum": ["illustrated", "pulid"] }
        }
      },
      "StorybookResult": {
        "type": "object",
        "properties": {
          "character_ids": { "type": "array", "items": { "type": "string" } },
          "character_names": { "type": "array", "items": { "type": "string" } },
          "total_pages": { "type": "integer" },
          "generated": { "type": "integer" },
          "failed": { "type": "integer" },
          "pages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "page": { "type": "integer" },
                "prompt": { "type": "string" },
                "image_url": { "type": "string", "nullable": true },
                "consistency_score": { "type": "integer" },
                "error": { "type": "string" }
              }
            }
          },
          "pdf_url": { "type": "string", "description": "PDF download URL (if export_pdf was true)" }
        }
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "key_prefix": { "type": "string", "description": "First 12 chars of the key" },
          "created_at": { "type": "string", "format": "date-time" },
          "last_used_at": { "type": "string", "format": "date-time", "nullable": true },
          "revoked_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" }
        }
      }
    }
  }
}
