openapi: 3.1.0
info:
  title: Nightjar API
  version: 1.0.0
  description: The canonical contract for Nightjar's production public API v1.
  license:
    name: Proprietary
    identifier: LicenseRef-Nightjar-Proprietary
servers:
  - url: https://api.nightjar.so/v1
security:
  - ApiCredential: []
externalDocs:
  description: Nightjar API documentation and compatibility policy
  url: https://docs.nightjar.so/
tags:
  - name: Team
    description: Team Credits and API capabilities.
  - name: Uploads
    description: Direct-upload session creation and finalization.
  - name: Assets
    description: Durable uploaded and generated images.
  - name: Products
    description: Reusable collections of Product Photos.
  - name: Photography Styles
    description: Reusable visual style ingredients.
  - name: Backgrounds
    description: Reusable background ingredients.
  - name: Poses
    description: Reusable pose ingredients.
  - name: Fashion Models
    description: Reusable fashion model ingredients.
  - name: Operations
    description: Asynchronous reusable-ingredient authoring work.
  - name: Generations
    description: Asynchronous creative workflow executions.
paths:
  /team:
    get:
      operationId: retrieveTeam
      summary: Retrieve the credential's Team
      tags: [Team]
      responses:
        "200":
          description: The credential, its Team, Credits, and effective API capabilities.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Team" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
  /uploads:
    post:
      operationId: createUpload
      summary: Create an Upload Session
      tags: [Uploads]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateUploadRequest" }
      responses:
        "201":
          description: One short-lived Upload Session. No Asset exists yet.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UploadSession" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /uploads/{upload_id}/complete:
    parameters:
      - $ref: "#/components/parameters/UploadId"
    post:
      operationId: completeUpload
      summary: Finalize an Upload Session into an Asset
      tags: [Uploads]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CompleteUploadRequest" }
      responses:
        "201":
          description: The complete usable Asset created by finalization.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
            Location: { $ref: "#/components/headers/Location" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Asset" }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "410": { $ref: "#/components/responses/Gone" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /assets:
    get:
      operationId: listAssets
      summary: List Team Assets
      tags: [Assets]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/After"
        - name: source
          in: query
          schema: { type: string, enum: [upload, generation] }
      responses:
        "200":
          description: Team Assets ordered by created_at desc, id desc.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AssetList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
  /assets/{asset_id}:
    parameters:
      - $ref: "#/components/parameters/AssetId"
    get:
      operationId: retrieveAsset
      summary: Retrieve an Asset
      tags: [Assets]
      responses:
        "200":
          description: One active Asset.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Asset" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deleteAsset
      summary: Delete an Asset
      tags: [Assets]
      responses:
        "200":
          description: Deleted from authenticated API and UI use. Every Product Photo membership is removed; affected nonempty Products receive a deterministic replacement Primary Product Photo, and only Products left empty are deleted. Historical Generation output references remain and may resolve to 404.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AssetDeletion" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /products:
    get:
      operationId: listProducts
      summary: List Products
      tags: [Products]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/After"
      responses:
        "200":
          description: Team Products ordered by created_at desc, id desc.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProductList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      operationId: createProduct
      summary: Create a Product
      tags: [Products]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProductWrite" }
      responses:
        "201":
          description: Product created synchronously and atomically.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
            Location: { $ref: "#/components/headers/Location" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Product" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /products/{product_id}:
    parameters:
      - $ref: "#/components/parameters/ProductId"
    get:
      operationId: retrieveProduct
      summary: Retrieve a Product
      tags: [Products]
      responses:
        "200":
          description: One Product.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Product" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    patch:
      operationId: updateProduct
      summary: Update a Product
      tags: [Products]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProductPatch" }
      responses:
        "200":
          description: Updated synchronously and atomically.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Product" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deleteProduct
      summary: Delete a Product
      tags: [Products]
      responses:
        "204":
          description: Permanently removed from API and UI use. Product Photo relationships dissolve; Assets and historical Product Attribution remain.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /photography-styles:
    get:
      operationId: listPhotographyStyles
      summary: List Photography Styles
      tags: [Photography Styles]
      description: Returns every usable Team-owned and global resource by default, ordered together by created_at desc, id desc. Use scope to narrow the collection.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/After"
        - $ref: "#/components/parameters/Scope"
      responses:
        "200":
          description: Team and premade Photography Styles.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PhotographyStyleList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      operationId: createPhotographyStyle
      summary: Create a Photography Style
      tags: [Photography Styles]
      description: Creates a Photography Style asynchronously from exactly three Team Assets. No image is generated, so it costs zero Credits.
      x-nightjar-credit-cost: Free
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PhotographyStyleWrite" }
      responses:
        "202": { $ref: "#/components/responses/OperationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /photography-styles/{photography_style_id}:
    parameters:
      - $ref: "#/components/parameters/PhotographyStyleId"
    get:
      operationId: retrievePhotographyStyle
      summary: Retrieve a Photography Style
      tags: [Photography Styles]
      responses:
        "200":
          description: One Photography Style.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PhotographyStyle" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    patch:
      operationId: updatePhotographyStyle
      summary: Update a Photography Style
      tags: [Photography Styles]
      description: Metadata updates and source replacement are free because no image is generated.
      x-nightjar-credit-cost: Free
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PhotographyStylePatch" }
      responses:
        "200":
          description: Returned only for a metadata-only patch, which commits synchronously and free.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PhotographyStyle" }
        "202": { $ref: "#/components/responses/OperationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deletePhotographyStyle
      summary: Delete a Photography Style
      tags: [Photography Styles]
      responses:
        "204":
          description: Permanently removed from API and UI use.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /backgrounds:
    get:
      operationId: listBackgrounds
      summary: List Backgrounds
      tags: [Backgrounds]
      description: Returns every usable Team-owned and global resource by default, ordered together by created_at desc, id desc. Use scope to narrow the collection.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/After"
        - $ref: "#/components/parameters/Scope"
      responses:
        "200":
          description: Team and premade Backgrounds.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BackgroundList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      operationId: createBackground
      summary: Create a Background
      tags: [Backgrounds]
      description: Creates a Background from one Team Asset. It is free when the scene is already clean and costs exactly one Credit only when foreground-removal image generation runs and commits.
      x-nightjar-credit-cost: 0 or 1 Credit
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BackgroundWrite" }
      responses:
        "202": { $ref: "#/components/responses/OperationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /backgrounds/{background_id}:
    parameters:
      - $ref: "#/components/parameters/BackgroundId"
    get:
      operationId: retrieveBackground
      summary: Retrieve a Background
      tags: [Backgrounds]
      responses:
        "200":
          description: One Background.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Background" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    patch:
      operationId: updateBackground
      summary: Update a Background
      tags: [Backgrounds]
      description: Metadata updates are free. Replacing the source costs one Credit only when foreground-removal image generation runs and commits.
      x-nightjar-credit-cost: 0 or 1 Credit
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BackgroundPatch" }
      responses:
        "200":
          description: Returned only for a metadata-only patch, which commits synchronously and free.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Background" }
        "202": { $ref: "#/components/responses/OperationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deleteBackground
      summary: Delete a Background
      tags: [Backgrounds]
      responses:
        "204":
          description: Permanently removed from API and UI use.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /poses:
    get:
      operationId: listPoses
      summary: List Poses
      tags: [Poses]
      description: Returns every usable Team-owned and global resource by default, ordered together by created_at desc, id desc. Use scope to narrow the collection.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/After"
        - $ref: "#/components/parameters/Scope"
      responses:
        "200":
          description: Team and premade Poses.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PoseList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      operationId: createPose
      summary: Create a Pose
      tags: [Poses]
      description: Creates a Pose from one Team Asset. A new neutral card image is generated, so a successful commit costs exactly one Credit.
      x-nightjar-credit-cost: 1 Credit
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PoseWrite" }
      responses:
        "202": { $ref: "#/components/responses/OperationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /poses/{pose_id}:
    parameters:
      - $ref: "#/components/parameters/PoseId"
    get:
      operationId: retrievePose
      summary: Retrieve a Pose
      tags: [Poses]
      responses:
        "200":
          description: One Pose.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Pose" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    patch:
      operationId: updatePose
      summary: Update a Pose
      tags: [Poses]
      description: Metadata updates are free. Replacing the source generates a new card and costs one Credit on successful commit.
      x-nightjar-credit-cost: 0 or 1 Credit
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PosePatch" }
      responses:
        "200":
          description: Returned only for a metadata-only patch, which commits synchronously and free.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Pose" }
        "202": { $ref: "#/components/responses/OperationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deletePose
      summary: Delete a Pose
      tags: [Poses]
      responses:
        "204":
          description: Permanently removed from API and UI use.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /fashion-models:
    get:
      operationId: listFashionModels
      summary: List Fashion Models
      tags: [Fashion Models]
      description: Returns every usable Team-owned and global resource by default, ordered together by created_at desc, id desc. Use scope to narrow the collection.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/After"
        - $ref: "#/components/parameters/Scope"
      responses:
        "200":
          description: Team and premade Fashion Models.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FashionModelList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      operationId: createFashionModel
      summary: Create a Fashion Model
      tags: [Fashion Models]
      description: Creates a Fashion Model from one to five Team Assets and required metadata. A new identity card is generated, so a successful commit costs exactly one Credit.
      x-nightjar-credit-cost: 1 Credit
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FashionModelWrite" }
      responses:
        "202": { $ref: "#/components/responses/OperationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /fashion-models/{fashion_model_id}:
    parameters:
      - $ref: "#/components/parameters/FashionModelId"
    get:
      operationId: retrieveFashionModel
      summary: Retrieve a Fashion Model
      tags: [Fashion Models]
      responses:
        "200":
          description: One Fashion Model.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FashionModel" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
    patch:
      operationId: updateFashionModel
      summary: Update a Fashion Model
      tags: [Fashion Models]
      description: Metadata updates are free. Replacing source Assets generates a new identity card and costs one Credit on successful commit.
      x-nightjar-credit-cost: 0 or 1 Credit
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FashionModelPatch" }
      responses:
        "200":
          description: Returned only for a metadata-only patch, which commits synchronously and free.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FashionModel" }
        "202": { $ref: "#/components/responses/OperationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
    delete:
      operationId: deleteFashionModel
      summary: Delete a Fashion Model
      tags: [Fashion Models]
      responses:
        "204":
          description: Permanently removed from API and UI use.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /operations:
    get:
      operationId: listOperations
      summary: List authoring Operations
      tags: [Operations]
      description: Returns Team authoring Operations ordered by created_at desc, id desc. Use the filters to recover specific in-flight or historical work.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/After"
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/AsyncStatus" }
        - name: type
          in: query
          schema: { $ref: "#/components/schemas/OperationTypeFilter" }
        - name: target_id
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Matching authoring Operations.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperationList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
  /operations/{operation_id}:
    parameters:
      - $ref: "#/components/parameters/OperationId"
    get:
      operationId: retrieveOperation
      summary: Retrieve an authoring Operation
      tags: [Operations]
      responses:
        "200":
          description: Authoritative authoring lifecycle snapshot.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
            Retry-After:
              description: Suggested polling delay while nonterminal.
              schema: { type: integer }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Operation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
  /generations:
    get:
      operationId: listGenerations
      summary: List Generations
      tags: [Generations]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/After"
        - name: workflow
          in: query
          schema: { $ref: "#/components/schemas/Workflow" }
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/GenerationStatus" }
      responses:
        "200":
          description: Public-API Generations for the Team across all of its API Credentials, ordered by created_at desc, id desc. Web-only Generations that cannot satisfy this contract are excluded.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GenerationList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
  /generations/{generation_id}:
    parameters:
      - $ref: "#/components/parameters/GenerationId"
    get:
      operationId: retrieveGeneration
      summary: Retrieve a Generation
      tags: [Generations]
      responses:
        "200":
          description: Authoritative Generation lifecycle and output snapshot.
          headers:
            Request-Id: { $ref: "#/components/headers/RequestId" }
            Retry-After:
              description: Suggested polling delay while nonterminal.
              schema: { type: integer }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Generation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        default: { $ref: "#/components/responses/Problem" }
  /generations/product-photography:
    post:
      operationId: createProductPhotographyGeneration
      summary: Start Product Photography or a Photoshoot
      tags: [Generations]
      description: Every selected Product and loose Asset is a joint subject in every output. single_shots costs one Credit per requested 1k or 2k image and two Credits per requested 4k image. photoshoot costs two Credits total and produces four output slots. The complete amount is planned and reserved before admission.
      x-nightjar-credit-cost: 1 Credit per generated image
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProductPhotographyRequest" }
            examples:
              controlledSingleShots:
                summary: Two model shots with reusable creative controls
                value:
                  product_ids: [prd_01JPRODUCT]
                  asset_ids: [ast_01JLOOSEANGLE]
                  photography_style_id: sty_01JEDITORIAL
                  background:
                    type: background
                    background_id: bkg_01JWARMSTUDIO
                  fashion_model:
                    type: selected
                    fashion_model_id: mdl_01JALEX
                  pose_id: pos_01JSTANDING
                  camera_distance: medium
                  custom_directions: Keep both products clearly visible.
                  output_mode: single_shots
                  image_count: 2
                  aspect_ratio: "4:5"
                  resolution: 2k
                  output_format: webp
              photoshoot:
                summary: One four-output cohesive Photoshoot
                value:
                  product_ids: [prd_01JPRODUCT]
                  background: { type: automatic }
                  fashion_model: { type: automatic }
                  custom_directions: Keep the set warm and understated.
                  output_mode: photoshoot
                  aspect_ratio: "4:5"
                  resolution: 2k
                  output_format: webp
      responses:
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /generations/edit-images:
    post:
      operationId: createEditImagesGeneration
      summary: Start Edit Images
      tags: [Generations]
      description: All one to eight input Assets jointly guide exactly one edited output. Cost is one Credit at 1k or 2k and two Credits at 4k, planned and reserved before admission.
      x-nightjar-credit-cost: 1 or 2 Credits
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EditImagesRequest" }
            example:
              asset_ids: [ast_01JPRODUCT, ast_01JSCENE]
              instructions: Place the product from image 1 into the scene from image 2.
              aspect_ratio: "4:5"
              resolution: 2k
              output_format: webp
      responses:
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
  /generations/upscale:
    post:
      operationId: createUpscaleGeneration
      summary: Start Upscale
      tags: [Generations]
      description: Produces exactly one upscaled output. Cost is one Credit at 2k and two Credits at 4k, planned and reserved before admission. An Asset already at or above the requested target is rejected synchronously and costs nothing.
      x-nightjar-credit-cost: 1 or 2 Credits
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpscaleRequest" }
            example:
              asset_id: ast_01JPRODUCT
              target_resolution: 2k
              output_format: webp
      responses:
        "202": { $ref: "#/components/responses/GenerationAccepted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        default: { $ref: "#/components/responses/Problem" }
components:
  securitySchemes:
    ApiCredential:
      type: http
      scheme: bearer
      bearerFormat: Nightjar API Credential
  headers:
    RequestId:
      description: Server-generated identifier for this HTTP attempt.
      schema: { type: string }
    Location:
      description: Canonical URL of the admitted resource.
      schema: { type: string }
    Deprecation:
      description: RFC 9745 deprecation date for behavior that remains available during migration.
      schema: { type: string }
    Sunset:
      description: RFC 8594 HTTP-date after which deprecated behavior may become unavailable.
      schema: { type: string, format: http-date }
    Link:
      description: Migration or deprecation documentation using the standard deprecation link relation.
      schema: { type: string }
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Scoped by API Credential, HTTP method, and the concrete canonical path including resolved path parameters, and retained for at least 24 hours. The fingerprint uses normalized request intent rather than raw JSON. Same-key/same-intent replays the original admission status, headers, and body exactly; an in-flight duplicate returns 409 and mismatched reuse returns 422.
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: "^[\\x21-\\x7E]+$"
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    After:
      name: after
      in: query
      schema: { type: string }
    Scope:
      name: scope
      in: query
      description: Omit for every resource the Team can use, or filter to Team-owned or global premade resources.
      schema: { type: string, enum: [all, team, global], default: all }
    UploadId:
      name: upload_id
      in: path
      required: true
      schema: { type: string }
    AssetId:
      name: asset_id
      in: path
      required: true
      schema: { type: string }
    ProductId:
      name: product_id
      in: path
      required: true
      schema: { type: string }
    PhotographyStyleId:
      name: photography_style_id
      in: path
      required: true
      schema: { type: string }
    BackgroundId:
      name: background_id
      in: path
      required: true
      schema: { type: string }
    PoseId:
      name: pose_id
      in: path
      required: true
      schema: { type: string }
    FashionModelId:
      name: fashion_model_id
      in: path
      required: true
      schema: { type: string }
    OperationId:
      name: operation_id
      in: path
      required: true
      schema: { type: string }
    GenerationId:
      name: generation_id
      in: path
      required: true
      schema: { type: string }
  responses:
    Problem:
      description: RFC 9457 error. Request-Id is returned on every response.
      headers:
        Request-Id: { schema: { type: string } }
        Retry-After: { schema: { type: integer } }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    BadRequest:
      description: The request or cursor is malformed. Stable codes are invalid_request and invalid_cursor.
      $ref: "#/components/responses/Problem"
    Unauthorized:
      description: The API Credential is missing, invalid, expired, or revoked. Stable codes are authentication_required, credential_expired, and credential_revoked.
      $ref: "#/components/responses/Problem"
    Forbidden:
      description: The credential is authenticated but its Team, profile, scope, or subscription does not permit this operation. Stable codes are permission_denied and subscription_required.
      $ref: "#/components/responses/Problem"
    PaymentRequired:
      description: The Team has insufficient Credits. The stable code is insufficient_credits.
      $ref: "#/components/responses/Problem"
    NotFound:
      description: The resource does not exist or is not visible to this Team. The stable code is not_found or the documented resource-specific *_not_found code.
      $ref: "#/components/responses/Problem"
    Gone:
      description: The short-lived Upload Session has expired. The stable code is upload_expired.
      $ref: "#/components/responses/Problem"
    Conflict:
      description: The request conflicts with an in-flight idempotency admission, accepted upload bytes, or resource Operation. Stable codes are idempotency_in_progress, upload_bytes_conflict, and resource_operation_in_progress. Every PATCH or DELETE on a resource with a nonterminal source-changing Operation is rejected; that problem includes active_operation_id.
      $ref: "#/components/responses/Problem"
    Unprocessable:
      description: The request shape is valid JSON but violates a field, reference, or Nightjar product invariant. Stable codes include invalid_request, invalid_input, invalid_reference, idempotency_key_reused, and already_at_target_resolution.
      $ref: "#/components/responses/Problem"
    RateLimited:
      description: A transport or Team API Concurrency limit was reached. Stable codes are rate_limited and concurrency_limit_reached. Retry-After is included.
      $ref: "#/components/responses/Problem"
    ServiceUnavailable:
      description: New public API writes are temporarily paused. Reads and already accepted work remain available. Retry-After is included.
      $ref: "#/components/responses/Problem"
    OperationAccepted:
      description: Non-cancelable authoring work accepted.
      headers:
        Request-Id: { $ref: "#/components/headers/RequestId" }
        Location: { $ref: "#/components/headers/Location" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Operation" }
    GenerationAccepted:
      description: Non-cancelable Generation admitted with stable output slots.
      headers:
        Request-Id: { $ref: "#/components/headers/RequestId" }
        Location: { $ref: "#/components/headers/Location" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Generation" }
  schemas:
    Problem:
      type: object
      description: RFC 9457 problem details. Known stable v1 codes are documented per endpoint and in the public error catalog; clients must tolerate unknown codes so Nightjar can add more precise failures without breaking them.
      required: [type, title, status, code, request_id]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        code:
          type: string
          description: Stable snake-case machine code. Core v1 codes include invalid_request, invalid_cursor, invalid_input, invalid_reference, authentication_required, credential_expired, credential_revoked, permission_denied, subscription_required, insufficient_credits, not_found, idempotency_in_progress, idempotency_key_reused, resource_operation_in_progress, upload_expired, upload_bytes_conflict, already_at_target_resolution, concurrency_limit_reached, and rate_limited.
        request_id: { type: string }
        active_operation_id:
          type: string
          description: Present when code is resource_operation_in_progress.
        errors:
          type: array
          items:
            type: object
            required: [field, code, detail]
            properties:
              field:
                type: string
                pattern: "^(|/.*)$"
                description: JSON Pointer to the invalid request field; the empty string denotes the request root.
              code: { type: string }
              detail: { type: string }
    Page:
      type: object
      required: [has_more, next_cursor]
      properties:
        has_more: { type: boolean }
        next_cursor: { type: [string, "null"] }
    Team:
      type: object
      required: [object, id, name, credits, credential, capabilities]
      properties:
        object: { const: team }
        id: { type: string }
        name: { type: string }
        credits: { type: integer, minimum: 0 }
        credential: { $ref: "#/components/schemas/ApiCredentialContext" }
        capabilities:
          $ref: "#/components/schemas/TeamCapabilities"
    ApiCredentialContext:
      type: object
      description: The authenticated credential responsible for this HTTP request. It is included here so integrations can verify which key and permission profile they are using.
      required: [object, id, name, prefix, profile, expires_at]
      properties:
        object: { const: api_credential }
        id: { type: string }
        name: { type: string }
        prefix: { type: string }
        profile: { type: string, enum: [read_only, full] }
        expires_at: { type: [string, "null"], format: date-time }
    TeamCapabilities:
      type: object
      required: [api_access, subscription_active, creative_writes_allowed, api_concurrency]
      properties:
        api_access: { type: boolean }
        subscription_active: { type: boolean }
        creative_writes_allowed:
          type: boolean
          description: Effective for this authenticated credential after both its permission profile and the Team's subscription state are applied.
        api_concurrency:
          type: object
          description: Effective Team-wide public-API Generation concurrency. used never exceeds limit. The initial configured limit is 20, but clients must use the returned value rather than hard-code it.
          required: [limit, used]
          properties:
            limit: { type: integer, minimum: 1 }
            used: { type: integer, minimum: 0 }
    CreateUploadRequest:
      type: object
      additionalProperties: false
      required: [content_type, size_bytes]
      example:
        content_type: image/jpeg
        size_bytes: 184320
      properties:
        content_type:
          type: string
          enum: [image/jpeg, image/png, image/gif, image/webp, image/avif]
          description: Accepted source formats match the web app. Nightjar normalizes the stored Asset to jpeg, png, or webp.
        size_bytes:
          type: integer
          minimum: 1
          maximum: 73400320
          description: Raw source byte length. The maximum is 70 MiB.
    UploadReceipt:
      type: object
      required: [upload_token]
      properties:
        upload_token:
          type: string
          description: Opaque proof that the direct upload endpoint accepted canonical image bytes for this Session.
    UploadSession:
      type: object
      required: [object, id, status, upload]
      properties:
        object: { const: upload }
        id: { type: string }
        status: { const: awaiting_upload }
        upload:
          type: object
          required: [method, url, headers, expires_at, response]
          properties:
            method: { const: POST }
            url: { type: string, format: uri }
            headers:
              type: object
              description: Send every returned header exactly as supplied, including Content-Type and X-Nightjar-Image-Intent. Do not send the API Credential to this signed Worker URL.
              minProperties: 2
              additionalProperties: { type: string }
            expires_at: { type: string, format: date-time }
            response:
              type: object
              required: [content_type, body]
              properties:
                content_type: { const: application/json }
                body: { $ref: "#/components/schemas/UploadReceipt" }
          description: POST the raw image bytes as the request body before expires_at. Success is 200 with UploadReceipt. A byte-identical retry returns the same logical receipt; retrying the Session with different bytes returns 409 upload_bytes_conflict. Invalid/expired intent is 401 or 410, oversized input is 413, unsupported input is 415, and transient service failure is 5xx. Only a successful receipt can finalize the Session.
    CompleteUploadRequest:
      type: object
      additionalProperties: false
      required: [upload_token]
      example:
        upload_token: upl_token_from_direct_upload
      properties:
        upload_token: { type: string }
    Asset:
      type: object
      description: A complete usable image. Its public-by-link URL remains stable while the underlying file exists; after Delete availability is not guaranteed.
      required: [object, id, media_type, source, source_generation_id, format, width, height, size_bytes, url, created_at]
      example:
        object: asset
        id: ast_example
        media_type: image
        source: upload
        source_generation_id: null
        format: jpeg
        width: 1200
        height: 800
        size_bytes: 184320
        url: https://assets.nightjar.so/team/example.jpg
        created_at: '2026-08-16T10:00:00.000Z'
      properties:
        object: { const: asset }
        id: { type: string }
        media_type: { const: image }
        source: { type: string, enum: [upload, generation] }
        source_generation_id:
          type: [string, "null"]
          description: Provenance when the Asset came from a Generation. Only IDs of public-API Generations are guaranteed to be retrievable through /generations/{generation_id}.
        format: { type: string, enum: [jpeg, png, webp] }
        width: { type: integer, minimum: 1 }
        height: { type: integer, minimum: 1 }
        size_bytes: { type: integer, minimum: 1 }
        url: { type: string, format: uri }
        created_at: { type: string, format: date-time }
    AssetList:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Asset" } }
        page: { $ref: "#/components/schemas/Page" }
    AssetDeletion:
      type: object
      required: [object, id, removed_product_memberships, reassigned_primary_product_ids, deleted_product_ids]
      example:
        object: asset_deletion
        id: ast_example
        removed_product_memberships: 2
        reassigned_primary_product_ids: [prd_surviving]
        deleted_product_ids: [prd_left_empty]
      properties:
        object: { const: asset_deletion }
        id: { type: string }
        removed_product_memberships: { type: integer, minimum: 0 }
        reassigned_primary_product_ids:
          type: array
          uniqueItems: true
          items: { type: string }
        deleted_product_ids:
          type: array
          uniqueItems: true
          items: { type: string }
    ProductWrite:
      type: object
      additionalProperties: false
      required: [name, asset_ids, primary_asset_id]
      example:
        name: Field Bag
        description: Waxed canvas with leather handles
        dimensions: 40 × 28 × 12 cm
        asset_ids: [ast_example_front, ast_example_detail]
        primary_asset_id: ast_example_front
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 2000 }
        dimensions: { type: string, maxLength: 240 }
        asset_ids: { type: array, minItems: 1, uniqueItems: true, items: { type: string } }
        primary_asset_id:
          type: string
          description: Must be present in asset_ids.
    ProductPatch:
      type: object
      additionalProperties: false
      minProperties: 1
      description: Product Photos are many-to-many; adding an Asset never moves or copies it. If asset_ids removes the current primary and no replacement is supplied, Nightjar selects a deterministic fallback primary.
      example:
        name: Field Bag · Navy
        asset_ids: [ast_example_front, ast_example_navy]
        primary_asset_id: ast_example_navy
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 2000 }
        dimensions: { type: string, maxLength: 240 }
        asset_ids: { type: array, minItems: 1, uniqueItems: true, items: { type: string } }
        primary_asset_id: { type: string }
    Product:
      type: object
      required: [object, id, name, description, dimensions, asset_ids, primary_asset_id, created_at, updated_at]
      example:
        object: product
        id: prd_example
        name: Field Bag
        description: Waxed canvas with leather handles
        dimensions: 40 × 28 × 12 cm
        asset_ids: [ast_example_front, ast_example_detail]
        primary_asset_id: ast_example_front
        created_at: '2026-08-16T10:00:00.000Z'
        updated_at: '2026-08-16T10:00:00.000Z'
      properties:
        object: { const: product }
        id: { type: string }
        name: { type: string }
        description: { type: string }
        dimensions: { type: string }
        asset_ids: { type: array, minItems: 1, uniqueItems: true, items: { type: string } }
        primary_asset_id: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ProductList:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Product" } }
        page: { $ref: "#/components/schemas/Page" }
    PhotographyStyleWrite:
      type: object
      additionalProperties: false
      required: [name, asset_ids]
      example:
        name: Quiet Editorial
        description: Soft daylight and restrained warm neutrals.
        asset_ids: [ast_style_one, ast_style_two, ast_style_three]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 2000 }
        asset_ids:
          type: array
          minItems: 3
          maxItems: 3
          uniqueItems: true
          items: { type: string }
    PhotographyStylePatch:
      type: object
      additionalProperties: false
      minProperties: 1
      example:
        name: Quiet Editorial · Spring
      description: Metadata-only changes return 200. If asset_ids is present, the entire patch is deferred and commits atomically through an Operation. Photography Style updates generate no image and cost zero Credits.
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 2000 }
        asset_ids:
          type: array
          minItems: 3
          maxItems: 3
          uniqueItems: true
          items: { type: string }
    BackgroundWrite:
      type: object
      additionalProperties: false
      required: [name, kind, asset_id]
      description: V1 authors Backgrounds only from existing Team Assets; text-to-Background candidate generation is outside v1.
      example:
        name: Warm Stone Studio
        kind: backdrop
        asset_id: ast_background_source
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        kind: { type: string, enum: [backdrop, location] }
        asset_id: { type: string }
    BackgroundPatch:
      type: object
      additionalProperties: false
      minProperties: 1
      description: Metadata-only changes return 200. If asset_id is present, the entire patch is deferred and commits atomically through an Operation. It costs zero Credits when the supplied scene is already clean and one Credit only if AI foreground removal runs and commits.
      example:
        name: Warm Stone Studio · Clean
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        kind: { type: string, enum: [backdrop, location] }
        asset_id: { type: string }
    PoseWrite:
      type: object
      additionalProperties: false
      required: [name, asset_id]
      example:
        name: Relaxed Standing
        asset_id: ast_pose_source
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        asset_id: { type: string }
        supported_camera_distances:
          type: array
          minItems: 1
          uniqueItems: true
          items: { $ref: "#/components/schemas/CameraDistance" }
    PosePatch:
      type: object
      additionalProperties: false
      minProperties: 1
      description: Metadata-only changes return 200. If asset_id is present, the entire patch is deferred and commits atomically through an Operation. A committed source replacement generates a new card image and costs one Credit.
      example:
        name: Relaxed Standing · Front
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        asset_id: { type: string }
        supported_camera_distances:
          type: array
          minItems: 1
          uniqueItems: true
          items: { $ref: "#/components/schemas/CameraDistance" }
    FashionModelWrite:
      type: object
      additionalProperties: false
      required: [name, age_range, gender, asset_ids]
      example:
        name: Alex
        age_range: 25-35
        gender: female
        asset_ids: [ast_model_front, ast_model_profile]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        age_range: { $ref: "#/components/schemas/FashionModelAgeRange" }
        gender: { $ref: "#/components/schemas/FashionModelGender" }
        asset_ids:
          type: array
          minItems: 1
          maxItems: 5
          uniqueItems: true
          items: { type: string }
    FashionModelPatch:
      type: object
      additionalProperties: false
      minProperties: 1
      description: Name, age range, and gender metadata changes return 200 and are free. If asset_ids is present, the entire patch is deferred and commits atomically through an Operation; a committed source replacement generates a new identity card and costs one Credit.
      example:
        name: Alex · Summer
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        age_range: { $ref: "#/components/schemas/FashionModelAgeRange" }
        gender: { $ref: "#/components/schemas/FashionModelGender" }
        asset_ids:
          type: array
          minItems: 1
          maxItems: 5
          uniqueItems: true
          items: { type: string }
    ReusableResourceBase:
      type: object
      required: [object, id, scope, name, source_asset_ids, created_at, updated_at]
      description: Global resources are read-only. Team source Asset references are provenance and may later resolve to 404 without disabling the pinned resource version.
      properties:
        object:
          type: string
          enum: [photography_style, background, pose, fashion_model]
        id: { type: string }
        scope: { type: string, enum: [team, global] }
        name: { type: string }
        source_asset_ids:
          description: Admitted source Asset IDs for Team resources. Null for global resources and legacy Team resources whose pre-API source provenance was never stored.
          oneOf:
            - type: array
              minItems: 1
              uniqueItems: true
              items: { type: string }
            - { type: "null" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    PhotographyStyle:
      allOf:
        - $ref: "#/components/schemas/ReusableResourceBase"
        - type: object
          required: [description, preview_image_urls]
          properties:
            object: { const: photography_style }
            description: { type: string }
            preview_image_urls:
              type: array
              minItems: 3
              maxItems: 3
              items: { type: string, format: uri }
    Background:
      allOf:
        - $ref: "#/components/schemas/ReusableResourceBase"
        - type: object
          required: [description, kind, card_image_url]
          properties:
            object: { const: background }
            description: { type: string }
            kind: { type: string, enum: [backdrop, location] }
            card_image_url: { type: string, format: uri }
    Pose:
      allOf:
        - $ref: "#/components/schemas/ReusableResourceBase"
        - type: object
          required: [description, card_image_url, supported_camera_distances]
          properties:
            object: { const: pose }
            description: { type: string }
            card_image_url: { type: string, format: uri }
            supported_camera_distances:
              type: array
              minItems: 1
              uniqueItems: true
              items: { $ref: "#/components/schemas/CameraDistance" }
    FashionModel:
      allOf:
        - $ref: "#/components/schemas/ReusableResourceBase"
        - type: object
          required: [age_range, gender, card_image_url]
          properties:
            object: { const: fashion_model }
            age_range: { $ref: "#/components/schemas/FashionModelAgeRange" }
            gender: { $ref: "#/components/schemas/FashionModelGender" }
            card_image_url: { type: string, format: uri }
    PhotographyStyleList:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/PhotographyStyle" } }
        page: { $ref: "#/components/schemas/Page" }
    BackgroundList:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Background" } }
        page: { $ref: "#/components/schemas/Page" }
    PoseList:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Pose" } }
        page: { $ref: "#/components/schemas/Page" }
    FashionModelList:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/FashionModel" } }
        page: { $ref: "#/components/schemas/Page" }
    CameraDistance:
      type: string
      enum: [close, medium, far]
    FashionModelGender:
      type: string
      enum: [male, female, neutral]
    FashionModelAgeRange:
      type: string
      enum: [18-25, 25-35, 35-45, 45-55, 55+]
    CreditUsage:
      type: object
      required: [reserved, charged]
      properties:
        reserved:
          type: integer
          minimum: 0
          description: Credits held for admitted work. Conditional Background processing may begin at zero until analysis determines whether image generation is required.
        charged:
          type: [integer, "null"]
          minimum: 0
          description: Null while nonterminal; the final settled charge when terminal.
    AsyncStatus:
      type: string
      enum: [queued, processing, completed, failed]
    PublicAsyncError:
      type: object
      required: [code, message, retryable]
      properties:
        code:
          type: string
          description: Known v1 values include temporarily_unavailable, content_policy_violation, invalid_input_asset, insufficient_credits, processing_failed, processing_timed_out, and internal_error. Clients must tolerate unknown future values.
        message: { type: string }
        retryable:
          type: boolean
          description: When true, the customer may submit a new logical request with a new Idempotency-Key. It never means that polling or reusing the original key retries failed work.
    OperationError:
      allOf:
        - $ref: "#/components/schemas/PublicAsyncError"
      description: Authoring Operation failure. Known values include invalid_input_asset, insufficient_credits, processing_failed, processing_timed_out, temporarily_unavailable, and internal_error; clients tolerate unknown codes.
    GenerationOutputError:
      allOf:
        - $ref: "#/components/schemas/PublicAsyncError"
      description: One Generation output failure. Known values include content_policy_violation, invalid_input_asset, processing_failed, processing_timed_out, temporarily_unavailable, and internal_error; clients tolerate unknown codes.
    OperationTypeFilter:
      type: string
      enum: [photography_style.create, photography_style.update, background.create, background.update, pose.create, pose.update, fashion_model.create, fashion_model.update]
    Operation:
      type: object
      description: Non-cancelable authoring lifecycle. Creation failure leaves no partial resource; update failure leaves the previous version unchanged. Create Operations always have target null and update Operations always identify the existing resource. While nonterminal, result, error, and credits.charged are null. On completed, result and credits.charged are non-null and error is null. On failed, result is null while error and credits.charged are non-null. Only successful committed AI-generated images are charged.
      required: [object, id, type, status, request, target, result, error, credits, created_at, updated_at, started_at, finished_at]
      properties:
        object: { const: operation }
        id: { type: string }
        type:
          type: string
          description: Current values are photography_style.create, photography_style.update, background.create, background.update, pose.create, pose.update, fashion_model.create, and fashion_model.update. Clients must tolerate unknown future authoring Operation types in responses.
        status: { $ref: "#/components/schemas/AsyncStatus" }
        request:
          type: object
          description: Normalized accepted public authoring intent.
          additionalProperties: true
        target:
          description: Null for create Operations; the existing resource for update Operations.
          oneOf:
            - { $ref: "#/components/schemas/ResourceReference" }
            - { type: "null" }
        result:
          oneOf:
            - { $ref: "#/components/schemas/ResourceReference" }
            - { type: "null" }
        error:
          oneOf:
            - { $ref: "#/components/schemas/OperationError" }
            - { type: "null" }
        credits: { $ref: "#/components/schemas/CreditUsage" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        finished_at: { type: [string, "null"], format: date-time }
    ResourceReference:
      type: object
      required: [object, id]
      properties:
        object:
          type: string
          enum: [photography_style, background, pose, fashion_model]
        id: { type: string }
    OperationList:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Operation" } }
        page: { $ref: "#/components/schemas/Page" }
    AssetReference:
      type: object
      required: [object, id]
      properties:
        object: { const: asset }
        id: { type: string }
    Workflow:
      type: string
      enum: [product_photography, edit_images, upscale]
    GenerationStatus:
      type: string
      description: Terminal only when every output is terminal. completed means at least one output produced an Asset; failed means none did.
      enum: [queued, processing, completed, failed]
    GenerationOutputStatus:
      type: string
      enum: [pending, processing, completed, failed]
    GenerationOutput:
      type: object
      description: Output slots are created at admission and their IDs, indexes, length, and order never change. Pending/processing outputs have asset and error null; completed outputs have a non-null Asset and null error; failed outputs have null asset and a non-null error. A historical Asset reference remains after Asset deletion and may dereference to 404.
      required: [id, index, status, asset, error]
      properties:
        id: { type: string }
        index: { type: integer, minimum: 0 }
        status: { $ref: "#/components/schemas/GenerationOutputStatus" }
        asset:
          oneOf:
            - { $ref: "#/components/schemas/AssetReference" }
            - { type: "null" }
        error:
          oneOf:
            - { $ref: "#/components/schemas/GenerationOutputError" }
            - { type: "null" }
    OutputSummary:
      type: object
      description: pending + processing + completed + failed always equals total, which also equals outputs.length.
      required: [total, pending, processing, completed, failed]
      properties:
        total: { type: integer, minimum: 1 }
        pending: { type: integer, minimum: 0 }
        processing: { type: integer, minimum: 0 }
        completed: { type: integer, minimum: 0 }
        failed: { type: integer, minimum: 0 }
    GenerationBase:
      type: object
      description: Public-API Generation history is Team-wide across API Credentials. The initiating credential is retained as an immutable audit snapshot even if the credential is later revoked.
      required: [object, id, initiated_by, status, output_summary, outputs, credits, created_at, updated_at, started_at, finished_at]
      properties:
        object: { const: generation }
        id: { type: string }
        initiated_by: { $ref: "#/components/schemas/ApiCredentialReference" }
        status: { $ref: "#/components/schemas/GenerationStatus" }
        output_summary: { $ref: "#/components/schemas/OutputSummary" }
        outputs:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/GenerationOutput" }
        credits: { $ref: "#/components/schemas/CreditUsage" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        finished_at: { type: [string, "null"], format: date-time }
    ApiCredentialReference:
      type: object
      required: [object, id, name, prefix]
      properties:
        object: { const: api_credential }
        id: { type: string }
        name: { type: string }
        prefix: { type: string }
    ProductPhotographyGeneration:
      allOf:
        - $ref: "#/components/schemas/GenerationBase"
        - type: object
          required: [workflow, request]
          properties:
            workflow: { const: product_photography }
            request: { $ref: "#/components/schemas/ProductPhotographyRequest" }
    EditImagesGeneration:
      allOf:
        - $ref: "#/components/schemas/GenerationBase"
        - type: object
          required: [workflow, request]
          properties:
            workflow: { const: edit_images }
            request: { $ref: "#/components/schemas/EditImagesRequest" }
    UpscaleGeneration:
      allOf:
        - $ref: "#/components/schemas/GenerationBase"
        - type: object
          required: [workflow, request]
          properties:
            workflow: { const: upscale }
            request: { $ref: "#/components/schemas/UpscaleRequest" }
    Generation:
      oneOf:
        - $ref: "#/components/schemas/ProductPhotographyGeneration"
        - $ref: "#/components/schemas/EditImagesGeneration"
        - $ref: "#/components/schemas/UpscaleGeneration"
      discriminator:
        propertyName: workflow
        mapping:
          product_photography: "#/components/schemas/ProductPhotographyGeneration"
          edit_images: "#/components/schemas/EditImagesGeneration"
          upscale: "#/components/schemas/UpscaleGeneration"
    GenerationList:
      type: object
      required: [data, page]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Generation" } }
        page: { $ref: "#/components/schemas/Page" }
    AspectRatio:
      type: string
      enum: ["21:9", "1:1", "4:3", "3:2", "2:3", "5:4", "4:5", "3:4", "16:9", "9:16"]
    Resolution:
      type: string
      enum: [1k, 2k, 4k]
    PhotoshootResolution:
      type: string
      enum: [1k, 2k]
    OutputFormat:
      type: string
      enum: [jpeg, png, webp]
    StandardOutputSettings:
      type: object
      required: [aspect_ratio, resolution, output_format]
      properties:
        aspect_ratio: { $ref: "#/components/schemas/AspectRatio" }
        resolution: { $ref: "#/components/schemas/Resolution" }
        output_format: { $ref: "#/components/schemas/OutputFormat" }
    PhotoshootOutputSettings:
      type: object
      required: [aspect_ratio, resolution, output_format]
      properties:
        aspect_ratio: { $ref: "#/components/schemas/AspectRatio" }
        resolution: { $ref: "#/components/schemas/PhotoshootResolution" }
        output_format: { $ref: "#/components/schemas/OutputFormat" }
    AutomaticBackgroundChoice:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type: { const: automatic }
    ColorBackgroundChoice:
      type: object
      additionalProperties: false
      required: [type, color]
      properties:
        type: { const: color }
        color: { type: string, pattern: "^#[0-9A-Fa-f]{6}$" }
    SelectedBackgroundChoice:
      type: object
      additionalProperties: false
      required: [type, background_id]
      properties:
        type: { const: background }
        background_id: { type: string }
    ProductPhotographyBackground:
      oneOf:
        - $ref: "#/components/schemas/AutomaticBackgroundChoice"
        - $ref: "#/components/schemas/ColorBackgroundChoice"
        - $ref: "#/components/schemas/SelectedBackgroundChoice"
      discriminator:
        propertyName: type
        mapping:
          automatic: "#/components/schemas/AutomaticBackgroundChoice"
          color: "#/components/schemas/ColorBackgroundChoice"
          background: "#/components/schemas/SelectedBackgroundChoice"
    NoFashionModelChoice:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type: { const: none }
    AutomaticFashionModelChoice:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type: { const: automatic }
    SelectedFashionModelChoice:
      type: object
      additionalProperties: false
      required: [type, fashion_model_id]
      properties:
        type: { const: selected }
        fashion_model_id: { type: string }
    FashionModelChoice:
      oneOf:
        - $ref: "#/components/schemas/NoFashionModelChoice"
        - $ref: "#/components/schemas/AutomaticFashionModelChoice"
        - $ref: "#/components/schemas/SelectedFashionModelChoice"
      discriminator:
        propertyName: type
        mapping:
          none: "#/components/schemas/NoFashionModelChoice"
          automatic: "#/components/schemas/AutomaticFashionModelChoice"
          selected: "#/components/schemas/SelectedFashionModelChoice"
    Framing:
      type: string
      enum: [eye-level, three-quarter, high-angle, low-angle, overhead, floating, pedestal, propped, macro, ghost-mannequin, dress-form, folded, hanging]
    Shadow:
      type: string
      enum: [none, soft, hard, long, reflection]
    ProductOnlySubjects:
      type: object
      required: [product_ids]
      properties:
        product_ids:
          type: array
          minItems: 1
          maxItems: 5
          uniqueItems: true
          items: { type: string }
        asset_ids:
          type: array
          maxItems: 0
          uniqueItems: true
          items: { type: string }
    AssetOnlySubjects:
      type: object
      required: [asset_ids]
      properties:
        product_ids:
          type: array
          maxItems: 0
          uniqueItems: true
          items: { type: string }
        asset_ids:
          type: array
          minItems: 1
          maxItems: 5
          uniqueItems: true
          items: { type: string }
    OneProductCombinedSubjects:
      type: object
      required: [product_ids, asset_ids]
      properties:
        product_ids:
          type: array
          minItems: 1
          maxItems: 1
          uniqueItems: true
          items: { type: string }
        asset_ids:
          type: array
          minItems: 1
          maxItems: 4
          uniqueItems: true
          items: { type: string }
    TwoProductCombinedSubjects:
      type: object
      required: [product_ids, asset_ids]
      properties:
        product_ids:
          type: array
          minItems: 2
          maxItems: 2
          uniqueItems: true
          items: { type: string }
        asset_ids:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          items: { type: string }
    ThreeProductCombinedSubjects:
      type: object
      required: [product_ids, asset_ids]
      properties:
        product_ids:
          type: array
          minItems: 3
          maxItems: 3
          uniqueItems: true
          items: { type: string }
        asset_ids:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          items: { type: string }
    FourProductCombinedSubjects:
      type: object
      required: [product_ids, asset_ids]
      properties:
        product_ids:
          type: array
          minItems: 4
          maxItems: 4
          uniqueItems: true
          items: { type: string }
        asset_ids:
          type: array
          minItems: 1
          maxItems: 1
          uniqueItems: true
          items: { type: string }
    CombinedSubjects:
      oneOf:
        - $ref: "#/components/schemas/OneProductCombinedSubjects"
        - $ref: "#/components/schemas/TwoProductCombinedSubjects"
        - $ref: "#/components/schemas/ThreeProductCombinedSubjects"
        - $ref: "#/components/schemas/FourProductCombinedSubjects"
      description: The combined number of Product and loose Asset references must not exceed five.
    ProductPhotographySubjects:
      description: At least one subject array must be nonempty and the combined input cap is five. An explicitly empty unused array is equivalent to omission and is removed from the normalized echoed request and idempotency fingerprint. Every selected subject appears together in every output.
      oneOf:
        - $ref: "#/components/schemas/ProductOnlySubjects"
        - $ref: "#/components/schemas/AssetOnlySubjects"
        - $ref: "#/components/schemas/CombinedSubjects"
    ProductPhotographyCommon:
      allOf:
        - $ref: "#/components/schemas/ProductPhotographySubjects"
        - type: object
          required: [background, fashion_model]
          properties:
            photography_style_id:
              type: [string, "null"]
              description: Null and omission are equivalent.
            background: { $ref: "#/components/schemas/ProductPhotographyBackground" }
            fashion_model: { $ref: "#/components/schemas/FashionModelChoice" }
            custom_directions:
              type: [string, "null"]
              maxLength: 4000
              description: Null and omission are equivalent.
    ProductPhotographySingleShotsRequest:
      unevaluatedProperties: false
      allOf:
        - $ref: "#/components/schemas/ProductPhotographyCommon"
        - $ref: "#/components/schemas/StandardOutputSettings"
        - type: object
          required: [output_mode]
          properties:
            output_mode: { const: single_shots }
            image_count:
              type: integer
              minimum: 1
              maximum: 6
              description: Optional; omission is normalized to 2 before admission and in the echoed request.
            pose_id:
              type: [string, "null"]
              description: Null and omission are equivalent.
            framing:
              oneOf:
                - { $ref: "#/components/schemas/Framing" }
                - { type: "null" }
              description: Null and omission are equivalent.
            shadow:
              oneOf:
                - { $ref: "#/components/schemas/Shadow" }
                - { type: "null" }
              description: Null and omission are equivalent.
            camera_distance:
              oneOf:
                - { $ref: "#/components/schemas/CameraDistance" }
                - { type: "null" }
              description: Null and omission are equivalent.
      description: Nightjar enforces the same control-compatibility rules as the web app; incompatible Pose, Framing, Shadow, Fashion Model, Background, and Camera Distance combinations return 422.
    ProductPhotographyPhotoshootRequest:
      unevaluatedProperties: false
      allOf:
        - $ref: "#/components/schemas/ProductPhotographyCommon"
        - $ref: "#/components/schemas/PhotoshootOutputSettings"
        - type: object
          required: [output_mode]
          properties:
            output_mode: { const: photoshoot }
      description: The one Photoshoot capability used by every web and API entry point. It always creates four cohesive output slots from one 2x2 composite and accepts no fixed camera controls or image_count.
    ProductPhotographyRequest:
      oneOf:
        - $ref: "#/components/schemas/ProductPhotographySingleShotsRequest"
        - $ref: "#/components/schemas/ProductPhotographyPhotoshootRequest"
      discriminator:
        propertyName: output_mode
        mapping:
          single_shots: "#/components/schemas/ProductPhotographySingleShotsRequest"
          photoshoot: "#/components/schemas/ProductPhotographyPhotoshootRequest"
    EditImagesRequest:
      unevaluatedProperties: false
      allOf:
        - $ref: "#/components/schemas/StandardOutputSettings"
        - type: object
          required: [asset_ids, instructions]
          properties:
            asset_ids: { type: array, minItems: 1, maxItems: 8, uniqueItems: true, items: { type: string } }
            instructions: { type: string, minLength: 1, maxLength: 4000 }
      description: All input Assets jointly guide exactly one edited output. There is no image_count field.
    UpscaleRequest:
      type: object
      additionalProperties: false
      description: An Asset already meeting the target is rejected synchronously with 422 already_at_target_resolution; no Generation or Credit reservation is created.
      required: [asset_id, target_resolution, output_format]
      properties:
        asset_id: { type: string }
        target_resolution: { type: string, enum: [2k, 4k] }
        output_format: { $ref: "#/components/schemas/OutputFormat" }
