openapi: 3.1.0
info:
  title: Katalo External API
  version: 1.0.0
  summary: Secure server-to-server API for Katalo generation, usage, and billing.
  description: |
    The Katalo External API allows approved API-billing organizations to submit
    a single original property image for staging and pro capture generation workflows,
    poll generation results, request regenerations, and inspect usage and
    billing metrics. This API is intended for trusted server-to-server
    integrations only.
servers:
  - url: https://app.katalo.ai
security:
  - bearerAuth: []
tags:
  - name: Source Assets
  - name: Generations
  - name: Usage
  - name: Billing
x-docs:
  generations:
    intro: |
      The recommended flow is two-step ingest: create a reusable source asset,
      poll until it is ready, then create a generation from `source_asset_id`.
      The generations API also supports transitional inline `file` and
      `source_url` submission, which returns a generation in `ingesting` state
      until the source asset is ready.
    references:
      workflow-model:
        title: Shared workflow model
        description: Public enum values used across create, get, and regenerate.
        items:
          - label: workflow
            value: '`staging` or `pro_capture`.'
          - label: capture_type
            value: '`photo`, `panorama360`, or `floorplan`.'
          - label: mode
            value: Only valid for `staging + photo`. Panorama and floorplan derive mode from `capture_type`.
      room-families:
        title: Room family rules
        description: Multi-room staging requests must stay inside one supported family.
        items:
          - label: Single room
            value: Any canonical room value is valid on its own.
          - label: Open plan
            value: '`Living Room`, `Kitchen`, `Dining Room`.'
          - label: Wet rooms
            value: '`Bathroom`, `Shower`, `Toilet`.'
      style-presets:
        title: Canonical style preset values
        description: The public API accepts only canonical preset ids. `style_preset` is required for `staging + panorama360`, optional for `staging + photo`, and ignored for `staging + photo + mode=clean`.
        values:
          - modern
          - scandi
          - boho
          - country
          - industrial
          - mediterranean
          - luxury
          - eclectic
          - japandia
paths:
  /api/v1/source-assets:
    post:
      tags: [Source Assets]
      summary: Create a source asset ingest job
      description: |
        Submit one original image as either `multipart/form-data` or `source_url`.
        Katalo stages the upload, ingests it asynchronously, deduplicates by
        content hash, and returns an ingest job you can poll until
        `source_asset_id` is available.
      operationId: createSourceAsset
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSourceAssetRequest'
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Source ingest accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SourceAssetIngestResponse'
        '400':
          $ref: '#/components/responses/Error400'
        '401':
          $ref: '#/components/responses/Error401'
        '403':
          $ref: '#/components/responses/Error403'
        '409':
          $ref: '#/components/responses/Error409'
        '422':
          $ref: '#/components/responses/Error422'
        '429':
          $ref: '#/components/responses/Error429'
        '500':
          $ref: '#/components/responses/Error500'
  /api/v1/source-assets/{ingest_id}:
    get:
      tags: [Source Assets]
      summary: Get a source asset ingest job
      description: Poll a previously created ingest job until it reaches `ready` or `failed`.
      operationId: getSourceAsset
      parameters:
        - name: ingest_id
          in: path
          required: true
          schema:
            type: string
            pattern: '^src_[A-Za-z0-9]+$'
      responses:
        '200':
          description: Current ingest state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SourceAssetIngestResponse'
        '401':
          $ref: '#/components/responses/Error401'
        '403':
          $ref: '#/components/responses/Error403'
        '404':
          $ref: '#/components/responses/Error404'
        '429':
          $ref: '#/components/responses/Error429'
        '500':
          $ref: '#/components/responses/Error500'
  /api/v1/generations:
    post:
      tags: [Generations]
      summary: Create a generation job
      description: |
        Submit a new generation job with exactly one source input:
        a single image uploaded as `multipart/form-data`, `source_url`, or
        `source_asset_id`.
        The recommended production pattern is to create a source asset first
        via `POST /api/v1/source-assets` and then pass the resulting
        `source_asset_id` here.
        The webhook signing secret is configured once in organization API settings;
        per-request webhook payloads may only override the callback URL.
        All create requests require an `Idempotency-Key` header.
      operationId: createGeneration
      x-docs:
        purpose: Create a job from one source image. Use the returned `job_id` to poll status or correlate webhook delivery.
        notes:
          - This is the write endpoint for new jobs.
          - The request must include exactly one source input and only the fields that are valid for that workflow and capture type.
        warning:
          title: Input exclusivity
          body: Exactly one of `file`, `source_url`, or `source_asset_id` is allowed in a single request.
        request_examples:
          - label: cURL + JSON
            content_type: application/json
            example: stagePhoto
          - label: Multipart upload
            content_type: multipart/form-data
            example: uploadPhoto
        response_example:
          status: '200'
          example: queued
        request_fields:
          - name: Authorization
            in: header
            type: bearer token
            necessity: required
            description: Organization-scoped API key in `Bearer <token>` format.
          - name: Idempotency-Key
            in: header
            type: string
            necessity: required
            description: Replay-safe write key. Reusing the same key with the same payload returns the original job instead of creating a duplicate.
          - name: file
            in: multipart
            type: binary image
            necessity: conditional
            description: Upload one source image directly to Katalo.
            notes: Exactly one of `file`, `source_url`, or `source_asset_id` must be provided.
          - name: source_url
            in: body
            type: string (uri)
            necessity: conditional
            description: HTTPS URL that Katalo can fetch as the original image.
            notes: Exactly one of `file`, `source_url`, or `source_asset_id` must be provided.
          - name: source_asset_id
            in: body
            type: string
            necessity: conditional
            description: Existing Katalo source asset id to reuse as the input image.
            notes: Exactly one of `file`, `source_url`, or `source_asset_id` must be provided.
          - name: workflow
            in: body
            type: enum
            necessity: required
            description: Workflow to run for this job.
            notes: See `#workflow-model`.
          - name: capture_type
            in: body
            type: enum
            necessity: required
            description: Capture format for this request.
            notes: See `#workflow-model`.
          - name: mode
            in: body
            type: enum
            necessity: conditional
            description: Staging mode for photo jobs.
            notes: Required for `workflow=staging` and `capture_type=photo`, including `mode=clean`. Not allowed for `pro_capture`, `panorama360`, or `floorplan`.
          - name: room_types
            in: body
            type: string[]
            necessity: conditional
            description: Canonical room labels for the staged scene.
            notes: Required for `workflow=staging` and `capture_type=photo` unless `mode=clean`. Ignored for `staging + photo + mode=clean`. Not allowed for `pro_capture`, `panorama360`, or `floorplan`. See `#room-families`.
          - name: style_preset
            in: body
            type: enum
            necessity: conditional
            description: Canonical style preset id.
            notes: Required for `staging + panorama360`. Optional for `staging + photo` except ignored when `mode=clean`. Not allowed for `pro_capture`. See `#style-presets`.
          - name: include_alternates
            in: body
            type: boolean
            necessity: optional
            description: Return the primary approved output plus at most one approved alternate.
            notes: Never exposes rejected or internal candidates.
          - name: reference
            in: body
            type: string
            necessity: optional
            description: Your own stable identifier for the source image or listing context.
          - name: metadata
            in: body
            type: object
            necessity: optional
            description: Arbitrary key-value metadata echoed back in responses and webhooks.
            notes: Maximum 20 properties. Values may be string, number, boolean, or null.
          - name: webhook
            in: body
            type: object
            necessity: optional
            description: Per-request callback URL override.
            notes: Only `url` is accepted per request. The signing secret is managed at the organization level.
        response_fields:
          - name: job_id
            type: string
            description: Public id for the new job.
          - name: parent_job_id
            type: string | null
            description: Parent job id when the job was created via regenerate.
          - name: source_asset_id
            type: string | null
            description: Public source asset id tied to the uploaded or referenced image. Null only while `status=ingesting`.
          - name: status
            type: enum
            description: '`ingesting`, `queued`, `running`, `succeeded`, or `failed`.'
          - name: workflow
            type: enum
            description: Stored workflow in public API form.
          - name: capture_type
            type: enum
            description: Stored capture type in public API form.
          - name: mode
            type: string | null
            description: Returned mode. For staging panorama and floorplan jobs this equals `capture_type`. For `pro_capture` it is null.
          - name: room_types
            type: string[]
            description: Canonical room labels stored for the job. Empty for clean jobs.
          - name: style_preset
            type: enum | null
            description: Canonical style preset stored for the job. Null for clean jobs.
          - name: include_alternates
            type: boolean
            description: Whether the response may include one approved alternate output in addition to the primary output.
          - name: reference
            type: string | null
            description: Your original identifier echoed back unchanged.
          - name: metadata
            type: object | null
            description: Your original metadata echoed back unchanged.
          - name: failure
            type: object | null
            description: Terminal failure details with `code`, `message`, and `retryable`.
          - name: outputs
            type: GenerationOutput[]
            description: Primary approved output first, plus at most one approved alternate when `include_alternates=true`.
          - name: created_at
            type: datetime
            description: Job creation timestamp in ISO 8601 format.
          - name: completed_at
            type: datetime | null
            description: Completion timestamp in ISO 8601 format when the job reaches a terminal state.
          - name: updated_at
            type: datetime
            description: Last job update timestamp in ISO 8601 format.
        errors:
          - status: '400'
            code: bad_request
            description: Malformed JSON, invalid multipart, or mutually exclusive source inputs.
          - status: '401'
            code: unauthorized
            description: Missing or malformed bearer token.
          - status: '403'
            code: forbidden
            description: API access is disabled or the key does not belong to an approved organization.
          - status: '409'
            code: idempotency_conflict
            description: The same `Idempotency-Key` was reused with a different payload.
          - status: '422'
            code: validation_error
            description: Request fields fail the workflow and capture-type validation matrix.
          - status: '429'
            code: rate_limited
            description: Per-key, per-org, or per-IP limit exceeded.
          - status: '500'
            code: internal_error
            description: Unexpected server-side failure.
        references:
          - workflow-model
          - room-families
          - style-presets
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/CreateGenerationWithSourceUrlRequest'
                - $ref: '#/components/schemas/CreateGenerationWithSourceAssetRequest'
            examples:
              stagePhoto:
                summary: Stage a photo using a source URL
                value:
                  source_url: https://cdn.example.com/listings/empty-living-room.jpg
                  workflow: staging
                  capture_type: photo
                  mode: refurnish
                  room_types:
                    - Living Room
                    - Kitchen
                  style_preset: modern
                  include_alternates: true
                  reference: listing-123-lr-1
                  metadata:
                    listing_id: listing-123
                    partner: acme
                  webhook:
                    url: https://partner.example.com/webhooks/katalo
              panorama:
                summary: Stage a panorama
                value:
                  source_url: https://cdn.example.com/listings/panorama.jpg
                  workflow: staging
                  capture_type: panorama360
                  style_preset: modern
                  reference: listing-123-pano-1
              cleanPhoto:
                summary: Clean a staged photo without room or style fields
                value:
                  source_asset_id: src_01JXABCDEF0123456789ABCDEF
                  workflow: staging
                  capture_type: photo
                  mode: clean
                  reference: listing-123-clean-1
              floorPlan:
                summary: Stage a floor plan
                value:
                  source_asset_id: src_01JXABCDEF0123456789ABCDEF
                  workflow: staging
                  capture_type: floorplan
                  reference: listing-123-floorplan-1
              proCapture:
                summary: Run a pro capture job
                value:
                  source_url: https://cdn.example.com/listings/kitchen.jpg
                  workflow: pro_capture
                  capture_type: photo
                  reference: listing-123-kitchen-pro
          multipart/form-data:
            schema:
              allOf:
                - $ref: '#/components/schemas/CreateGenerationMultipartFields'
                - type: object
                  required: [file]
                  properties:
                    file:
                      type: string
                      format: binary
                      description: Single image file to upload.
            examples:
              uploadPhoto:
                summary: Stage a photo from a direct file upload
                value:
                  workflow: staging
                  capture_type: photo
                  mode: refurnish
                  room_types:
                    - Living Room
                    - Kitchen
                  style_preset: modern
                  include_alternates: true
                  reference: listing-123-lr-1
                  metadata:
                    listing_id: listing-123
                  webhook:
                    url: https://partner.example.com/webhooks/katalo
            encoding:
              metadata:
                contentType: application/json
              webhook:
                contentType: application/json
      responses:
        '200':
          description: Generation job accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationResponse'
              examples:
                queued:
                  value:
                    job_id: job_01JXJOBABCDEFGHIJKL0123456
                    parent_job_id: null
                    source_asset_id: src_01JXSRCABCDEFGHIJKL012345
                    status: queued
                    workflow: staging
                    capture_type: photo
                    mode: refurnish
                    room_types: [Living Room, Kitchen]
                    style_preset: modern
                    include_alternates: true
                    reference: listing-123-lr-1
                    metadata:
                      listing_id: listing-123
                    failure: null
                    created_at: '2026-04-15T10:11:12.000Z'
                    completed_at: null
                    updated_at: '2026-04-15T10:11:12.000Z'
                    outputs: []
        '400':
          $ref: '#/components/responses/Error400'
        '401':
          $ref: '#/components/responses/Error401'
        '403':
          $ref: '#/components/responses/Error403'
        '409':
          $ref: '#/components/responses/Error409'
        '422':
          $ref: '#/components/responses/Error422'
        '429':
          $ref: '#/components/responses/Error429'
        '500':
          $ref: '#/components/responses/Error500'
  /api/v1/generations/{job_id}:
    get:
      tags: [Generations]
      summary: Get a generation job
      operationId: getGeneration
      x-docs:
        purpose: Read job state by public id. Use this endpoint for polling, recovery, or to fetch fresh signed output URLs.
        notes:
          - Use this as the polling endpoint after create or regenerate returns a `job_id`.
          - Signed output URLs are temporary. Re-read the job if a URL has expired.
        warning:
          title: Temporary output URLs
          body: '`image_url` values are signed and short-lived. Persist the asset in your own storage if your workflow needs longer retention.'
        request_examples:
          - label: cURL
            path_params:
              job_id: job_01JXJOBABCDEFGHIJKL0123456
        response_example:
          status: '200'
          example: succeeded
        request_fields:
          - name: Authorization
            in: header
            type: bearer token
            necessity: required
            description: Organization-scoped API key in `Bearer <token>` format.
          - name: job_id
            in: path
            type: string
            necessity: required
            description: Public job id returned by create or regenerate.
        response_fields:
          - name: job_id
            type: string
            description: Public id for the job.
          - name: parent_job_id
            type: string | null
            description: Parent job id when the job was created via regenerate.
          - name: source_asset_id
            type: string
            description: Public source asset id tied to the original input image.
          - name: status
            type: enum
            description: '`queued`, `running`, `succeeded`, or `failed`.'
          - name: workflow
            type: enum
            description: Stored workflow in public API form.
          - name: capture_type
            type: enum
            description: Stored capture type in public API form.
          - name: mode
            type: string | null
            description: Returned mode. For staging panorama and floorplan jobs this equals `capture_type`. For `pro_capture` it is null.
          - name: room_types
            type: string[]
            description: Canonical room labels stored for the job. Empty for clean jobs.
          - name: style_preset
            type: enum | null
            description: Canonical style preset stored for the job. Null for clean jobs.
          - name: include_alternates
            type: boolean
            description: Whether the response may include one approved alternate output in addition to the primary output.
          - name: reference
            type: string | null
            description: Your original identifier echoed back unchanged.
          - name: metadata
            type: object | null
            description: Your original metadata echoed back unchanged.
          - name: failure
            type: object | null
            description: Terminal failure details with `code`, `message`, and `retryable`.
          - name: outputs
            type: GenerationOutput[]
            description: Primary approved output first, plus at most one approved alternate when `include_alternates=true`.
          - name: created_at
            type: datetime
            description: Job creation timestamp in ISO 8601 format.
          - name: completed_at
            type: datetime | null
            description: Completion timestamp in ISO 8601 format when the job reaches a terminal state.
          - name: updated_at
            type: datetime
            description: Last job update timestamp in ISO 8601 format.
        errors:
          - status: '401'
            code: unauthorized
            description: Missing or malformed bearer token.
          - status: '403'
            code: forbidden
            description: API access is disabled or the key does not belong to an approved organization.
          - status: '404'
            code: not_found
            description: No job with that public id exists for the organization.
          - status: '429'
            code: rate_limited
            description: Per-key, per-org, or per-IP limit exceeded.
          - status: '500'
            code: internal_error
            description: Unexpected server-side failure.
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Generation job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationResponse'
              examples:
                succeeded:
                  value:
                    job_id: job_01JXJOBABCDEFGHIJKL0123456
                    parent_job_id: null
                    source_asset_id: src_01JXSRCABCDEFGHIJKL012345
                    status: succeeded
                    workflow: staging
                    capture_type: photo
                    mode: refurnish
                    room_types: [Living Room, Kitchen]
                    style_preset: modern
                    include_alternates: true
                    reference: listing-123-lr-1
                    metadata:
                      listing_id: listing-123
                    failure: null
                    created_at: '2026-04-15T10:11:12.000Z'
                    completed_at: '2026-04-15T10:11:58.000Z'
                    updated_at: '2026-04-15T10:11:58.000Z'
                    outputs:
                      - output_id: out_01JXOUTABCDEFGHIJKL012345
                        rank: 1
                        is_primary: true
                        image_url: https://utfs.io/f/example-primary?signed=1
                        expires_at: '2026-04-15T10:26:58.000Z'
                        cached: false
                        created_at: '2026-04-15T10:11:58.000Z'
                      - output_id: out_01JXOUTABCDEFGHIJKL012346
                        rank: 2
                        is_primary: false
                        image_url: https://utfs.io/f/example-alt?signed=1
                        expires_at: '2026-04-15T10:26:58.000Z'
                        cached: true
                        created_at: '2026-04-15T10:11:58.000Z'
        '401':
          $ref: '#/components/responses/Error401'
        '403':
          $ref: '#/components/responses/Error403'
        '404':
          $ref: '#/components/responses/Error404'
        '429':
          $ref: '#/components/responses/Error429'
        '500':
          $ref: '#/components/responses/Error500'
  /api/v1/generations/{job_id}/regenerate:
    post:
      tags: [Generations]
      summary: Regenerate from the same source image
      description: |
        Creates a fresh job from the same `source_asset_id` and bypasses approved
        cache consumption so clients can request more variations or retry a
        failed or suspect result.
        Override rules depend on the parent job:
        - `staging` + `photo`: `mode`, `reference`, `metadata`, and `webhook` are allowed. `room_types` and `style_preset` are allowed for `refurnish` and `renovate`, and ignored for `clean`.
        - `staging` + `panorama360`: `style_preset`, `reference`, `metadata`, and `webhook` are allowed, and `style_preset` is required.
        - `staging` + `floorplan`: `style_preset`, `reference`, `metadata`, and `webhook` are allowed.
        - `pro_capture`: only `reference`, `metadata`, and `webhook` are allowed.
      operationId: regenerateGeneration
      x-docs:
        purpose: Create a new job from the same source asset when you need another approved output or another pass from the same original image.
        notes:
          - Regenerate always reuses the source asset from the parent job and returns a new public `job_id`.
          - Use it for retries, alternate approved outputs, or a follow-up pass from the same original image.
        warning:
          title: Override limits
          body: Allowed override fields depend on the parent job’s `workflow` and `capture_type`.
        request_examples:
          - label: cURL + JSON
            content_type: application/json
            path_params:
              job_id: job_01JXJOBABCDEFGHIJKL0123456
            example: redesign
        response_example:
          status: '200'
          example: queued
        request_fields:
          - name: Authorization
            in: header
            type: bearer token
            necessity: required
            description: Organization-scoped API key in `Bearer <token>` format.
          - name: Idempotency-Key
            in: header
            type: string
            necessity: required
            description: Replay-safe write key. Reusing the same key with the same payload returns the original job instead of creating a duplicate.
          - name: job_id
            in: path
            type: string
            necessity: required
            description: Public job id to regenerate from.
          - name: file
            in: body
            type: binary image
            necessity: not allowed
            description: Regenerate always reuses the original source image from the parent job.
          - name: source_url
            in: body
            type: string (uri)
            necessity: not allowed
            description: Regenerate always reuses the original source image from the parent job.
          - name: source_asset_id
            in: body
            type: string
            necessity: not allowed
            description: Regenerate always reuses the original source asset from the parent job.
          - name: mode
            in: body
            type: enum
            necessity: conditional
            description: Override the staging mode for a parent `staging + photo` job.
            notes: Not allowed for `pro_capture`, `panorama360`, or `floorplan`.
          - name: room_types
            in: body
            type: string[]
            necessity: conditional
            description: Override the staged room labels for a parent `staging + photo` job.
            notes: Not allowed for `pro_capture`, `panorama360`, or `floorplan`. Ignored when the effective photo mode is `clean`. See `#room-families`.
          - name: style_preset
            in: body
            type: enum
            necessity: conditional
            description: Override the canonical style preset for staging jobs.
            notes: Required for `staging + panorama360`. Optional for `staging + photo` except ignored when the effective photo mode is `clean`. Not allowed for `pro_capture`. See `#style-presets`.
          - name: reference
            in: body
            type: string
            necessity: optional
            description: New external identifier to associate with the regenerated job.
          - name: metadata
            in: body
            type: object
            necessity: optional
            description: New arbitrary metadata echoed back in responses and webhooks.
            notes: Maximum 20 properties. Values may be string, number, boolean, or null.
          - name: webhook
            in: body
            type: object
            necessity: optional
            description: Per-request callback URL override.
            notes: Only `url` is accepted per request. The signing secret is managed at the organization level.
        response_fields:
          - name: job_id
            type: string
            description: New public id for the regeneration request.
          - name: parent_job_id
            type: string
            description: Public job id of the source job this regeneration was created from.
          - name: source_asset_id
            type: string
            description: Public source asset id reused from the original job.
          - name: status
            type: enum
            description: '`queued`, `running`, `succeeded`, or `failed`.'
          - name: workflow
            type: enum
            description: Stored workflow in public API form.
          - name: capture_type
            type: enum
            description: Stored capture type in public API form.
          - name: mode
            type: string | null
            description: Returned mode. For staging panorama and floorplan jobs this equals `capture_type`. For `pro_capture` it is null.
          - name: room_types
            type: string[]
            description: Canonical room labels stored for the regenerated job. Empty for clean jobs.
          - name: style_preset
            type: enum | null
            description: Canonical style preset stored for the regenerated job. Null for clean jobs.
          - name: include_alternates
            type: boolean
            description: Whether the response may include one approved alternate output in addition to the primary output.
          - name: reference
            type: string | null
            description: Your override identifier echoed back unchanged.
          - name: metadata
            type: object | null
            description: Your override metadata echoed back unchanged.
          - name: failure
            type: object | null
            description: Terminal failure details with `code`, `message`, and `retryable`.
          - name: outputs
            type: GenerationOutput[]
            description: Primary approved output first, plus at most one approved alternate when `include_alternates=true`.
          - name: created_at
            type: datetime
            description: Job creation timestamp in ISO 8601 format.
          - name: completed_at
            type: datetime | null
            description: Completion timestamp in ISO 8601 format when the job reaches a terminal state.
          - name: updated_at
            type: datetime
            description: Last job update timestamp in ISO 8601 format.
        errors:
          - status: '400'
            code: bad_request
            description: Malformed JSON or unsupported overrides.
          - status: '401'
            code: unauthorized
            description: Missing or malformed bearer token.
          - status: '403'
            code: forbidden
            description: API access is disabled or the key does not belong to an approved organization.
          - status: '404'
            code: not_found
            description: No job with that public id exists for the organization.
          - status: '409'
            code: idempotency_conflict
            description: The same `Idempotency-Key` was reused with a different payload.
          - status: '422'
            code: validation_error
            description: Override fields are not valid for the parent workflow and capture type.
          - status: '429'
            code: rate_limited
            description: Per-key, per-org, or per-IP limit exceeded.
          - status: '500'
            code: internal_error
            description: Unexpected server-side failure.
        references:
          - workflow-model
          - room-families
          - style-presets
      parameters:
        - $ref: '#/components/parameters/JobId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegenerateGenerationRequest'
            examples:
              simpleRetry:
                summary: Retry without changing parameters
                value: {}
              redesign:
                summary: Retry with a new style and room combo
                value:
                  mode: renovate
                  room_types:
                    - Living Room
                    - Dining Room
                  style_preset: mediterranean
                  reference: listing-123-lr-redesign-2
                  metadata:
                    note: second pass
              cleanRetry:
                summary: Retry as clean without room or style overrides
                value:
                  mode: clean
                  reference: listing-123-clean-2
      responses:
        '200':
          description: Regeneration job accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationResponse'
              examples:
                queued:
                  value:
                    job_id: job_01JXJOBABCDEFGHIJKL0123457
                    parent_job_id: job_01JXJOBABCDEFGHIJKL0123456
                    source_asset_id: src_01JXSRCABCDEFGHIJKL012345
                    status: queued
                    workflow: staging
                    capture_type: photo
                    mode: renovate
                    room_types: [Living Room, Dining Room]
                    style_preset: mediterranean
                    include_alternates: true
                    reference: listing-123-lr-redesign-2
                    metadata:
                      note: second pass
                    failure: null
                    created_at: '2026-04-15T10:31:12.000Z'
                    completed_at: null
                    updated_at: '2026-04-15T10:31:12.000Z'
                    outputs: []
        '400':
          $ref: '#/components/responses/Error400'
        '401':
          $ref: '#/components/responses/Error401'
        '403':
          $ref: '#/components/responses/Error403'
        '404':
          $ref: '#/components/responses/Error404'
        '409':
          $ref: '#/components/responses/Error409'
        '422':
          $ref: '#/components/responses/Error422'
        '429':
          $ref: '#/components/responses/Error429'
        '500':
          $ref: '#/components/responses/Error500'
  /api/v1/usage/summary:
    get:
      tags: [Usage]
      summary: Get current billing-period usage summary
      operationId: getUsageSummary
      responses:
        '200':
          description: Current billing period API usage summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageSummaryResponse'
              examples:
                default:
                  value:
                    access:
                      enabled: true
                      status: active
                      plan: enterprise
                    billing_period:
                      start_at: '2026-04-01T00:00:00.000Z'
                      end_at: '2026-04-30T23:59:59.999Z'
                    usage:
                      included: 100
                      consumed: 42
                      overage: 0
                      remaining: 58
                      projected_overage_amount_usd: 0
                    jobs:
                      total: 18
                      succeeded: 16
                      failed: 2
                      outputs_returned: 21
                      regenerate_count: 4
                      cache_hit_count: 3
                    rate_limits:
                      create_generation_per_minute: 120
                      regenerate_generation_per_minute: 120
                      get_generation_per_minute: 1200
                      usage_summary_per_minute: 120
                      billing_events_per_minute: 120
                      source_url_fetch_per_minute: 60
                      max_concurrent_jobs: 50
        '401':
          $ref: '#/components/responses/Error401'
        '403':
          $ref: '#/components/responses/Error403'
        '429':
          $ref: '#/components/responses/Error429'
        '500':
          $ref: '#/components/responses/Error500'
  /api/v1/billing/events:
    get:
      tags: [Billing]
      summary: List API-originated billing events
      operationId: listBillingEvents
      parameters:
        - in: query
          name: cursor
          schema:
            type: string
          required: false
          description: Opaque pagination cursor returned by a previous response.
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
          required: false
      responses:
        '200':
          description: Cursor-paginated API billing ledger
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingEventsResponse'
              examples:
                page:
                  value:
                    items:
                      - event_id: use_01JXABCDEFGHIJKL0123456789
                        occurred_at: '2026-04-14T10:11:12.000Z'
                        job_id: job_01JXJOBABCDEFGHIJKL0123456
                        source_asset_id: src_01JXSRCABCDEFGHIJKL012345
                        workflow: staging
                        capture_type: photo
                        mode: refurnish
                        room_types:
                          - Living Room
                          - Kitchen
                        style_preset: modern
                        usage_type: image_generation
                        config_fingerprint: 8783d76516c5d2fc9345f4f1b67889be
                        billable: true
                        unit_price_usd: 15
                        amount_usd: 15
                    next_cursor: null
        '401':
          $ref: '#/components/responses/Error401'
        '403':
          $ref: '#/components/responses/Error403'
        '429':
          $ref: '#/components/responses/Error429'
        '500':
          $ref: '#/components/responses/Error500'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        Use an organization-scoped API key issued by the Katalo billing UI.
        Format: `kat_live_<publicKeyId>.<secret>`.
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 200
      description: Required for write endpoints. Replays with the same body return the original job.
    JobId:
      in: path
      name: job_id
      required: true
      schema:
        type: string
        pattern: '^job_[A-Za-z0-9]+$'
  responses:
    Error400:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Error401:
      description: Authentication failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Error403:
      description: Access denied
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Error404:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Error409:
      description: Idempotency conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Error422:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Error429:
      description: Rate limited
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until retry is permitted.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Error500:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    CreateGenerationMultipartFields:
      type: object
      required:
        - workflow
        - capture_type
      properties:
        workflow:
          $ref: '#/components/schemas/Workflow'
        capture_type:
          $ref: '#/components/schemas/CaptureType'
        mode:
          $ref: '#/components/schemas/Mode'
        room_types:
          oneOf:
            - type: array
              items:
                $ref: '#/components/schemas/RoomType'
            - type: string
              description: JSON array string or comma-separated room types from the canonical room list.
          description: |
            Valid room labels are listed in RoomType.
            Required when `workflow=staging`, `capture_type=photo`, and `mode` is not `clean`.
            Ignored when `workflow=staging`, `capture_type=photo`, and `mode=clean`.
            Not allowed for `workflow=pro_capture`, `capture_type=panorama360`, or `capture_type=floorplan`.
            When sending more than one room, keep the combination within a
            single supported family:
            - Open plan: Living Room, Kitchen, Dining Room
            - Wet rooms: Bathroom, Shower, Toilet
        style_preset:
          $ref: '#/components/schemas/StylePreset'
        include_alternates:
          type: boolean
          default: false
          description: Return the primary approved output plus at most one approved alternate. Never exposes rejected/internal candidates.
        reference:
          type: string
          maxLength: 200
          description: Your own stable identifier for the source image or listing context. Echoed back in responses and webhooks.
        metadata:
          oneOf:
            - $ref: '#/components/schemas/Metadata'
            - type: string
              description: JSON object string.
        webhook:
          oneOf:
            - $ref: '#/components/schemas/WebhookConfig'
            - type: string
              description: JSON object string.
    CreateGenerationWithSourceUrlRequest:
      type: object
      required:
        - source_url
        - workflow
        - capture_type
      properties:
        source_url:
          type: string
          format: uri
        workflow:
          $ref: '#/components/schemas/Workflow'
        capture_type:
          $ref: '#/components/schemas/CaptureType'
        mode:
          $ref: '#/components/schemas/Mode'
        room_types:
          $ref: '#/components/schemas/RoomTypes'
        style_preset:
          $ref: '#/components/schemas/StylePreset'
        include_alternates:
          type: boolean
          default: false
          description: Return the primary approved output plus at most one approved alternate. Never exposes rejected/internal candidates.
        reference:
          type: string
          maxLength: 200
          description: Your own stable identifier for the source image or listing context. Echoed back in responses and webhooks.
        metadata:
          $ref: '#/components/schemas/Metadata'
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
    CreateSourceAssetRequest:
      type: object
      required:
        - source_url
      properties:
        source_url:
          type: string
          format: uri
    CreateGenerationWithSourceAssetRequest:
      type: object
      required:
        - source_asset_id
        - workflow
        - capture_type
      properties:
        source_asset_id:
          type: string
          pattern: '^src_[A-Za-z0-9]+$'
        workflow:
          $ref: '#/components/schemas/Workflow'
        capture_type:
          $ref: '#/components/schemas/CaptureType'
        mode:
          $ref: '#/components/schemas/Mode'
        room_types:
          $ref: '#/components/schemas/RoomTypes'
        style_preset:
          $ref: '#/components/schemas/StylePreset'
        include_alternates:
          type: boolean
          default: false
          description: Return the primary approved output plus at most one approved alternate. Never exposes rejected/internal candidates.
        reference:
          type: string
          maxLength: 200
          description: Your own stable identifier for the source image or listing context. Echoed back in responses and webhooks.
        metadata:
          $ref: '#/components/schemas/Metadata'
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
    RegenerateGenerationRequest:
      type: object
      properties:
        mode:
          $ref: '#/components/schemas/Mode'
        room_types:
          $ref: '#/components/schemas/RoomTypes'
        style_preset:
          $ref: '#/components/schemas/StylePreset'
        reference:
          type: string
          maxLength: 200
        metadata:
          $ref: '#/components/schemas/Metadata'
        webhook:
          $ref: '#/components/schemas/WebhookConfig'
    Workflow:
      type: string
      enum: [staging, pro_capture]
    CaptureType:
      type: string
      enum: [photo, panorama360, floorplan]
    Mode:
      type: string
      enum: [refurnish, renovate, clean]
    StylePreset:
      type: string
      enum: [modern, scandi, boho, country, industrial, mediterranean, luxury, eclectic, japandia]
      description: Canonical style preset ids accepted by the public API. Required for `staging + panorama360`, optional for `staging + photo`, and ignored for `staging + photo + mode=clean`.
    RoomType:
      type: string
      enum:
        - Living Room
        - Kitchen
        - Dining Room
        - Bathroom
        - Shower
        - Toilet
        - Bedroom
        - Children's Room
        - Hallway
        - Office
        - Closet
        - Laundry
        - Basement
        - Attic
        - Garage
        - Balcony
        - Terrace
        - Garden
        - Exterior
        - Gym
        - Sauna
        - Pool
        - Staircase
        - Utility Room
        - Studio
        - Library
        - Wine Cellar
        - Home Theater
    RoomTypes:
      type: array
      minItems: 1
      maxItems: 3
      uniqueItems: true
      items:
        $ref: '#/components/schemas/RoomType'
      description: |
        Valid room labels are listed in RoomType.
        Required when `workflow=staging`, `capture_type=photo`, and `mode` is not `clean`.
        Ignored when `workflow=staging`, `capture_type=photo`, and `mode=clean`.
        Not allowed for `workflow=pro_capture`, `capture_type=panorama360`, or `capture_type=floorplan`.
        When sending more than one room, keep the combination within a single
        supported family:
        - Open plan: Living Room, Kitchen, Dining Room
        - Wet rooms: Bathroom, Shower, Toilet
        Combinations outside those families are rejected.
    Metadata:
      type: object
      additionalProperties:
        oneOf:
          - type: string
          - type: number
          - type: boolean
          - type: 'null'
      maxProperties: 20
    WebhookConfig:
      type: object
      description: |
        Optional per-job callback URL override. The signing secret is managed
        once in organization API settings and is not sent on each request.
      required: [url]
      properties:
        url:
          type: string
          format: uri
    GenerationResponse:
      type: object
      required:
        - job_id
        - source_asset_id
        - status
        - workflow
        - capture_type
        - include_alternates
        - created_at
        - updated_at
        - outputs
      properties:
        job_id:
          type: string
          pattern: '^job_[A-Za-z0-9]+$'
        parent_job_id:
          type: string
          pattern: '^job_[A-Za-z0-9]+$'
          nullable: true
        source_asset_id:
          type: string
          pattern: '^src_[A-Za-z0-9]+$'
          nullable: true
        status:
          type: string
          enum: [ingesting, queued, running, succeeded, failed]
        workflow:
          type: string
          enum: [staging, pro_capture]
        capture_type:
          $ref: '#/components/schemas/CaptureType'
        mode:
          type: string
          nullable: true
          description: |
            Returned workflow mode.
            - `staging` + `photo`: one of `refurnish`, `renovate`, `clean`
            - `staging` + `panorama360|floorplan`: equals the capture type
            - `pro_capture`: null
        room_types:
          $ref: '#/components/schemas/RoomTypes'
        style_preset:
          $ref: '#/components/schemas/StylePreset'
          nullable: true
        include_alternates:
          type: boolean
        reference:
          type: string
          nullable: true
        metadata:
          $ref: '#/components/schemas/Metadata'
        failure:
          type: object
          nullable: true
          properties:
            code:
              type: string
            message:
              type: string
            retryable:
              type: boolean
        outputs:
          type: array
          items:
            $ref: '#/components/schemas/GenerationOutput'
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
    SourceAssetIngestResponse:
      type: object
      required:
        - ingest_id
        - status
        - source_asset_id
        - created_at
        - updated_at
      properties:
        ingest_id:
          type: string
          pattern: '^src_[A-Za-z0-9]+$'
        status:
          type: string
          enum: [queued, ingesting, ready, failed]
        source_asset_id:
          type: string
          pattern: '^src_[A-Za-z0-9]+$'
          nullable: true
        failure:
          type: object
          nullable: true
          properties:
            code:
              type: string
            message:
              type: string
            retryable:
              type: boolean
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
    GenerationOutput:
      type: object
      required:
        - output_id
        - rank
        - is_primary
        - image_url
        - expires_at
        - cached
      properties:
        output_id:
          type: string
          pattern: '^out_[A-Za-z0-9]+$'
        rank:
          type: integer
          minimum: 1
        is_primary:
          type: boolean
        image_url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time
        cached:
          type: boolean
    UsageSummaryResponse:
      type: object
      required:
        - access
        - billing_period
        - usage
        - jobs
        - rate_limits
      properties:
        access:
          type: object
          required: [enabled, status, plan]
          properties:
            enabled:
              type: boolean
            status:
              type: string
            plan:
              type: string
              nullable: true
        billing_period:
          type: object
          required: [start_at, end_at]
          properties:
            start_at:
              type: string
              format: date-time
            end_at:
              type: string
              format: date-time
        usage:
          type: object
          required: [included, consumed, overage, remaining, projected_overage_amount_usd]
          properties:
            included:
              type: integer
            consumed:
              type: integer
            overage:
              type: integer
            remaining:
              type: integer
            projected_overage_amount_usd:
              type: number
        jobs:
          type: object
          required:
            - total
            - succeeded
            - failed
            - outputs_returned
            - regenerate_count
            - cache_hit_count
          properties:
            total:
              type: integer
            succeeded:
              type: integer
            failed:
              type: integer
            outputs_returned:
              type: integer
            regenerate_count:
              type: integer
            cache_hit_count:
              type: integer
        rate_limits:
          type: object
          required:
            - create_generation_per_minute
            - regenerate_generation_per_minute
            - get_generation_per_minute
            - usage_summary_per_minute
            - billing_events_per_minute
            - source_url_fetch_per_minute
            - max_concurrent_jobs
          properties:
            create_generation_per_minute:
              type: integer
            regenerate_generation_per_minute:
              type: integer
            get_generation_per_minute:
              type: integer
            usage_summary_per_minute:
              type: integer
            billing_events_per_minute:
              type: integer
            source_url_fetch_per_minute:
              type: integer
            max_concurrent_jobs:
              type: integer
    BillingEventsResponse:
      type: object
      required: [items, next_cursor]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/BillingEvent'
        next_cursor:
          type: string
          nullable: true
    BillingEvent:
      type: object
      required:
        - event_id
        - occurred_at
        - job_id
        - source_asset_id
        - workflow
        - capture_type
        - usage_type
        - config_fingerprint
        - billable
        - unit_price_usd
        - amount_usd
      properties:
        event_id:
          type: string
          pattern: '^use_[A-Za-z0-9]+$'
        occurred_at:
          type: string
          format: date-time
        job_id:
          type: string
          pattern: '^job_[A-Za-z0-9]+$'
        source_asset_id:
          type: string
          pattern: '^src_[A-Za-z0-9]+$'
        workflow:
          type: string
          enum: [staging, pro_capture]
        capture_type:
          $ref: '#/components/schemas/CaptureType'
        mode:
          type: string
          nullable: true
        room_types:
          $ref: '#/components/schemas/RoomTypes'
        style_preset:
          $ref: '#/components/schemas/StylePreset'
          nullable: true
        usage_type:
          type: string
        config_fingerprint:
          type: string
        billable:
          type: boolean
        unit_price_usd:
          type: number
        amount_usd:
          type: number
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, retryable]
          properties:
            code:
              type: string
            message:
              type: string
            retryable:
              type: boolean
            retry_after:
              type: integer
              nullable: true
