{
  "openapi": "3.1.0",
  "info": {
    "version": "1.0.0",
    "title": "@flam/api",
    "description": "FLAM — the fashion house for creative AI. Session-cookie auth unless a route says otherwise."
  },
  "components": {
    "schemas": {
      "BrandProfile": {
        "type": "object",
        "description": "The house's visual DNA, read off its own imagery. `clause` is exactly what the generators inject — built by the same builder the worker calls.\n",
        "properties": {
          "visionEn": {
            "type": "string"
          },
          "vibe": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "palette": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "signatureKeywords": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "category": {
                  "type": "string"
                },
                "phrase": {
                  "type": "string"
                }
              }
            }
          },
          "injectEnabled": {
            "type": "boolean"
          },
          "dnaRuns": {
            "type": "integer"
          },
          "clause": {
            "type": "string",
            "description": "Empty when injectEnabled is false."
          }
        }
      },
      "Folder": {
        "type": "object",
        "description": "A collection. Flat on the wire — the client builds the tree from parentId.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "parentId": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SearchResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "piece",
              "collection",
              "look",
              "workflow"
            ]
          },
          "title": {
            "type": "string"
          },
          "meta": {
            "type": "string",
            "nullable": true,
            "description": "The lane's caption — a SKU, a piece count, a status. Never invented."
          },
          "thumbUrl": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "DevelopRun": {
        "type": "object",
        "description": "A run is DERIVED from what generation_jobs already carries — there is no run_id column.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "run.<runId|oldestJobId> or look.<lookId>.<stage>"
          },
          "kind": {
            "type": "string",
            "enum": [
              "lookbook",
              "batch",
              "single"
            ]
          },
          "tool": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "stage": {
            "type": "string",
            "nullable": true,
            "enum": [
              "lock",
              "variation"
            ]
          },
          "lookId": {
            "type": "string",
            "nullable": true
          },
          "state": {
            "type": "string",
            "enum": [
              "queued",
              "developing",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "counts": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "done": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "cancelled": {
                "type": "integer"
              },
              "active": {
                "type": "integer"
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "finishedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "thumbAssetId": {
            "type": "string",
            "nullable": true
          },
          "jobIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "againCost": {
            "type": "integer",
            "nullable": true,
            "description": "Tokens to develop this whole run again. NULL when the price is unknown — the UI then does not offer the verb at all, because the house never puts a spend behind a button without its price.\n"
          }
        }
      },
      "Look": {
        "type": "object",
        "description": "The lookbook_looks row as stored — jsonb columns come back as-is.",
        "properties": {
          "id": {
            "type": "string"
          },
          "organizationId": {
            "type": "string"
          },
          "createdByUserId": {
            "type": "string",
            "nullable": true
          },
          "brandId": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "modelAssetId": {
            "type": "string"
          },
          "backgroundAssetId": {
            "type": "string",
            "nullable": true
          },
          "itemAssetIds": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "assetId": {
                  "type": "string"
                },
                "role": {
                  "type": "string"
                }
              }
            }
          },
          "preset": {
            "type": "object",
            "properties": {
              "aesthetic": {
                "type": "string"
              },
              "background": {
                "type": "object"
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "developing",
              "locked",
              "complete"
            ]
          },
          "lookBaseAssetId": {
            "type": "string",
            "nullable": true
          },
          "lookBaseJobId": {
            "type": "string",
            "nullable": true
          },
          "variations": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Model": {
        "type": "object",
        "description": "A face on the casting board.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "sale",
              "exclusive"
            ]
          },
          "priceCents": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "coverUrl": {
            "type": "string",
            "nullable": true
          },
          "previewUrls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "owned": {
            "type": "boolean",
            "description": "Granted to this house",
            "or its own": null
          },
          "mine": {
            "type": "boolean",
            "description": "This house registered it"
          },
          "usable": {
            "type": "boolean",
            "description": "free OR owned — what actually gates casting"
          },
          "exclusiveYours": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PipelineRun": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "developId": {
            "type": "string",
            "description": "The Darkroom's id for the same run"
          },
          "status": {
            "type": "string",
            "enum": [
              "developing",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "framesPlanned": {
            "type": "integer"
          },
          "framesSkipped": {
            "type": "integer"
          },
          "tokensPlanned": {
            "type": "integer"
          },
          "tokensSpent": {
            "type": "integer",
            "description": "SETTLED tokens",
            "never an estimate": null
          },
          "steps": {
            "type": "object",
            "properties": {
              "done": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "cancelled": {
                "type": "integer"
              },
              "active": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              }
            }
          },
          "line": {
            "type": "string",
            "description": "The honest sentence the screen shows"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PipelinePreset": {
        "type": "object",
        "description": "A plan with no items bound.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "nodes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "sinkFolderId": {
            "type": "string",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "HousePreset": {
        "type": "object",
        "description": "The house's standing choices. Always answers — a director who never opened brand settings still gets a working one.\n",
        "properties": {
          "modelAssetId": {
            "type": "string",
            "nullable": true
          },
          "background": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "preset",
                  "asset"
                ]
              },
              "value": {
                "type": "string"
              },
              "assetId": {
                "type": "string"
              }
            }
          },
          "aesthetic": {
            "type": "string"
          },
          "variationKeys": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "quality": {
            "type": "string",
            "enum": [
              "1k",
              "2k",
              "4k"
            ]
          }
        }
      },
      "DevelopRequest": {
        "type": "object",
        "description": "What a develop is told. Every field optional unless noted.",
        "properties": {
          "quality": {
            "type": "string",
            "enum": [
              "1k",
              "2k",
              "4k"
            ],
            "default": "2k",
            "description": "The tier the frame develops at — 1,024 / 2,048 / 4,096 px on the long edge."
          },
          "output": {
            "$ref": "#/components/schemas/OutputDestination"
          },
          "idempotencyKey": {
            "type": "string",
            "description": "Makes one gesture safe to retry. Generated when omitted."
          }
        }
      },
      "OutputDestination": {
        "type": "object",
        "description": "WHERE the frame is going. Omit and the frame simply develops at the tier. A named place carries its own exact pixels; \"custom\" carries yours. The frame is COMPOSED for that shape and developed at the tier's pixels — the response is not resampled to width × height, so cut it yourself (a pure downscale for every place except the two A-series print sizes, which are composed at 2:3 and cost a 5.7% trim). Never upscale a frame that came back smaller than the place asked for: that is invented detail, and the house sells it only as a priced restoration pass on the 4K tier.\n",
        "required": [
          "destination"
        ],
        "properties": {
          "destination": {
            "type": "string",
            "enum": [
              "web-tile",
              "web-hero",
              "shopify-product",
              "ecom-packshot",
              "marketplace-listing",
              "ig-square",
              "ig-portrait",
              "ig-story",
              "pinterest-pin",
              "print-page",
              "print-spread",
              "custom"
            ]
          },
          "width": {
            "type": "integer",
            "minimum": 64,
            "maximum": 8192,
            "description": "Required with \"custom\", ignored otherwise."
          },
          "height": {
            "type": "integer",
            "minimum": 64,
            "maximum": 8192,
            "description": "Required with \"custom\", ignored otherwise."
          }
        }
      },
      "OutputError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "detail": {
            "type": "string",
            "description": "The refusal in plain language, in pixels the caller can check."
          }
        }
      },
      "Job": {
        "type": "object",
        "description": "The client-facing job shape (WS push + reconnect backfill).",
        "properties": {
          "id": {
            "type": "string"
          },
          "tool": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed",
              "canceled"
            ]
          },
          "resultUrl": {
            "type": "string",
            "nullable": true
          },
          "resultAssetId": {
            "type": "string",
            "nullable": true
          },
          "error": {
            "type": "string",
            "nullable": true
          },
          "brandId": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "eventSeq": {
            "type": "integer"
          }
        }
      },
      "Asset": {
        "type": "object",
        "description": "One piece in the house library. `sku`, `kind` and `groupId` are projected out of the row's `tags` array (`sku:` / `kind:` / `group:`), which is where they are stored — there are no columns for them.\n",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "The filename",
            "or the label a director gave it": null
          },
          "sku": {
            "type": "string",
            "nullable": true,
            "description": "The brand's own code — never invented"
          },
          "kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "clothes",
              "accessory",
              "jewelry",
              "model",
              "background"
            ]
          },
          "url": {
            "type": "string",
            "description": "Owner-gated streaming route (/api/toolkit/assets/{id}/file)"
          },
          "folderId": {
            "type": "string",
            "nullable": true
          },
          "groupId": {
            "type": "string",
            "nullable": true,
            "description": "Shared by pieces grouped together"
          },
          "mime": {
            "type": "string"
          },
          "tool": {
            "type": "string",
            "nullable": true,
            "description": "The tool that produced it",
            "if any": null
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ToolItem": {
        "type": "object",
        "description": "Snake_case on the wire — matches the raw-SQL rows + the client ToolItem type.",
        "properties": {
          "id": {
            "type": "string"
          },
          "tool_id": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "title_he": {
            "type": "string",
            "nullable": true
          },
          "label_he": {
            "type": "string",
            "nullable": true
          },
          "content_en": {
            "type": "string",
            "nullable": true
          },
          "image_url": {
            "type": "string",
            "nullable": true
          },
          "notes_he": {
            "type": "string",
            "nullable": true
          },
          "sort_order": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OnboardingProgress": {
        "type": "object",
        "properties": {
          "steps": {
            "type": "object",
            "description": "step id → ISO completion timestamp or null",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            }
          },
          "completed": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "reward": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "granted": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "Account": {
        "type": "object",
        "description": "Everything the settings room needs that nothing else can answer. Money is stated only where we recorded it — Dodo is the merchant of record and issues the tax document, so `reference` is what a director quotes.\n",
        "properties": {
          "profile": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "emailVerified": {
                "type": "boolean"
              },
              "image": {
                "type": "string",
                "nullable": true
              },
              "joinedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "plan": {
            "type": "object",
            "nullable": true,
            "description": "null = no plan. `status` 'canceled' means paid to `renewsAt`, then it stops.",
            "properties": {
              "tier": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "trialing",
                  "active",
                  "past_due",
                  "canceled",
                  "revoked"
                ]
              },
              "priceUsd": {
                "type": "number"
              },
              "monthlyTokens": {
                "type": "integer"
              },
              "rolloverMultiple": {
                "type": "integer"
              },
              "renewsAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "tokens": {
            "type": "object",
            "properties": {
              "balance": {
                "type": "integer",
                "description": "Spendable — derived, never stored"
              },
              "grant": {
                "type": "integer"
              },
              "topup": {
                "type": "integer"
              },
              "held": {
                "type": "integer",
                "description": "Reserved by develops in flight"
              },
              "days": {
                "type": "integer",
                "enum": [
                  7,
                  30,
                  90
                ]
              },
              "byWorkflow": {
                "type": "array",
                "description": "Where the tokens actually went, heaviest first.",
                "items": {
                  "type": "object",
                  "properties": {
                    "workflow": {
                      "type": "string"
                    },
                    "tokens": {
                      "type": "integer"
                    },
                    "runs": {
                      "type": "integer"
                    },
                    "lastAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "invoices": {
            "type": "array",
            "description": "Money that landed — newest first, capped at 100.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "at": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "subscription_grant",
                    "topup",
                    "promo",
                    "refund",
                    "purchase"
                  ]
                },
                "item": {
                  "type": "string"
                },
                "tokens": {
                  "type": "integer",
                  "nullable": true
                },
                "amountUsd": {
                  "type": "number",
                  "nullable": true,
                  "description": "Null where no amount was recorded — never an invented one."
                },
                "reference": {
                  "type": "string",
                  "nullable": true
                }
              }
            }
          },
          "billing": {
            "type": "object",
            "properties": {
              "cardOnFile": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "Post": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "p_abc123"
          },
          "title": {
            "type": "string",
            "example": "Hello world"
          },
          "body": {
            "type": "string",
            "example": "My first post."
          },
          "authorId": {
            "type": "string",
            "example": "u_abc123"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-01T12:00:00.000Z"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-07-01T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "title",
          "body",
          "authorId",
          "createdAt",
          "updatedAt"
        ]
      },
      "NewPost": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100000
          }
        },
        "required": [
          "title",
          "body"
        ]
      },
      "PatchPost": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100000
          }
        }
      },
      "InviteRequestResult": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "received",
              "already_requested"
            ]
          }
        },
        "required": [
          "status"
        ]
      },
      "InviteRequestError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "InviteRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 80,
            "example": "Noa Berger"
          },
          "house": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "example": "Atelier Berger"
          },
          "email": {
            "type": "string",
            "maxLength": 160,
            "format": "email",
            "example": "noa@atelierberger.com"
          },
          "collection": {
            "type": "string",
            "minLength": 10,
            "maxLength": 400,
            "example": "Resort 26 — 40 pieces of knitwear, shooting the week of the 12th."
          },
          "website": {
            "type": "string",
            "maxLength": 200
          }
        },
        "required": [
          "name",
          "house",
          "email",
          "collection"
        ]
      },
      "InviteRequestRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "house": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "collection": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "house",
          "email",
          "collection",
          "status",
          "createdAt"
        ]
      },
      "InviteIssueResult": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "token": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "issued",
              "already_invited",
              "already_accepted"
            ]
          },
          "toolkitsOpened": {
            "type": "number"
          }
        },
        "required": [
          "email",
          "token",
          "url",
          "status",
          "toolkitsOpened"
        ]
      },
      "InviteIssue": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 160,
            "format": "email",
            "example": "noa@atelierberger.com"
          }
        },
        "required": [
          "email"
        ]
      },
      "SeatInviteResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ]
          },
          "organizationId": {
            "type": "string"
          },
          "organizationName": {
            "type": "string"
          },
          "expiresAt": {
            "type": "string"
          },
          "url": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "email",
          "role",
          "organizationId",
          "organizationName",
          "expiresAt",
          "url"
        ]
      },
      "SeatInvite": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 160,
            "format": "email",
            "example": "eli@maison.com"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ],
            "default": "member"
          }
        },
        "required": [
          "email"
        ]
      },
      "SeatAcceptResult": {
        "type": "object",
        "properties": {
          "organizationId": {
            "type": "string"
          },
          "organizationName": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ]
          }
        },
        "required": [
          "organizationId",
          "organizationName",
          "role"
        ]
      },
      "SeatAccept": {
        "type": "object",
        "properties": {
          "invitationId": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "invitationId"
        ]
      },
      "SeatRoleResult": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ]
          },
          "email": {
            "type": "string"
          }
        },
        "required": [
          "userId",
          "role",
          "email"
        ]
      },
      "HouseError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "SeatRole": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "minLength": 1,
            "example": "usr_eli"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member",
              "viewer"
            ],
            "example": "admin"
          }
        },
        "required": [
          "userId",
          "role"
        ]
      },
      "SeatRemoveResult": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "workStaysWithHouse": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "userId",
          "email",
          "workStaysWithHouse"
        ]
      },
      "HouseTransferResult": {
        "type": "object",
        "properties": {
          "organizationId": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "previousOwnerRole": {
            "type": "string",
            "enum": [
              "admin"
            ]
          }
        },
        "required": [
          "organizationId",
          "userId",
          "email",
          "previousOwnerRole"
        ]
      },
      "HouseTransfer": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "minLength": 1,
            "example": "usr_eli"
          }
        },
        "required": [
          "userId"
        ]
      },
      "McpResponse": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "enum": [
              "2.0"
            ]
          },
          "id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "nullable": true
              }
            ]
          },
          "result": {
            "nullable": true
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "number"
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "jsonrpc"
        ]
      },
      "McpRequest": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "enum": [
              "2.0"
            ]
          },
          "id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "nullable": true
              }
            ]
          },
          "method": {
            "type": "string"
          },
          "params": {
            "type": "object",
            "additionalProperties": {
              "nullable": true
            }
          }
        },
        "required": [
          "jsonrpc",
          "method"
        ]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "No valid session",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Session email not in the admin allowlist",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid input",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "parameters": {}
  },
  "paths": {
    "/openapi.json": {
      "get": {
        "summary": "The merged OpenAPI document this API serves",
        "description": "The zod-openapi-generated half plus the hand-maintained half compiled from this file (bun run gen:openapi). /docs renders it. A route missing here is invisible to every generated client, typed SDK and MCP tool list.\n",
        "tags": [
          "meta"
        ],
        "responses": {
          "200": {
            "description": "An OpenAPI 3.1 document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/my-access": {
      "get": {
        "tags": [
          "toolkit"
        ],
        "responses": {
          "200": {
            "description": "The active tool + toolkit registry, per-user access",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "toolkits": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": {
                          "nullable": true
                        }
                      }
                    },
                    "tools": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "sortOrder": {
                            "type": "number"
                          },
                          "accessible": {
                            "type": "boolean"
                          }
                        },
                        "required": [
                          "id",
                          "slug",
                          "name",
                          "sortOrder",
                          "accessible"
                        ],
                        "additionalProperties": {
                          "nullable": true
                        }
                      }
                    },
                    "trialExpired": {
                      "type": "boolean"
                    },
                    "trialActive": {
                      "type": "boolean"
                    },
                    "trialDaysLeft": {
                      "type": "number",
                      "nullable": true
                    }
                  },
                  "required": [
                    "toolkits",
                    "tools",
                    "trialExpired",
                    "trialActive",
                    "trialDaysLeft"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/tools/{slug}/items": {
      "get": {
        "summary": "Curated content items for a tool",
        "tags": [
          "toolkit"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "tool_items rows, ordered per category by sort_order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ToolItem"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/ai/remove-background": {
      "post": {
        "summary": "Remove background (sync, fal birefnet)",
        "tags": [
          "toolkit"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result image URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "NO_IMAGE | EMPTY_IMAGE | BAD_FORM"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE (12 MB cap) — body includes maxMb"
          },
          "502": {
            "description": "REMOVE_BG_FAILED"
          },
          "503": {
            "description": "FAL_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/prompt-enhancer": {
      "post": {
        "summary": "Improve a rough prompt (sync text lane)",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "prompt"
                ],
                "properties": {
                  "prompt": {
                    "type": "string"
                  },
                  "intent": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Improved prompt + notes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "improved": {
                      "type": "string"
                    },
                    "notes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "PROMPT_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/prompt-builder": {
      "post": {
        "summary": "Build one photographic prompt from a subject",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "subject"
                ],
                "properties": {
                  "subject": {
                    "type": "string"
                  },
                  "vibe": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The built prompt",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "prompt": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "SUBJECT_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/midjourney-prompts": {
      "post": {
        "summary": "Generate N varied shot prompts from a reference description",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "description"
                ],
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "direction": {
                    "type": "string"
                  },
                  "count": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Labeled shot prompts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "shots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string"
                          },
                          "prompt": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "DESCRIPTION_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/inspiration-keywords": {
      "post": {
        "summary": "Categorized keyword sets for a vibe",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "vibe"
                ],
                "properties": {
                  "vibe": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Keyword categories",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "category": {
                            "type": "string"
                          },
                          "keywords": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "VIBE_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/face-features": {
      "post": {
        "summary": "20 facial-feature keywords from a portrait (vision lane)",
        "tags": [
          "text-tools"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "imageUrl": {
                    "type": "string"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Keywords (empty + faceDetected=false when no face)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "faceDetected": {
                      "type": "boolean"
                    },
                    "keywords": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED | EMPTY_IMAGE | BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "AI_NEEDS_BILLING"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT | AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/jobs/submit": {
      "post": {
        "summary": "Submit an async generation job",
        "description": "Generic over tools and N named images: `tool` must be a known async slug; every file field stages as a named input (field name = role, e.g. try-on sends `model` + `garment`); other string fields become the tool's params. Reserves a token hold before enqueue; idempotent on (user, idempotencyKey). Progress is pushed over the jobs WebSocket.\n",
        "tags": [
          "jobs"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "tool"
                ],
                "properties": {
                  "tool": {
                    "type": "string",
                    "description": "One of ASYNC_TOOL_SLUGS (upscale, reframe, generate-character, packshot, camera-lens, harmonize, tryon, iron, cleanup, color-change, fit-adjust, lookbook-video)"
                  },
                  "idempotencyKey": {
                    "type": "string"
                  },
                  "brandId": {
                    "type": "string"
                  },
                  "image": {
                    "type": "string",
                    "format": "binary",
                    "description": "Single-image tools; multi-input tools use per-role field names instead"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job queued (or the existing job on an idempotent replay)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "UNKNOWN_TOOL | NO_IMAGE | EMPTY_IMAGE | BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS — body includes need + spendable"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE (12 MB per image)"
          },
          "500": {
            "description": "SUBMIT_FAILED"
          }
        }
      }
    },
    "/api/toolkit/jobs": {
      "get": {
        "summary": "Reconnect backfill — jobs newer than an eventSeq cursor",
        "description": "The session user's generation_jobs with event_seq > since, ascending, in the same Job shape the WebSocket pushes. The web client calls this on every WS (re)open with its max seen eventSeq so events missed while the socket was down replay. since=0 returns everything.\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Missed job events, ascending by eventSeq",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Job"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_SINCE"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/jobs/ws": {
      "get": {
        "summary": "Job event stream (WebSocket upgrade)",
        "description": "Upgrades to a WebSocket forwarded to the caller's per-user JobStreamDO. Every job status change is pushed as {\"type\":\"job\",\"job\":Job}. Requires the session cookie on the handshake.\n",
        "tags": [
          "jobs"
        ],
        "responses": {
          "101": {
            "description": "Switching Protocols — socket attached"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "426": {
            "description": "Upgrade header missing — not a WebSocket request"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/balance": {
      "get": {
        "summary": "Spendable token balance + open holds",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "Derived from the append-only credit_ledger",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance": {
                      "type": "integer"
                    },
                    "grant": {
                      "type": "integer"
                    },
                    "topup": {
                      "type": "integer"
                    },
                    "held": {
                      "type": "integer"
                    },
                    "holds": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "amount": {
                            "type": "integer"
                          },
                          "generationId": {
                            "type": "string",
                            "nullable": true
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/catalog": {
      "get": {
        "summary": "Public pricing catalog (tiers + packs)",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "Active subscription tiers and top-up packs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "provider": {
                      "type": "string",
                      "enum": [
                        "dodo",
                        "none"
                      ]
                    },
                    "tiers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "priceUsd": {
                            "type": "number"
                          },
                          "monthlyTokens": {
                            "type": "integer"
                          },
                          "rolloverMultiple": {
                            "type": "number"
                          },
                          "featured": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "packs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "integer"
                          },
                          "priceUsd": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/ai/billing/checkout/subscription": {
      "post": {
        "summary": "Hosted Dodo checkout for a subscription tier",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tier"
                ],
                "properties": {
                  "tier": {
                    "type": "string",
                    "enum": [
                      "studio",
                      "editorial",
                      "couture"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hosted checkout URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_TIER"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "502": {
            "description": "CHECKOUT_FAILED"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/checkout/topup": {
      "post": {
        "summary": "Hosted Dodo checkout for a token pack",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "pack"
                ],
                "properties": {
                  "pack": {
                    "type": "string",
                    "enum": [
                      "pack_250",
                      "pack_1000",
                      "pack_3000"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hosted checkout URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_PACK"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "502": {
            "description": "CHECKOUT_FAILED"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/checkout/custom": {
      "post": {
        "summary": "Hosted Dodo checkout for a pay-what-you-want top-up",
        "description": "The director names a dollar amount; it becomes tokens at the top-up rate — the highest price per token any active row in `topup_packs` sells at, so a custom amount is never cheaper per token than a pack, and packs are already pricier than every plan (check:pricing rule 4). The amount is quantised DOWN to whole tokens, so $10.30 buys 257 tokens and charges $10.28. No processing-fee gross-up, so no fee line: the fee is the same one the fixed packs already carry. Owner only — it charges the house.\n",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amountUsd"
                ],
                "properties": {
                  "amountUsd": {
                    "type": "number",
                    "description": "Dollars. Minimum is the cheapest active pack's price ($10 today); maximum is $5,000, a typo guard rather than a policy.\n",
                    "example": 25
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hosted checkout URL, plus what will be charged and granted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "tokens": {
                      "type": "integer",
                      "description": "Whole tokens the webhook will grant when the money lands."
                    },
                    "amountCents": {
                      "type": "integer",
                      "description": "What the card is actually charged — never more than was named."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_AMOUNT or AMOUNT_OUT_OF_RANGE (with minCents/maxCents)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN — only the owner spends the house's money"
          },
          "502": {
            "description": "CHECKOUT_FAILED"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/autocharge": {
      "get": {
        "summary": "The house's auto top-up rule, ceiling, card and audit trail",
        "description": "The standing order — \"when the balance falls below X, buy pack Y\" — plus what it has already spent this calendar month, whether a card is saved, and every automatic charge the provider has confirmed. `available` is false unless AUTOCHARGE_ENABLED=1 AND a Dodo token is configured; `canArm` mirrors the server's canSpendMoney (owner only).\n",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "Settings + ceiling + packs + charges",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "available": {
                      "type": "boolean"
                    },
                    "canArm": {
                      "type": "boolean"
                    },
                    "hasCard": {
                      "type": "boolean"
                    },
                    "hasCustomer": {
                      "type": "boolean"
                    },
                    "settings": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "thresholdTokens": {
                          "type": "integer"
                        },
                        "refillCents": {
                          "type": "integer"
                        },
                        "refillPack": {
                          "type": "string",
                          "nullable": true
                        },
                        "monthlyCapCents": {
                          "type": "integer"
                        }
                      }
                    },
                    "thisMonth": {
                      "type": "object",
                      "properties": {
                        "period": {
                          "type": "string",
                          "example": "2026-07"
                        },
                        "spentCents": {
                          "type": "integer"
                        }
                      }
                    },
                    "lastChargeAt": {
                      "type": "string",
                      "nullable": true,
                      "format": "date-time"
                    },
                    "packs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "integer"
                          },
                          "priceCents": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "limits": {
                      "type": "object"
                    },
                    "charges": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "amountCents": {
                            "type": "integer"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "reference": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "put": {
        "summary": "Arm, retune or disarm the auto top-up rule (OWNER ONLY)",
        "description": "Real money on a standing order, so this is gated by canSpendMoney — owner only, 403 for every other seat. The refill is always a real catalogue pack; the monthly ceiling is a hard stop and may never be below one refill.\n",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled",
                  "thresholdTokens",
                  "pack",
                  "monthlyCapUsd"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "thresholdTokens": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000
                  },
                  "pack": {
                    "type": "string",
                    "enum": [
                      "pack_250",
                      "pack_1000",
                      "pack_3000"
                    ]
                  },
                  "monthlyCapUsd": {
                    "type": "number",
                    "minimum": 10,
                    "maximum": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved rule, read back",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "settings": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_THRESHOLD · BAD_CAP · BAD_PACK · CAP_BELOW_REFILL"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "forbidden — role_cannot_spend_money"
          },
          "409": {
            "description": "AMBIGUOUS_PACK_PRICE"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/autocharge/run": {
      "post": {
        "summary": "Fire the standing order if the house is genuinely short",
        "description": "A HINT endpoint — every condition is re-validated server-side, so calling it is safe and it answers 200 either way with `{ charged, reason }`. The reservation (enabled · 10-minute debounce · monthly ceiling · live spendable balance) is one atomic UPDATE, so two concurrent calls can never both charge. Nothing here credits a token: payment.succeeded is the sole grant authority. Gated by canSpendTokens (viewers excluded) — it executes an instruction the owner already gave.\n",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "Charged, or the reason nothing happened",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "charged": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": "string",
                      "enum": [
                        "flag_off",
                        "not_configured",
                        "disabled",
                        "bad_refill",
                        "no_customer",
                        "no_payment_method",
                        "above_threshold",
                        "monthly_cap_reached",
                        "debounced",
                        "raced"
                      ]
                    },
                    "pack": {
                      "type": "string"
                    },
                    "tokens": {
                      "type": "integer"
                    },
                    "amountCents": {
                      "type": "integer"
                    },
                    "paymentId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "forbidden — role_cannot_spend_tokens"
          },
          "502": {
            "description": "charge_failed — the provider refused; the reservation is rolled back"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/portal": {
      "post": {
        "summary": "Open the merchant of record's hosted portal (OWNER ONLY)",
        "description": "Dodo owns the card, the subscription controls and the tax documents, so this hands off rather than rebuilding them. Owner only (canSpendMoney).\n",
        "tags": [
          "billing"
        ],
        "responses": {
          "200": {
            "description": "The portal session URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "NO_CUSTOMER — nothing has ever been bought"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "forbidden — role_cannot_spend_money"
          },
          "502": {
            "description": "PORTAL_FAILED · PORTAL_NO_URL"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/billing/webhook/dodo": {
      "post": {
        "summary": "Dodo payment webhook (standard-webhooks signature)",
        "description": "The only unauthenticated billing write — authenticated by the webhook-id / webhook-timestamp / webhook-signature headers, not a session. Duplicate deliveries dedupe via mor_webhook_events.\n",
        "tags": [
          "billing"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied ({ ok, ... }) or deduped ({ ok, duplicate })"
          },
          "400": {
            "description": "UNPARSEABLE"
          },
          "401": {
            "description": "BAD_SIGNATURE"
          },
          "500": {
            "description": "APPLY_FAILED"
          }
        }
      }
    },
    "/api/toolkit/email/webhook/resend": {
      "post": {
        "summary": "Resend delivery webhook (standard-webhooks signature)",
        "description": "Delivery truth. email_log.status is written once at send time — 'sent' means Resend accepted it, not that a director read it — so without this a hard bounce is indistinguishable from a delivered mail. Public and unauthenticated, authenticated by the webhook-id / webhook-timestamp / webhook-signature headers (the same verifier the Dodo webhook uses), and it fails CLOSED when RESEND_WEBHOOK_SECRET is unset. Correlates on provider_id (Resend's message id, echoed back as data.email_id). email.bounced and email.complained also raise an alert; events that say nothing about arrival answer 200 and write nothing.\n",
        "tags": [
          "email"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded ({ ok, status }), unmatched ({ ok, matched:false }) or ignored ({ ok, ignored })"
          },
          "400": {
            "description": "UNPARSEABLE"
          },
          "401": {
            "description": "BAD_SIGNATURE"
          }
        }
      }
    },
    "/api/toolkit/assets": {
      "get": {
        "summary": "The house's captured assets, newest first",
        "description": "The body is `{ assets: [...] }`, not a bare array — it was documented as a bare array until task #40 and never was one.\n",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "folderId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Narrow to one collection"
          }
        ],
        "responses": {
          "200": {
            "description": "Asset list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Asset"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/assets/upload": {
      "post": {
        "summary": "Hang one image in the house library (multipart)",
        "description": "Bytes → R2, row → assets. The upload itself is FREE.\nSMART UPLOAD — `classify=1` is an OPT-IN read priced at 1 token per image (the describe-item classifier, run in-process after the row is written). It files the piece into its wardrobe bucket (`kind`) and gives it a human name — \"IMG_2489.jpeg\" becomes \"white ribbed lace trim henley\", a model collage comes back kind=model. Anything you state yourself wins: a supplied `name`/`kind` is never overwritten, and `sku` is your own code — it is never invented. Omit `classify` and nothing is read and nothing is charged.\nA failed read never loses the upload and never charges: 201 with the asset unclassified and `classify.ok=false` carrying the reason (INSUFFICIENT_TOKENS, AI_NOT_CONFIGURED, AI_FAILED, …).\n",
        "tags": [
          "assets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "folderId": {
                    "type": "string",
                    "description": "Target collection; omit for the root"
                  },
                  "name": {
                    "type": "string",
                    "description": "Your own label; wins over the smart read"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "clothes",
                      "accessory",
                      "jewelry",
                      "model",
                      "background"
                    ],
                    "description": "Your own wardrobe bucket; wins over the smart read"
                  },
                  "sku": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "The brand's own code"
                  },
                  "classify": {
                    "type": "string",
                    "enum": [
                      "1",
                      "true",
                      "on",
                      "0"
                    ],
                    "description": "Opt in to the 1-token smart read"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The hung asset (+ `classify` when the read was requested)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "asset": {
                      "$ref": "#/components/schemas/Asset"
                    },
                    "classify": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        },
                        "nameEn": {
                          "type": "string"
                        },
                        "kind": {
                          "type": "string"
                        },
                        "error": {
                          "type": "string"
                        },
                        "tokens": {
                          "type": "integer",
                          "description": "Charged — 1 on a read, 0 on a failure"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "NO_FILE / UNSUPPORTED_TYPE / EMPTY_FILE / BAD_KIND / BAD_SKU / BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown or foreign folderId"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE (12 MB cap)"
          }
        }
      }
    },
    "/api/toolkit/assets/{id}": {
      "get": {
        "summary": "Stream one asset's bytes from R2 (owner-gated)",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image bytes (content-type from the asset row)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the owner"
          },
          "404": {
            "description": "Unknown or deleted"
          }
        }
      },
      "patch": {
        "summary": "Rename, classify, tag or move one piece",
        "description": "An explicit `folderId: null` moves the piece back to the root. `kind` is stored as the reserved `kind:` tag and `sku` as the `sku:` tag, so both can be cleared with null. An empty patch is a 200 no-op, not an error.\n",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "sku": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 64
                  },
                  "kind": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "clothes",
                      "accessory",
                      "jewelry",
                      "model",
                      "background"
                    ]
                  },
                  "folderId": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated asset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "asset": {
                      "$ref": "#/components/schemas/Asset"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_KIND / BAD_SKU / BAD_FOLDER"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown or foreign asset",
            "or unknown target folder": null
          }
        }
      }
    },
    "/api/toolkit/onboarding": {
      "get": {
        "summary": "First-workflow checklist progress",
        "tags": [
          "onboarding"
        ],
        "responses": {
          "200": {
            "description": "Progress + reward state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingProgress"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Complete a checklist step (idempotent)",
        "description": "Completing the last step mints a one-time 30-token promo grant (rewardJustGranted true exactly once).\n",
        "tags": [
          "onboarding"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "step"
                ],
                "properties": {
                  "step": {
                    "type": "string",
                    "enum": [
                      "brand",
                      "upload",
                      "cleanup",
                      "model",
                      "tryon",
                      "organize",
                      "tokens"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated progress",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/OnboardingProgress"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "rewardJustGranted": {
                          "type": "boolean"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY | UNKNOWN_STEP"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/admin/users": {
      "get": {
        "summary": "All users with balance + access (admin)",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "User list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/access": {
      "post": {
        "summary": "Grant / revoke toolkit or tool access (admin)",
        "tags": [
          "admin"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "grant_toolkit",
                      "revoke_toolkit",
                      "grant_tool",
                      "revoke_tool"
                    ]
                  },
                  "userId": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  },
                  "toolkitId": {
                    "type": "string"
                  },
                  "toolId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, count }"
          },
          "400": {
            "description": "Missing action / item id / target"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/grant-tokens": {
      "post": {
        "summary": "Comp a token grant to a user (admin)",
        "tags": [
          "admin"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "userId",
                  "tokens"
                ],
                "properties": {
                  "userId": {
                    "type": "string"
                  },
                  "tokens": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "expiryDays": {
                    "type": "integer",
                    "nullable": true
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, granted, tokens, expiresAt }"
          },
          "400": {
            "description": "USER_REQUIRED | BAD_TOKENS (body includes max)"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "USER_NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/admin/tool-items": {
      "get": {
        "summary": "Curated items for one tool (admin)",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "toolId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ items }"
          },
          "400": {
            "description": "toolId required"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "summary": "Append a curated item (admin)",
        "tags": [
          "admin"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "toolId",
                  "category"
                ],
                "properties": {
                  "toolId": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "contentEn": {
                    "type": "string"
                  },
                  "titleHe": {
                    "type": "string"
                  },
                  "labelHe": {
                    "type": "string"
                  },
                  "imageUrl": {
                    "type": "string"
                  },
                  "notesHe": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, id, sortOrder }"
          },
          "400": {
            "description": "toolId and category required"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "delete": {
        "summary": "Delete a curated item by id (admin)",
        "tags": [
          "admin"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ ok, deleted }"
          },
          "400": {
            "description": "id required"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/ai-costs": {
      "get": {
        "summary": "Token/cost usage summary from the ledger (admin)",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Settled tokens + real USD per tool, per day, and totals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "byTool": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tool": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "integer"
                          },
                          "cost_usd": {
                            "type": "number"
                          },
                          "n": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "daily": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "day": {
                            "type": "string"
                          },
                          "tokens": {
                            "type": "integer"
                          },
                          "cost_usd": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "totalTokensSettled": {
                      "type": "integer"
                    },
                    "totalCostUsd": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/explore": {
      "get": {
        "summary": "Every offering this director may see, across all kinds",
        "tags": [
          "explore"
        ],
        "responses": {
          "200": {
            "description": "The board",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "offerings"
                  ],
                  "properties": {
                    "offerings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "kind",
                          "id",
                          "name"
                        ],
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "model",
                              "background",
                              "variation_set",
                              "workflow"
                            ]
                          },
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "note": {
                            "type": "string",
                            "nullable": true
                          },
                          "previewUrl": {
                            "type": "string",
                            "nullable": true
                          },
                          "priceCents": {
                            "type": "integer",
                            "description": "Absent or 0 = free"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "owned": {
                            "type": "boolean"
                          },
                          "exclusive": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/explore/{kind}/{id}/take": {
      "post": {
        "summary": "Take an offering — free grants now, priced opens checkout",
        "tags": [
          "explore"
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Free — granted; or priced — a hosted checkout url",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string",
                      "description": "Hosted checkout (priced offerings)"
                    },
                    "pending": {
                      "type": "boolean",
                      "description": "A live claim already had a checkout open"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "TAKEN (an exclusive is claimed) or CHECKOUT_PENDING",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "NOT_FOR_SALE or BILLING_NOT_CONFIGURED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/entitlements": {
      "get": {
        "summary": "What this director holds, and any checkout still open",
        "description": "`holdings` is granted only. `open` is pending-and-unexpired and is returned SEPARATELY so a standing charge is never dressed as a possession.\n",
        "tags": [
          "explore"
        ],
        "responses": {
          "200": {
            "description": "Holdings and open checkouts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "holdings",
                    "open"
                  ],
                  "properties": {
                    "holdings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "offeringId": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "purchase",
                              "own"
                            ]
                          },
                          "priceCents": {
                            "type": "integer"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "paymentRef": {
                            "type": "string",
                            "nullable": true
                          },
                          "exclusive": {
                            "type": "boolean"
                          },
                          "acquiredAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "open": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string"
                          },
                          "offeringId": {
                            "type": "string"
                          },
                          "checkoutUrl": {
                            "type": "string",
                            "nullable": true
                          },
                          "expiresAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/admin/offerings": {
      "get": {
        "summary": "The catalogue, as the house sees it",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Offerings"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "summary": "Create or update an offering (price, tier, previews)",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "The saved offering"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/offerings/retire": {
      "post": {
        "summary": "Retire an offering from the board",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Retired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/entitlements": {
      "get": {
        "summary": "Real-money purchases across the house",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Entitlements"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/entitlements/revoke": {
      "post": {
        "summary": "Revoke a granted entitlement (a refund's other half)",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Revoked"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/develops": {
      "get": {
        "summary": "Develop runs across the house, newest first",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Runs"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/develops/action": {
      "post": {
        "summary": "Retry or cancel a develop",
        "description": "Cancel carries the Darkroom's honest semantics — queued frames release their hold, a frame already rendering finishes and still costs.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "The action's honest tally"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/overview": {
      "get": {
        "summary": "The house at a glance — the first screen",
        "description": "Directors and houses, how many worked this week, the outstanding token float, what the engine is doing right now, what moved today against yesterday, and a fourteen-day strip. Money and tokens are separate fields and are never summed — there is deliberately no field holding the two added together.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "The house at a glance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/frames": {
      "get": {
        "summary": "Every house's outputs, browsable",
        "description": "What is actually being MADE on FLAM. Narrows server-side by house (organization id), tool (source_tool), age in days, and origin — `made` is anything the house developed, `brought` is what a director uploaded. The filter lists ride along in the same answer so the panel never needs a second round trip.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Frames, plus the houses and tools that exist"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/frames/{id}/file": {
      "get": {
        "summary": "The bytes of any house's frame (admin only)",
        "description": "assets.ts's streamAsset owner-gates on organization_id and answers 404 to everyone else — correct for a director, and fatal for an admin who has to look at the frame a customer is complaining about. Widening that route would put \"or an admin\" inside the DIRECTOR path, where a bug in the allowlist becomes a cross-house read for real users. This one is admin-only from its first line.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "The image bytes"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such frame"
          }
        }
      }
    },
    "/api/toolkit/admin/tokenomics": {
      "get": {
        "summary": "What a token costs against what we charge, and the funnel",
        "description": "Three layers, never blended — the price book per tool AND per quality (tokens charged against credit_price_book.loaded_cost_usd), what actually ran from the jobs spine, and the conversion funnel. While `costUnmeasured` is true the worker still settles with a measured cost of zero, so every margin is MODELLED from the price book and the panel says so rather than printing a 100% margin.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Unit economics, actuals and the funnel"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/emails": {
      "get": {
        "summary": "Every letter the house sent (email_log)",
        "description": "The list omits `html` on purpose — a hundred rendered emails is megabytes of body for a table that shows none of it. Pass ?id= for ONE row WITH its byte-exact archived HTML, exactly as the recipient received it. `skipped` is the dev-safe lane, not a fault.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Letters, or one letter with its archive"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such letter"
          }
        }
      }
    },
    "/api/toolkit/admin/failures": {
      "get": {
        "summary": "Failures grouped by what the provider actually said",
        "description": "Twelve rows of \"the provider returned no frame\" is one problem wearing twelve uuids, so failures group on the error text with ids and digits knocked out. Each group carries how many HOUSES it reached — one house is probably their input, several is ours — and the whole real message from the most recent occurrence. Refused emails ride along.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Grouped failures, plus refused emails"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/toolkit/admin/payments": {
      "get": {
        "summary": "Real transactions — subscriptions, top-ups, offerings, comps",
        "description": "Money reaches FLAM through three doors and the Money book only showed one. Comps are included and read as zero: hiding them makes the token float unexplainable, pricing them at anything else makes them look like revenue. `amountUsd` is currency and `tokens` is what that payment minted — separate fields, no total, because a top-up is money that bought tokens, not money and tokens.\n",
        "tags": [
          "admin"
        ],
        "responses": {
          "200": {
            "description": "Transactions and standing subscriptions"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/account": {
      "get": {
        "summary": "The director's own account, in one read",
        "description": "Profile, current plan, token balance WITH where those tokens went by workflow, and the book of money that actually landed. Deliberately does NOT re-serve what already has a door: name/avatar, password, social linking and sessions are Better Auth (/api/auth/*); plan and pack prices are /api/toolkit/ai/billing/catalog; upgrades and top-ups are the checkout routes; export/erase are /me/export and /me/delete.\n",
        "tags": [
          "account"
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Spend window — 7, 30 or 90. Anything else falls back to 30.",
            "schema": {
              "type": "integer",
              "enum": [
                7,
                30,
                90
              ],
              "default": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/account/plan/cancel": {
      "post": {
        "summary": "Stop the renewal (cancel at period end)",
        "description": "Cancel means \"do not renew\", never \"take it back now\" — every token already granted stays, and the plan runs to the end of the period it is paid for. The provider is told first and our row is mirrored only on its yes, so a failed vendor call can never leave a director believing they cancelled while the card is still charged. Grants nothing; the webhook remains the only thing that grants.\n",
        "tags": [
          "account"
        ],
        "responses": {
          "200": {
            "description": "The plan will not renew",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "canceled"
                      ]
                    },
                    "endsAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NO_ACTIVE_PLAN — nothing is renewing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "CANCEL_FAILED — the provider refused; nothing was changed here",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED — no provider credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}/develop-lock": {
      "post": {
        "summary": "Develop the LOOK's lock frame",
        "description": "Composes the lock frame server-side (the prompt never enters the jobs DB) and runs it through the spine as ONE job. Draft looks only. Reserves the whole cost — tier plus any restoration pass the destination forces — before anything is enqueued. The finished `resultUrl` is the frame as DEVELOPED: the tier's pixels at the destination's shape. It is not resampled to `output.width` × `output.height` — cut it yourself, which is a pure downscale for every place except the two A-series print sizes.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DevelopRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job queued (or the existing job on an idempotent replay)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY | UNKNOWN_DESTINATION | BAD_DIMENSIONS | OUTPUT_TOO_LARGE | QUALITY_TOO_LOW_FOR_OUTPUT — the body carries a plain-language `detail` naming the pixels (\"1K develops at 1,024 px — an Instagram post needs 1,080.\").\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutputError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS — body includes need + spendable"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "409": {
            "description": "DEVELOP_IN_FLIGHT | LOOK_LOCKED"
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}/variations": {
      "post": {
        "summary": "Fan the LOOK out into its variations",
        "description": "One job per chosen variation, with smart refs per the library (a medium shot drops the shoes; a back shot keeps the model). The look must be locked. All-or-nothing reserve across the whole batch: the first shortfall releases every hold already taken. An `output` destination applies to EVERY frame, so its cost is reserved per frame up front — and every `resultUrl` comes back as developed (the tier's pixels at that destination's shape), not resampled to its exact width × height.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/DevelopRequest"
                  },
                  {
                    "type": "object",
                    "required": [
                      "keys"
                    ],
                    "properties": {
                      "keys": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 12,
                        "items": {
                          "type": "string"
                        },
                        "description": "Variation keys from the library"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "One queued job per requested key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "jobId": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY | KEYS_REQUIRED | TOO_MANY_FRAMES | UNKNOWN_VARIATION | UNKNOWN_DESTINATION | BAD_DIMENSIONS | OUTPUT_TOO_LARGE | QUALITY_TOO_LOW_FOR_OUTPUT\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutputError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS — body includes need + spendable"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "409": {
            "description": "DEVELOP_IN_FLIGHT | LOCK_REQUIRED"
          }
        }
      }
    },
    "/api/toolkit/ai/extract-character": {
      "post": {
        "summary": "Read a person out of a reference image (or a four-view collage)",
        "description": "Returns the five character-studio fields — age, skin, eyes, hair, extras — so a director can review WHO the house just cast before any frame is developed. A FLAM house model is a collage of several views of one person; the engine reads across all the panels and answers once. No-face guard: a landscape, an object or a garment shot returns faceDetected=false with every field empty. It never invents a person, and `extras` stays empty unless a real identifying mark is visible. Flat 1 token, refunded on any failure.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "imageUrl"
                ],
                "properties": {
                  "imageUrl": {
                    "type": "string",
                    "description": "URL or data URI"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The five fields",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "faceDetected": {
                      "type": "boolean"
                    },
                    "fields": {
                      "type": "object",
                      "properties": {
                        "age": {
                          "type": "string"
                        },
                        "skin": {
                          "type": "string"
                        },
                        "eyes": {
                          "type": "string"
                        },
                        "hair": {
                          "type": "string"
                        },
                        "extras": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS / AI_NEEDS_BILLING"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/detect-items": {
      "post": {
        "summary": "Find every garment and accessory in a frame",
        "description": "Step 1 of 2 (detect then segment). Exhaustively detects each distinct garment, accessory and editable hair/face feature so a UI can draw tap targets. A rail of hanging clothes, a flat lay or a shop shelf is a normal input. Boxes are [ymin, xmin, ymax, xmax] normalized 0-1000 and are APPROXIMATE tap targets, not cutouts.\nORIENTATION: boxes are in the RAW stored pixel matrix; this lane does not apply a JPEG's EXIF Orientation flag and fal (behind /segment) does. Pass the returned `label` to /segment, or bake orientation before both calls. Flat 1 token, refunded on any failure.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "imageUrl"
                ],
                "properties": {
                  "imageUrl": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Detected items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "label": {
                            "type": "string"
                          },
                          "box": {
                            "type": "array",
                            "items": {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 1000
                            },
                            "minItems": 4,
                            "maxItems": 4
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS / AI_NEEDS_BILLING"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/segment": {
      "post": {
        "summary": "A clean cutout of the ONE item that was tapped",
        "description": "Step 2 of 2 (detect then segment). SAM 3 on fal, prompted by the detect-items `label` (orientation-immune — prefer it), a `box` [x0,y0,x1,y1] in PIXELS of the EXIF-APPLIED frame, or a `point` [x,y]. At least one target is required. What comes back is an RGBA CUTOUT, not a white-on-black binary mask. It is an internal compositing input and is never captured as a library asset. Flat 1 token, refunded on failure.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "imageUrl"
                ],
                "properties": {
                  "imageUrl": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "box": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 4,
                    "maxItems": 4
                  },
                  "point": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "minItems": 2,
                    "maxItems": 2
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cutout",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "bbox": {
                      "type": "array",
                      "nullable": true,
                      "items": {
                        "type": "number"
                      },
                      "description": "[x_min, y_min, x_max, y_max] NORMALIZED 0-1. Note the request's `box` is in PIXELS — same axis order, different units. Multiply by the frame's width/height to compare.\n"
                    },
                    "spent": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_JSON / IMAGE_REQUIRED / TARGET_REQUIRED"
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "502": {
            "description": "SEGMENT_FAILED"
          },
          "503": {
            "description": "FAL_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/brand": {
      "get": {
        "summary": "The house's extracted visual DNA",
        "description": "Returns the profile plus `clause` — the EXACT text the generators will inject, built by the same @flam/ai builder the worker calls, so this review surface can never disagree with the renders. A house that has never run a read gets empty defaults, never a 404.\n",
        "tags": [
          "brand"
        ],
        "responses": {
          "200": {
            "description": "The profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "profile": {
                      "$ref": "#/components/schemas/BrandProfile"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      },
      "patch": {
        "summary": "Correct the extracted DNA, or switch the injection off",
        "description": "The house gets the last word. Every field is optional and merges onto what is stored; the merged result is then clamped exactly as an extraction would be, so a partial edit cannot smuggle past a bound the extractor enforces. `injectEnabled: false` is the kill switch — the profile stays, the clause goes empty, every generator renders unbranded. dna_runs is never touched here, so editing is free and unlimited.\n",
        "tags": [
          "brand"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "visionEn": {
                    "type": "string",
                    "maxLength": 600
                  },
                  "vibe": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "palette": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "pattern": "^#[0-9a-fA-F]{6}$"
                    }
                  },
                  "signatureKeywords": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "category": {
                          "type": "string"
                        },
                        "phrase": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "injectEnabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "profile": {
                      "$ref": "#/components/schemas/BrandProfile"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_JSON"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "A viewer may not edit the clause every render pays for"
          }
        }
      }
    },
    "/api/toolkit/brand/dna": {
      "post": {
        "summary": "Read the house's visual DNA off its own imagery",
        "description": "The cross-tool magic. Upload 1-10 of the house's own previous frames and one multi-image vision call finds the through-line: vibe, palette, signature cues and a visionEn paragraph that is injected into EVERY packshot, character and lookbook frame the house develops afterwards. The read describes the house faithfully — the 8-category fashion vocabulary is a descriptive lens, never a style to impose.\nBILLING is server-decided from brand_profiles.dna_runs and never from the client: the FIRST read per house is free (charged 0), every re-read costs 2 tokens. The counter is bumped on every successful read whether or not the house keeps the answer. A failed read charges nothing and does not bump it.\n",
        "tags": [
          "brand"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "image"
                ],
                "properties": {
                  "image": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "maxItems": 10
                  },
                  "vibe": {
                    "type": "string",
                    "description": "JSON string[] of vibe hints"
                  },
                  "products": {
                    "type": "string",
                    "description": "what the house sells"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The extracted (and stored) DNA",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "profile": {
                      "$ref": "#/components/schemas/BrandProfile"
                    },
                    "charged": {
                      "type": "integer",
                      "description": "0 on the first read, 2 after"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "NO_IMAGES / BAD_FORM"
          },
          "401": {
            "description": "Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS / AI_NEEDS_BILLING"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE / SET_TOO_LARGE (32MB across the set)"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Liveness",
        "description": "Unauthenticated. The readiness probe Tilt polls.",
        "tags": [
          "meta"
        ],
        "responses": {
          "200": {
            "description": "The process is up",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    },
                    "service": {
                      "type": "string",
                      "enum": [
                        "api"
                      ]
                    },
                    "uptime": {
                      "type": "number",
                      "description": "Seconds since the process started"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/docs": {
      "get": {
        "summary": "Swagger UI rendered over /openapi.json",
        "tags": [
          "meta"
        ],
        "responses": {
          "200": {
            "description": "An HTML page"
          }
        }
      }
    },
    "/me": {
      "get": {
        "summary": "The signed-in user",
        "description": "The Better Auth `user` row for the session cookie. Identity only — no house scope.",
        "tags": [
          "me"
        ],
        "responses": {
          "200": {
            "description": "The Better Auth user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/me/export": {
      "get": {
        "summary": "GDPR access — export the caller's own user row",
        "description": "Sent as an attachment (Content-Disposition: attachment; filename=\"my-data.json\"). STARTER SCOPE — the Better Auth `user` row only; widen it before relying on this for a real DSAR.\n",
        "tags": [
          "me"
        ],
        "responses": {
          "200": {
            "description": "The export document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exportedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "user": {
                      "type": "object",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/me/delete": {
      "post": {
        "summary": "GDPR erasure — delete the caller's account",
        "description": "ONE transaction. Every organization the caller is the SOLE member of is deleted FIRST (that CASCADEs its imagery, wallet and ledger), then the user row. A SHARED house survives its departing seat — the house is the brand, not the person. No body.\n",
        "tags": [
          "me"
        ],
        "responses": {
          "200": {
            "description": "Erased",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "userId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/folders": {
      "get": {
        "summary": "Every collection in the house, flat",
        "description": "Oldest first. The client builds the tree from `parentId`.",
        "tags": [
          "assets"
        ],
        "responses": {
          "200": {
            "description": "The folder list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "folders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Folder"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Create a collection",
        "tags": [
          "assets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "parentId": {
                    "type": "string",
                    "nullable": true,
                    "description": "Nest under an owned folder"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created folder",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "folder": {
                      "$ref": "#/components/schemas/Folder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / NAME_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown or foreign parentId"
          }
        }
      }
    },
    "/api/toolkit/folders/{id}": {
      "patch": {
        "summary": "Rename a collection",
        "description": "A no-op rename (same name, or none supplied) still answers 200 with the folder.",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The folder",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "folder": {
                      "$ref": "#/components/schemas/Folder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown or foreign id"
          }
        }
      }
    },
    "/api/toolkit/assets/{id}/file": {
      "get": {
        "summary": "Stream one asset's bytes (the contract serving route)",
        "description": "Where every `url` in a list response points. Owner-gated, and a FOREIGN id answers 404, never 403 — this route never confirms that someone else's asset exists. (The legacy /api/toolkit/assets/{id} answers 403 for a foreign id; it is kept only for old links.)\n",
        "tags": [
          "assets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image bytes (Cache-Control private, max-age=3600; ETag from R2)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown",
            "deleted": null,
            "foreign": null,
            "or missing in R2": null
          }
        }
      }
    },
    "/api/toolkit/assets/group": {
      "post": {
        "summary": "Stamp a fresh shared group onto a set of pieces",
        "description": "Writes `group:<uuid>` onto every named asset, REPLACING any prior group (re-grouping moves a piece). All-or-nothing: every id must be one of the caller's live assets or nothing is written.\n",
        "tags": [
          "assets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assetIds"
                ],
                "properties": {
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Unique, non-empty. Duplicates are rejected."
                  },
                  "name": {
                    "type": "string",
                    "description": "Accepted and currently unused"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new group id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "groupId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_ASSET_IDS"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "One of the ids is unknown",
            "deleted or foreign — nothing was written": null
          }
        }
      }
    },
    "/api/toolkit/backgrounds": {
      "get": {
        "summary": "The whole library a look can stand on",
        "description": "Two sources, one call — the house's curated `grounds` (static, shipped with the code) and this director's own `scenes` (assets tagged `kind:background`, uploaded through /api/toolkit/assets/upload). Only the PUBLIC projection of a ground crosses the wire; the prompt clause it contributes stays server-side.\n",
        "tags": [
          "assets"
        ],
        "responses": {
          "200": {
            "description": "Grounds + the house's own sets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "grounds": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "note": {
                            "type": "string"
                          },
                          "tone": {
                            "type": "string"
                          },
                          "previewUrl": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "scenes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/search": {
      "get": {
        "summary": "One field over the whole house",
        "description": "Four lanes in ONE statement — piece (assets, matched on filename AND the `sku:` tag), collection (folders), look, workflow (lookbook templates). Under 2 characters is not an error: the same query runs wide open and the answer arrives as a flat `recent` list with `groups: []`.\n",
        "tags": [
          "search"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Grouped hits, or `recent` for a short query",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "string"
                    },
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "piece",
                              "collection",
                              "look",
                              "workflow"
                            ]
                          },
                          "items": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/SearchResult"
                            }
                          },
                          "total": {
                            "type": "integer",
                            "description": "Hits BEFORE the per-lane cap"
                          }
                        }
                      }
                    },
                    "recent": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchResult"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/develops": {
      "get": {
        "summary": "The house's develop RUNS, newest first",
        "description": "A run is DERIVED, not stored — params.runId, else the lookbook batch (lookId + stage), else same tool within a time gap. `state=active` keeps only runs with a frame still queued or rendering; the counts stay WHOLE (a 2-of-5 run still reports 5).\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "all"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DevelopRun"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_STATE / BAD_LIMIT"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/develops/{id}/cancel": {
      "post": {
        "summary": "Stop a whole run — honestly",
        "description": "A frame still QUEUED is cancelled and its hold RELEASED. A frame already RENDERING finishes and still costs: the model call is paid for and cannot be aborted. The response says both numbers out loud rather than implying a refund that is not happening.\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "description": "A develop-run id (run.<uuid> / look.<uuid>.<stage>)"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What actually stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "cancelled": {
                      "type": "integer"
                    },
                    "cancelledJobIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "stillRendering": {
                      "type": "integer",
                      "description": "Frames that will still land and still cost"
                    },
                    "tokensReleased": {
                      "type": "integer"
                    },
                    "run": {
                      "$ref": "#/components/schemas/DevelopRun"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown",
            "foreign": null,
            "or aged out of the recent window": null
          }
        }
      }
    },
    "/api/toolkit/jobs/{id}/cancel": {
      "post": {
        "summary": "Stop ONE frame",
        "description": "Same honesty as the run-level stop. `cancelled: false` carries a `reason` — ALREADY_RENDERING (paid for, still coming) or ALREADY_SETTLED.\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The frame's true state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "cancelled": {
                      "type": "boolean"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "done",
                        "failed",
                        "canceled"
                      ]
                    },
                    "tokensReleased": {
                      "type": "integer"
                    },
                    "reason": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "ALREADY_RENDERING",
                        "ALREADY_SETTLED"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND — unknown or foreign (never a 403 that leaks existence)"
          }
        }
      }
    },
    "/api/toolkit/jobs/{id}/duplicate": {
      "post": {
        "summary": "Run the same frame again as a NEW job",
        "description": "A fresh reserve, and that is correct — it is a new model call. Params are copied VERBATIM including the destination, so the quote covers any pass that destination forces. The source job is never re-billed or mutated.\n",
        "tags": [
          "jobs"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "idempotencyKey": {
                    "type": "string",
                    "description": "Makes one gesture safe to retry. Without it a double-click reserves twice and enqueues two paid model calls.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    },
                    "cost": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS { need, spendable } — nothing created, nothing charged"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "422": {
            "description": "UNKNOWN_TOOL / NO_INPUTS — never charged for a job that cannot run"
          }
        }
      }
    },
    "/api/toolkit/check-email": {
      "post": {
        "summary": "Route an entered email to the right sign-in step",
        "description": "PUBLIC by design. Reveals only whether an address exists and whether it has a password, for an address the caller already typed — the login page uses it to choose sign-in / Google-only / sign-up. Never 4xx: an unknown or missing email answers 200 with both flags false.\n",
        "tags": [
          "auth"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existence + method",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exists": {
                      "type": "boolean"
                    },
                    "hasPassword": {
                      "type": "boolean",
                      "description": "A Better Auth `credential` account with a password"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/invite/accept": {
      "get": {
        "summary": "Look up an invitation token before signing in",
        "description": "Answers the invited email so the sign-up screen can pre-fill it. No session needed.",
        "tags": [
          "invite"
        ],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The invited address",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "email": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Token required"
          },
          "404": {
            "description": "Invalid token"
          },
          "410": {
            "description": "Already accepted"
          }
        }
      },
      "post": {
        "summary": "Accept an invitation as the signed-in user",
        "description": "The email on the invite must match the session's, otherwise 403 — an invite is not transferable. Accepting attaches the pre-created email-keyed grants to the caller's HOUSE and repairs a missing personal org (ensurePersonalOrg is idempotent). Replaying an accepted token is a no-op, not an error.\n",
        "tags": [
          "invite"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted (or already was)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "alreadyAccepted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Token required"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "This invite is for a different email"
          },
          "404": {
            "description": "Invalid token"
          }
        }
      }
    },
    "/api/toolkit/ai/depth": {
      "post": {
        "summary": "Depth map for the Lens Blur tool (1 token)",
        "description": "Depth Anything V2 on fal → a grayscale depth PNG url. The map is an INTERNAL input to the client-side depth-of-field; it is NOT captured as a library asset. Reserve → settle on 200 / release on any failure, so a failed call charges nothing.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "imageUrl": {
                    "type": "string",
                    "description": "An http(s) URL or a data URI"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The depth map",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "description": "A fal.media URL"
                    },
                    "spent": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS { need, spendable }"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "DEPTH_FAILED"
          },
          "503": {
            "description": "FAL_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/ai/describe-item": {
      "post": {
        "summary": "Classify one piece — auto-name + wardrobe bucket (1 token)",
        "description": "The smart-upload classifier as a standalone lane. The same read runs in-process for /api/toolkit/assets/upload?classify=1. `nameEn` is capped at 40 characters and an unknown `kind` is clamped to \"clothes\" — the model can drift, the contract cannot.\n",
        "tags": [
          "ai"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "imageUrl": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The label",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "nameEn": {
                      "type": "string",
                      "maxLength": 40
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "clothes",
                        "accessory",
                        "jewelry",
                        "model",
                        "background"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "IMAGE_REQUIRED / EMPTY_IMAGE / BAD_FORM"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS / AI_NEEDS_BILLING"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "413": {
            "description": "IMAGE_TOO_LARGE"
          },
          "502": {
            "description": "AI_BAD_OUTPUT / AI_FAILED"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/lookbook/templates": {
      "get": {
        "summary": "Shot templates — the house's built-ins plus the caller's own",
        "description": "Built-ins are the rows with `organization_id IS NULL`; they list first.",
        "tags": [
          "lookbook"
        ],
        "responses": {
          "200": {
            "description": "The templates, each with its shot count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "slots": {
                            "type": "object",
                            "nullable": true
                          },
                          "shot_count": {
                            "type": "integer"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/lookbook/templates/{id}": {
      "get": {
        "summary": "One template and its shots, in order",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The template + its shots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "type": "object"
                    },
                    "shots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "framing": {
                            "type": "string",
                            "nullable": true
                          },
                          "pose": {
                            "type": "string",
                            "nullable": true
                          },
                          "expression": {
                            "type": "string",
                            "nullable": true
                          },
                          "sort_order": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "id required"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND — unknown",
            "or another house's template": null
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks": {
      "get": {
        "summary": "The house's looks, newest first (50)",
        "tags": [
          "lookbook"
        ],
        "responses": {
          "200": {
            "description": "The looks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "looks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Look"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Create a look from the director's three choices",
        "description": "Model + items + background, dressed by an aesthetic preset. SELECTIONS ONLY — nothing develops and nothing is charged here. Every referenced asset is owner-gated up front, so a foreign id writes nothing.\n",
        "tags": [
          "lookbook"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "modelAssetId",
                  "itemAssetIds",
                  "background",
                  "aesthetic"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "modelAssetId": {
                    "type": "string"
                  },
                  "itemAssetIds": {
                    "type": "array",
                    "maxItems": 6,
                    "items": {
                      "type": "object",
                      "required": [
                        "assetId",
                        "role"
                      ],
                      "properties": {
                        "assetId": {
                          "type": "string"
                        },
                        "role": {
                          "type": "string",
                          "description": "One of the composer's ITEM_ROLES"
                        }
                      }
                    }
                  },
                  "background": {
                    "type": "object",
                    "description": "{ kind: \"preset\", value } or { kind: \"asset\", assetId }",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "preset",
                          "asset"
                        ]
                      },
                      "value": {
                        "type": "string"
                      },
                      "assetId": {
                        "type": "string"
                      }
                    }
                  },
                  "aesthetic": {
                    "type": "string",
                    "description": "A key of the composer's AESTHETIC_PRESETS"
                  },
                  "brandId": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The draft look",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "look": {
                      "$ref": "#/components/schemas/Look"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / NAME_REQUIRED / MODEL_REQUIRED / ITEMS_REQUIRED / TOO_MANY_ITEMS / BAD_ITEM / BAD_BACKGROUND / BAD_AESTHETIC — the rejection carries the legal vocabulary (roles / presets)\n"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND — a referenced asset is unknown",
            "deleted or foreign": null
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}": {
      "get": {
        "summary": "One look, whole — selections, lock state, the variation batch",
        "description": "Each variation is joined with its generation_job, so status / resultAssetId / error come back per frame. `lockDevelop` is the newest lock-stage job, which is what lets a reloaded wizard rehydrate the loop.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The look + its frames",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "look": {
                      "$ref": "#/components/schemas/Look"
                    },
                    "variations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "framing": {
                            "type": "string"
                          },
                          "jobId": {
                            "type": "string"
                          },
                          "assetId": {
                            "type": "string",
                            "nullable": true
                          },
                          "locked": {
                            "type": "boolean"
                          },
                          "status": {
                            "type": "string"
                          },
                          "error": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "lockDevelop": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "resultAssetId": {
                          "type": "string",
                          "nullable": true
                        },
                        "error": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}/lock": {
      "post": {
        "summary": "Freeze the Look Base",
        "description": "PROVENANCE IS REQUIRED — the asset must be the captured result of one of THIS look's develop jobs, otherwise 422. Locking also records lineage: the Look Base's parent becomes the model collage.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assetId"
                ],
                "properties": {
                  "assetId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The locked look",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "look": {
                      "$ref": "#/components/schemas/Look"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / ASSET_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "409": {
            "description": "DEVELOP_IN_FLIGHT — the look is mid-batch"
          },
          "422": {
            "description": "NOT_A_LOOK_FRAME — that asset did not develop from this look"
          }
        }
      }
    },
    "/api/toolkit/lookbook/looks/{id}/unlock": {
      "post": {
        "summary": "The explicit intent required before re-developing a locked look",
        "description": "Nothing is destroyed — the look-base fields stay and the next lock supersedes them. Already a draft ⇒ 200, unchanged. No body.\n",
        "tags": [
          "lookbook"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The draft look",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "look": {
                      "$ref": "#/components/schemas/Look"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          },
          "409": {
            "description": "DEVELOP_IN_FLIGHT"
          }
        }
      }
    },
    "/api/toolkit/models": {
      "get": {
        "summary": "The casting board",
        "description": "House models plus this director's own. An exclusive face under a LIVE claim by SOMEONE ELSE is absent entirely — it can be neither bought nor cast, so showing it would be noise. Order: yours, then free, sale, exclusive; newest first inside each band.\n",
        "tags": [
          "models"
        ],
        "responses": {
          "200": {
            "description": "The board",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "models": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Model"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/models/{id}": {
      "get": {
        "summary": "One face",
        "description": "Same visibility rule as the board — out of scope is 404, never a 403 that leaks.",
        "tags": [
          "models"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The model",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "model": {
                      "$ref": "#/components/schemas/Model"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown",
            "foreign": null,
            "or claimed by another director": null
          }
        }
      }
    },
    "/api/toolkit/models/own": {
      "post": {
        "summary": "Bring your own — register held assets as a personal model",
        "description": "Free by definition: the imagery is already the house's, so nothing is billed and the entitlement records source `own`. All-or-nothing — every assetId must be one of the caller's live assets.\n",
        "tags": [
          "models"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "assetIds"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Unique",
                    "non-empty": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The registered model",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "model": {
                      "$ref": "#/components/schemas/Model"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / NAME_REQUIRED / BAD_ASSET_IDS"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "An assetId is unknown",
            "deleted or foreign — nothing was written": null
          }
        }
      }
    },
    "/api/toolkit/models/{id}/checkout": {
      "post": {
        "summary": "Open the hosted checkout for a face",
        "description": "REAL MONEY — owner only. This route NEVER grants; the payment webhook does. Order is claim (the cross-director gate) → reserve (the per-director gate) → create the checkout, and every failure after the claim releases what it took. A double-click gets the SAME url back (`pending: true`); an already-owned face answers `already: true`.\n",
        "tags": [
          "models"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The hosted page, the same live one, or \"you already own this\"",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "pending": {
                      "type": "boolean"
                    },
                    "ok": {
                      "type": "boolean"
                    },
                    "already": {
                      "type": "boolean"
                    },
                    "model": {
                      "$ref": "#/components/schemas/Model"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ALREADY_YOURS / FREE_MODEL / NOT_FOR_SALE"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Only an owner may spend the house's money"
          },
          "404": {
            "description": "Unknown or another director's personal model"
          },
          "409": {
            "description": "TAKEN (the exclusive went to someone else) / CHECKOUT_PENDING"
          },
          "502": {
            "description": "CHECKOUT_FAILED"
          },
          "503": {
            "description": "BILLING_NOT_CONFIGURED"
          }
        }
      }
    },
    "/api/toolkit/pipelines/preview": {
      "post": {
        "summary": "The number before the click",
        "description": "Compiles the plan against the chosen items and prices every frame, then states the live spendable balance beside it. Reads only — nothing is reserved, nothing is queued.\n",
        "tags": [
          "pipelines"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "plan",
                  "items"
                ],
                "properties": {
                  "plan": {
                    "type": "object",
                    "description": "A pipeline plan { id?, name, nodes }"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The compiled line + affordability",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "compiled": {
                      "type": "object",
                      "description": "{ steps, frames, tokensTotal, skippedTotal }"
                    },
                    "spendable": {
                      "type": "integer"
                    },
                    "affordable": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_PLAN / BAD_ITEMS"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          },
          "422": {
            "description": "LINE_TOO_WIDE { frames, max }"
          }
        }
      }
    },
    "/api/toolkit/pipelines/runs": {
      "get": {
        "summary": "The house's pipeline runs, newest first",
        "tags": [
          "pipelines"
        ],
        "responses": {
          "200": {
            "description": "The runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PipelineRun"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Start the line",
        "description": "Drives either an inline `plan` or a saved `planId`. Reserve BEFORE the job row exists, one hold per step — money first, always. If a concurrent run takes the tokens mid-fan-out, everything this request started is retired and the run is cancelled rather than left half-running.\n",
        "tags": [
          "pipelines"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "items"
                ],
                "properties": {
                  "plan": {
                    "type": "object"
                  },
                  "planId": {
                    "type": "string",
                    "description": "A saved preset; used when `plan` is absent"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "brandId": {
                    "type": "string",
                    "nullable": true
                  },
                  "idempotencyKey": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The line is running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runId": {
                      "type": "string"
                    },
                    "developId": {
                      "type": "string",
                      "description": "The Darkroom's id — run.<runId>"
                    },
                    "started": {
                      "type": "integer"
                    },
                    "compiled": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_PLAN / BAD_ITEMS"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "INSUFFICIENT_TOKENS { need, spendable }"
          },
          "403": {
            "description": "A viewer may not spend the house's tokens"
          },
          "404": {
            "description": "PRESET_NOT_FOUND / ASSET_NOT_FOUND"
          },
          "422": {
            "description": "LINE_TOO_WIDE"
          }
        }
      }
    },
    "/api/toolkit/pipelines/runs/{id}": {
      "get": {
        "summary": "One run, its compiled steps and its plan",
        "tags": [
          "pipelines"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The run",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run": {
                      "$ref": "#/components/schemas/PipelineRun"
                    },
                    "steps": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "plan": {
                      "type": "object",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/pipelines/runs/{id}/cancel": {
      "post": {
        "summary": "Stop the line",
        "description": "The run is flagged FIRST so the scheduler stops enqueueing, then every queued frame is claimed and released. A frame already rendering finishes and still costs. Never implies a refund that is not happening.\n",
        "tags": [
          "pipelines"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What actually stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "cancelled": {
                      "type": "integer"
                    },
                    "cancelledJobIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "stillRendering": {
                      "type": "integer"
                    },
                    "tokensReleased": {
                      "type": "integer"
                    },
                    "run": {
                      "$ref": "#/components/schemas/PipelineRun"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/pipelines/presets": {
      "get": {
        "summary": "Saved plans — a line with no items bound",
        "tags": [
          "pipelines"
        ],
        "responses": {
          "200": {
            "description": "The presets, newest first (100)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "presets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PipelinePreset"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Save a plan as a preset",
        "description": "The body may be `{ plan }` or the plan object itself.",
        "tags": [
          "pipelines"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The saved preset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/PipelinePreset"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_PLAN"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/toolkit/pipelines/presets/{id}": {
      "patch": {
        "summary": "Replace a saved plan",
        "tags": [
          "pipelines"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated preset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/PipelinePreset"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_PLAN"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      },
      "delete": {
        "summary": "Delete a saved plan",
        "tags": [
          "pipelines"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/presets": {
      "get": {
        "summary": "The house's standing preset",
        "description": "NEVER 404s. A director who has never opened brand settings still gets a working preset, so the factory is one drop away.\n",
        "tags": [
          "factory"
        ],
        "responses": {
          "200": {
            "description": "The preset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/HousePreset"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Set the house's standing preset",
        "description": "Every field is validated against the COMPOSER's own vocabulary, so a stored preset can never carry a key the develop routes would reject later — a preset that fails at develop time is a trap.\n",
        "tags": [
          "factory"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "aesthetic",
                  "background",
                  "variationKeys"
                ],
                "properties": {
                  "aesthetic": {
                    "type": "string"
                  },
                  "background": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "preset",
                          "asset"
                        ]
                      },
                      "value": {
                        "type": "string"
                      },
                      "assetId": {
                        "type": "string"
                      }
                    }
                  },
                  "variationKeys": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "modelAssetId": {
                    "type": "string",
                    "nullable": true
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "1k",
                      "2k",
                      "4k"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored preset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/HousePreset"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_AESTHETIC / BAD_BACKGROUND / KEYS_REQUIRED / TOO_MANY_FRAMES"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/factory/plan": {
      "post": {
        "summary": "What the house INTENDS, before a token moves",
        "description": "Groups the dropped pieces into candidate looks using the stamps the smart upload already wrote (a hand-made group beats a shared SKU; a piece with neither is its own look), itemises the whole cost and states the live balance beside it. `modelReady` says up front whether the preset's model still exists — the one thing the house cannot invent. Reads only.\n",
        "tags": [
          "factory"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assetIds"
                ],
                "properties": {
                  "assetIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "1k",
                      "2k",
                      "4k"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The proposal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preset": {
                      "$ref": "#/components/schemas/HousePreset"
                    },
                    "looks": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "truncated": {
                      "type": "boolean"
                    },
                    "modelReady": {
                      "type": "boolean"
                    },
                    "cost": {
                      "type": "object",
                      "properties": {
                        "perFrame": {
                          "type": "integer"
                        },
                        "framesPerLook": {
                          "type": "integer"
                        },
                        "frames": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "quality": {
                          "type": "string"
                        }
                      }
                    },
                    "balance": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / ASSETS_REQUIRED"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          }
        }
      }
    },
    "/api/toolkit/factory/run": {
      "post": {
        "summary": "The one confirm — create the batch of draft looks",
        "description": "SPENDS NOTHING. It creates one DRAFT look per proposed look, stamped with a shared batch marker, and answers with their ids; the caller then walks those ids through the crown's own develop routes, which own every reserve. The body is the EDITED proposal — a dropped look is simply absent, and a look may override the preset's ground or variation set. One bad look rejects the whole confirm rather than leaving half a batch.\n",
        "tags": [
          "factory"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "looks"
                ],
                "properties": {
                  "looks": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "name",
                        "itemAssetIds"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 120
                        },
                        "itemAssetIds": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "background": {
                          "type": "object"
                        },
                        "variationKeys": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "modelAssetId": {
                    "type": "string"
                  },
                  "aesthetic": {
                    "type": "string"
                  },
                  "quality": {
                    "type": "string",
                    "enum": [
                      "1k",
                      "2k",
                      "4k"
                    ]
                  },
                  "brandId": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created batch",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "batchId": {
                      "type": "string"
                    },
                    "quality": {
                      "type": "string"
                    },
                    "looks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "variationKeys": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / LOOKS_REQUIRED / TOO_MANY_LOOKS / MODEL_REQUIRED / BAD_AESTHETIC / NAME_REQUIRED / ITEMS_REQUIRED / TOO_MANY_ITEMS / BAD_ITEM / KEYS_REQUIRED / TOO_MANY_FRAMES / UNKNOWN_VARIATION\n"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          }
        }
      }
    },
    "/api/chat": {
      "post": {
        "summary": "One turn of the loop",
        "description": "The house reads the message against the current draft and answers with what it said plus the mutations it applied. Every applied mutation is ALSO pushed over the job socket the moment it lands (`turnId` is echoed so the tab that asked does not render it twice). Free — a turn never spends tokens.\n",
        "tags": [
          "chat"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "message": {
                    "type": "string"
                  },
                  "draft": {
                    "type": "object"
                  },
                  "history": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "role": {
                          "type": "string",
                          "enum": [
                            "director",
                            "house"
                          ]
                        },
                        "text": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "screen": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "turnId": {
                    "type": "string",
                    "maxLength": 64
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The turn",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string"
                    },
                    "applied": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "mirror": {
                      "type": "object"
                    },
                    "turnId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_MESSAGE / BAD_DRAFT"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "502": {
            "description": "AI_UNAVAILABLE"
          },
          "503": {
            "description": "AI_NOT_CONFIGURED / AI_GATEWAY_NEEDS_BILLING"
          }
        }
      }
    },
    "/api/chat/apply": {
      "post": {
        "summary": "Undo, redo, and any direct edit",
        "description": "ONE door for both directions of the walk — undo posts the inverse, redo posts the mutation. Free by definition: a mutation never spends. A selection can only ever name pieces this house holds.\n",
        "tags": [
          "chat"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "mutation"
                ],
                "properties": {
                  "draft": {
                    "type": "object"
                  },
                  "mutation": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new draft",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "draft": {
                      "type": "object"
                    },
                    "applied": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "BAD_BODY / BAD_DRAFT / BAD_MUTATION"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "ASSET_NOT_FOUND"
          },
          "422": {
            "description": "CANNOT_APPLY { reason }"
          }
        }
      }
    },
    "/posts": {
      "get": {
        "tags": [
          "posts"
        ],
        "responses": {
          "200": {
            "description": "All posts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Post"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "posts"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPost"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created post",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/posts/{id}": {
      "get": {
        "tags": [
          "posts"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The post",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "posts"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchPost"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated post",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — not the author",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "posts"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden — not the author",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/invite/request": {
      "post": {
        "tags": [
          "invite"
        ],
        "summary": "Request an invitation to the house",
        "description": "Public, unauthenticated. Rate-limited per IP. A duplicate address answers 200 with status `already_requested` rather than an error — asking twice is not a fault.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request was recorded (or already was).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, or a disposable email domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/admin/invite-requests": {
      "get": {
        "tags": [
          "admin",
          "invite"
        ],
        "summary": "List invitation requests (admin)",
        "responses": {
          "200": {
            "description": "Every request, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InviteRequestRow"
                      }
                    }
                  },
                  "required": [
                    "requests"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/admin/invite": {
      "post": {
        "tags": [
          "admin",
          "invite"
        ],
        "summary": "Open the door for one email (admin)",
        "description": "Issues the PLATFORM-gate invitation: an `invitations` row plus permanent email-keyed studio access, so the invitee lands usable on signup. Idempotent — re-issuing an open door returns the same token and opens nothing twice. This is NOT the seat invitation (`/api/toolkit/org/seats/invite`).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteIssue"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The door is open for this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteIssueResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/seats/invite": {
      "post": {
        "tags": [
          "organization",
          "invite"
        ],
        "summary": "Invite a teammate into this house",
        "description": "Organization-level. Owner or admin only, and only an owner may offer an owner's seat. Re-inviting the same address supersedes the pending invitation rather than stacking a second one. NOT the platform gate (`/api/toolkit/admin/invite`).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeatInvite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The seat is offered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeatInviteResult"
                }
              }
            }
          },
          "400": {
            "description": "Already a member, or a bad address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "403": {
            "description": "This role may not invite",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/seats/accept": {
      "post": {
        "tags": [
          "organization",
          "invite"
        ],
        "summary": "Take an offered seat",
        "description": "Auth-gated, and the session's email must match the invited one. Writes the `member` row and makes that house the caller's ACTIVE one, so their very next request resolves to it rather than to their personal house.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeatAccept"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The seat is taken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeatAcceptResult"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "403": {
            "description": "This seat is for a different email",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          },
          "404": {
            "description": "No such seat, or it lapsed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteRequestError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/seats/role": {
      "post": {
        "tags": [
          "organization"
        ],
        "summary": "Change what a seat may do",
        "description": "Owner or admin. An admin may only re-seat a `member` or a `viewer` — never an owner, never another admin. `owner` is NOT settable here: handing the house over is `POST /api/toolkit/org/transfer`, which demotes the outgoing owner in the same write so a house never has two.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeatRole"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The seat is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeatRoleResult"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "403": {
            "description": "This seat may not do that",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "404": {
            "description": "Nobody by that id sits in this house",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/seats/{userId}": {
      "delete": {
        "tags": [
          "organization"
        ],
        "summary": "Empty a seat (or leave the house yourself)",
        "description": "Owner or admin — and anyone may remove THEMSELVES, which is how a person leaves. Deletes exactly one `member` row. NOTHING ELSE MOVES: every frame, model, look and collection keys to `organization_id`, and `created_by_user_id` is attribution only, so the work stays with the house and keeps their name on it. The founder of a personal house cannot be removed from it — the house's id is derived from theirs and a lazy repair would silently re-seat them as owner.",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "required": true,
            "name": "userId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The seat is empty. The work stayed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeatRemoveResult"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "403": {
            "description": "This seat may not do that",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "404": {
            "description": "Nobody by that id sits in this house",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          }
        }
      }
    },
    "/api/toolkit/org/transfer": {
      "post": {
        "tags": [
          "organization"
        ],
        "summary": "Hand this house to somebody else",
        "description": "OWNER ONLY, and the one route that can create an `owner`. The outgoing owner is demoted to `admin` in the SAME statement that promotes the new one, so the house never has two owners and never has none. Money follows the seat: `canSpendMoney` is `owner` only, so the plan, top-ups and purchases move with it immediately.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HouseTransfer"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The house has a new owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseTransferResult"
                }
              }
            }
          },
          "401": {
            "description": "No session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "403": {
            "description": "Only an owner may hand the house over",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          },
          "404": {
            "description": "Nobody by that id sits in this house",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseError"
                }
              }
            }
          }
        }
      }
    },
    "/api/keys": {
      "post": {
        "tags": [
          "keys"
        ],
        "summary": "Mint an organisation API key",
        "description": "Returns the raw key ONCE — it is stored only as a SHA-256 hash and cannot be shown again. Session auth only: an API key may not mint another API key.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "member"
                    ],
                    "default": "member"
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new key — `key` is shown only here",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "viewer",
                        "member"
                      ]
                    },
                    "prefix": {
                      "type": "string"
                    },
                    "lastUsedAt": {
                      "type": "string",
                      "nullable": true
                    },
                    "revokedAt": {
                      "type": "string",
                      "nullable": true
                    },
                    "createdAt": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "role",
                    "prefix",
                    "lastUsedAt",
                    "revokedAt",
                    "createdAt",
                    "key"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated, or authenticated with an API key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "A viewer seat may not mint a token-spending key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "keys"
        ],
        "summary": "List the house's API keys",
        "description": "Revoked keys are included (with `revokedAt` set) so the audit trail is visible.",
        "responses": {
          "200": {
            "description": "Every key this organisation has minted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "viewer",
                              "member"
                            ]
                          },
                          "prefix": {
                            "type": "string"
                          },
                          "lastUsedAt": {
                            "type": "string",
                            "nullable": true
                          },
                          "revokedAt": {
                            "type": "string",
                            "nullable": true
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "role",
                          "prefix",
                          "lastUsedAt",
                          "revokedAt",
                          "createdAt"
                        ]
                      }
                    }
                  },
                  "required": [
                    "keys"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated, or authenticated with an API key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/keys/{id}": {
      "delete": {
        "tags": [
          "keys"
        ],
        "summary": "Revoke an API key",
        "description": "Takes effect on the next request — the key row is re-read from Postgres on every call, so there is no cache to wait out. The row is kept, not deleted.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked (idempotent)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "revoked",
                    "id"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated, or authenticated with an API key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such key in this organisation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "post": {
        "tags": [
          "mcp"
        ],
        "summary": "MCP server (Streamable HTTP, JSON responses)",
        "description": "Model Context Protocol endpoint. Send JSON-RPC 2.0: `initialize`, `tools/list`, `tools/call`, `ping`. Authenticate with an organisation API key (`Authorization: Bearer flam_sk_…`) or a session cookie — scope, role and 403s are identical either way. Notifications (no `id`) are answered 202 with an empty body. No SSE: a develop is started here and watched with the `get_develop` tool or the existing jobs WebSocket.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/McpRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The JSON-RPC response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpResponse"
                }
              }
            }
          },
          "202": {
            "description": "A notification was accepted (no body)"
          },
          "401": {
            "description": "No valid API key or session",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}
