openapi: 3.1.0
info:
  title: Pix4Less Library API
  version: 1.7.0
  description: Search, browse and draw random AI-generated photos, list collections, explicitly request asynchronous generation, inspect previews, and authorize full-resolution downloads. Search does not generate images. Published images may be unreviewed; assess suitability and the license before use. Read GET /v1/pricing for current credit costs; all currency amounts are USD. Image preview url fields are relative paths on https://pix4less.com. Every authenticated endpoint can also return 403 account_suspended and 429 rate_limit_exceeded.
servers:
  - url: https://pix4less.com
    description: Production
  - url: http://localhost:3000
    description: Local development
security:
  - ApiKey: []
  - BearerAuth: []
paths:
  /v1/images/generate:
    post:
      operationId: generateImage
      summary: Generate one new image without searching the catalog
      description: Explicit generation request. Everything is validated before enqueueing and charging, including the model (see generation_credits_by_model in GET /v1/pricing), the resolution (only resolutions the model produces natively; 4K is not offered), the preset ID, and the balance. Trial credits can create with the models in trial_generation_models (Pix4Less Standard, model id `pix4less-cluster`); other models require paid standing (a completed top-up or plan payment) and otherwise return 402. Joins an active job with the same normalized query and settings (model, resolution, aspect ratio, negative prompt, preset) when one exists; each subscriber pays once per job. This is active-job deduplication; for safe retries send an Idempotency-Key header (see the IdempotencyKey parameter); a retry with the same key and body within 24 hours replays the first response instead of creating another chargeable job. Without a key, resubmitting after a terminal result creates another chargeable job, so never blindly retry an uncertain POST without one. Jobs that fail, time out, or are cancelled are refunded. Successful subscribers own the image, so the explicit download endpoint does not charge them again for the original. Generated images join the public searchable catalog (see public_catalog_notice in GET /v1/pricing) and may be downloaded by others under the Commercial License Agreement; participation in the Creator Royalty Program (earning up to 25% back in credits on downloads) is a mandatory prerequisite of service. No API-search charge. Generation is asynchronous; a job still pending or generating at its deadline_at (30 minutes after enqueue for standard delivery, 24 hours for batch delivery) fails with reject_reason timed_out and is refunded.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/GenerationRequest' }
            example: { query: 'A red canoe on a still alpine lake at sunrise, realistic stock photograph' }
      responses:
        '202':
          description: Queued or generating; poll data.status_url with the same API key
          headers:
            Location: { schema: { type: string } }
            Retry-After: { schema: { type: integer }, description: Suggested polling interval in seconds }
          content: { application/json: { schema: { $ref: '#/components/schemas/GenerationResponse' } } }
        '200':
          description: Terminal result, including prompt-gate rejection without a charge
          content: { application/json: { schema: { $ref: '#/components/schemas/GenerationResponse' } } }
        '400':
          description: Invalid request; nothing is created or charged. error.code is invalid_request (malformed body or values, or batch delivery_tier with a model other than pix4less-cluster), unknown_model (not a Pix4Less model id), model_unavailable (a known model that is not available right now and is not listed in GET /v1/pricing), unsupported_resolution (the model does not produce that resolution natively; 4K is not offered on any model), or unknown_preset (neither a built-in preset ID nor one of your custom preset IDs).
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '409': { $ref: '#/components/responses/IdempotencyInProgress' }
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '429': { $ref: '#/components/responses/CreationLimited' }
        '503':
          description: Nothing is created or charged. error.code is generation_disabled (on-demand generation is turned off), pricing_unavailable (the live price book could not be read), or creation_limits_unavailable (the account's creation limits could not be checked). Retry later.
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/generation-requests/{id}:
    get:
      operationId: getGenerationRequest
      summary: Poll a generation job belonging to the authenticated account
      description: No credit charge; API-key rate limits apply. Poll at most once every five seconds per active job and honor 429 Retry-After, or pass wait to long-poll instead. pending, generating, preview_ready (a preview exists while the final file is prepared) and certifying (the image is being published to the catalog) are nonterminal; fulfilled, failed and rejected are terminal. Failed and rejected jobs carry a public reject_reason code and a plain-language error_message; a cancelled job is failed with reject_reason cancelled. The fulfilled image contains preview paths only; call POST /v1/images/{id}/download with image.id for the full-resolution file. Private worker prompts and metadata are never returned. Unknown jobs, jobs owned only by another account, and shared jobs this account left by cancelling all return 404.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: wait
          in: query
          required: false
          schema: { type: number, minimum: 0, maximum: 25, default: 0 }
          description: Long-poll for up to this many seconds. The response returns as soon as the job changes status or becomes terminal, or when the wait ends. 0 (default) answers immediately.
      responses:
        '200':
          description: Current status and preview when fulfilled
          headers:
            Retry-After: { schema: { type: integer }, description: Present while the job is nonterminal }
          content: { application/json: { schema: { $ref: '#/components/schemas/GenerationResponse' } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '404':
          description: No accessible generation request with that ID
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/CatalogUnavailable' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/generation-requests:
    get:
      operationId: listGenerationRequests
      summary: List this account's generation jobs, newest first
      description: The API twin of the studio history. Includes standard jobs and batch items the account subscribes to, from every surface (website, studio and API). No credit charge; API-key rate limits apply.
      parameters:
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
        - name: model
          in: query
          required: false
          schema: { type: string, minLength: 1, maxLength: 100 }
          description: Only jobs created with this model id.
        - name: status
          in: query
          required: false
          schema: { $ref: '#/components/schemas/GenerationStatus' }
      responses:
        '200':
          description: The account's jobs, newest first
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/GenerationJob' } }
                  request_id: { type: string, format: uuid }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/library:
    get:
      operationId: listLibraryImages
      summary: List the images this account owns, newest first
      description: Every image the account generated or paid to download. Re-downloading an owned image with POST /v1/images/{id}/download is free, so look here before searching or generating again. q matches words in the title, description and tags and may scan several pages of the library before returning; keep following pagination.next_cursor while pagination.has_more is true. No credit charge; API-key rate limits apply.
      parameters:
        - name: q
          in: query
          required: false
          schema: { type: string, minLength: 1, maxLength: 200 }
          description: Words that must all appear in the image's title, description or tags.
        - name: source
          in: query
          required: false
          schema: { type: string, enum: [generated, downloaded] }
          description: generated = images this account created; downloaded = catalog images it paid for.
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
        - name: cursor
          in: query
          required: false
          schema: { type: string, maxLength: 20 }
          description: pagination.next_cursor from the previous page.
      responses:
        '200':
          description: One page of the account's library
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination, request_id]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/LibraryImage' } }
                  pagination:
                    type: object
                    required: [limit, has_more, next_cursor]
                    properties:
                      limit: { type: integer }
                      has_more: { type: boolean }
                      next_cursor: { type: [string, 'null'] }
                  request_id: { type: string, format: uuid }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/generation-requests/{id}/preview:
    get:
      operationId: getGenerationPreview
      summary: Fetch the on-screen JPEG preview of a generation job
      description: Returns image bytes (image/jpeg) once the job is preview_ready or fulfilled. No credit charge. The full-resolution file comes from POST /v1/images/{id}/download with the fulfilled job's image.id, which is free for the job's subscribers.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: The preview image
          content:
            image/jpeg:
              schema: { type: string, contentMediaType: image/jpeg }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '404':
          description: No accessible generation request with that ID (not_found), or its preview is no longer stored (preview_unavailable).
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '409':
          description: The job has no preview yet or did not complete (error.code preview_not_ready).
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/generation-requests/{id}/cancel:
    post:
      operationId: cancelGenerationRequest
      summary: Cancel a pending or generating job and refund its charge
      description: Cancels a job while it is pending or generating (cancellable is true). No request body is required. When this account is the job's only subscriber, the job stops (scope job) and is failed with reject_reason cancelled. When other accounts joined the same job, it keeps running for them and only this account is unsubscribed (scope subscription); data then carries only id, status, reject_reason, error_message and status_url, and later polls of that job return 404 for this account. Either way this account's charge for the job is refunded exactly once and reported in billing.refunded_credits. Cancelling a job that is already cancelled returns 200 with refunded_credits 0. A job that is preview_ready, certifying, or terminal returns 409 not_cancellable. Batch items can be cancelled the same way.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Cancelled (or already cancelled) and refunded
          content: { application/json: { schema: { $ref: '#/components/schemas/CancelGenerationResponse' } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '404':
          description: No accessible generation request with that ID
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '409':
          description: The job can no longer be cancelled (error.code not_cancellable) because it is already finishing (preview_ready or certifying), has finished, or has already stopped. Nothing is refunded by this request.
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/batches:
    post:
      operationId: createBatch
      summary: Submit a bulk batch generation job (delivery within 24 hours at batch prices)
      description: Submit up to batch_max_items_per_batch prompts (never more than 100) for delivery within 24 hours at batch prices (generation_credits_batch_by_resolution in GET /v1/pricing; batch_discount_percent is the effective saving). Pix4Less Standard (model id `pix4less-cluster`) only; trial credits can be used. All-or-nothing. Every item is validated (400 invalid_item names the first bad item) and screened by the prompt gate (400 items_rejected lists every rejected item in error.details.rejected_items); then the batch, its items and the whole charge are created in one transaction. Batch items never join other jobs. The response lists each item's generation-request id and status_url; items can be polled and cancelled like any generation job. webhook_url is not supported yet. Send an Idempotency-Key header to make retries safe; the same key and body within 24 hours replays the first response instead of creating and charging another batch.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchRequest' }
      responses:
        '201':
          description: Batch accepted and queued; data.items lists the created jobs. Poll data.status_url.
          headers:
            Location: { schema: { type: string } }
            Retry-After: { schema: { type: integer }, description: Suggested polling interval in seconds }
          content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } }
        '400':
          description: Invalid batch; nothing is created or charged. error.code is invalid_request (malformed body), webhook_not_supported (webhook_url was sent; poll status_url instead), unknown_model, model_unavailable, invalid_model (a model other than pix4less-cluster), batch_too_large, invalid_item (an item's query, resolution, negative_prompt, or preset ID is invalid; 4K is not offered), or items_rejected (the prompt gate rejected one or more items; error.details.rejected_items lists index, custom_id, reason and message for each).
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '409': { $ref: '#/components/responses/IdempotencyInProgress' }
        '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
        '429': { $ref: '#/components/responses/CreationLimited' }
        '503':
          description: Nothing is created or charged. error.code is generation_disabled, pricing_unavailable, or creation_limits_unavailable. Retry later.
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/batches/{id}:
    get:
      operationId: getBatch
      summary: Check status and progress of a batch
      description: Returns progress (completed_requests, failed_requests), status, the credits charged and refunded, and the deadline. No credit charge.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Current batch status and progress
          headers:
            Retry-After: { schema: { type: integer }, description: Present while the batch is nonterminal }
          content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '404':
          description: No batch with that ID belongs to this account
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/batches/{id}/results:
    get:
      operationId: getBatchResults
      summary: Retrieve every item of a batch with its current result
      description: Returns the batch and every item with its current generation-job view (preview paths only for fulfilled images), its custom_id, and the credits charged and refunded for that item. No credit charge.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: The batch and all of its items
          content: { application/json: { schema: { $ref: '#/components/schemas/BatchResultsResponse' } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '404':
          description: No batch with that ID belongs to this account
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/CatalogUnavailable' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/presets:
    get:
      operationId: listPresets
      summary: List all available style presets
      description: Returns both standard built-in presets and any custom style presets created by the authenticated account. Pass a preset's id as preset in generation and batch requests.
      responses:
        '200':
          description: List of available presets
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [data, request_id]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Preset' }
                  request_id: { type: string, format: uuid }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
    post:
      operationId: createPreset
      summary: Create a custom style preset
      description: Creates a user-defined style preset with a 200-character prompt limit. The name and prompt_suffix are screened like a generation prompt; text the prompt gate rejects returns 400 preset_rejected, and invalid values return 400 invalid_request or invalid_preset. Use the returned id as preset in generation and batch requests.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name, prompt_suffix]
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 50
                  description: Human-readable name for the preset.
                prompt_suffix:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: Visual style directives with a strict 200 character limit.
      responses:
        '201':
          description: Preset created successfully
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [data, request_id]
                properties:
                  data: { $ref: '#/components/schemas/Preset' }
                  request_id: { type: string, format: uuid }
        '400':
          description: Invalid preset (error.code invalid_request or invalid_preset) or text the prompt gate rejected (preset_rejected).
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/presets/{id}:
    delete:
      operationId: deletePreset
      summary: Delete a custom style preset
      description: Permanently removes a custom preset belonging to the authenticated account.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Preset deleted
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    additionalProperties: false
                    required: [deleted, id]
                    properties:
                      deleted: { type: boolean }
                      id: { type: string, format: uuid }
                  request_id: { type: string, format: uuid }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '404':
          description: Preset not found or not owned by this account
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/images/search:
    post:
      operationId: searchImages
      summary: Search published AI-generated photos
      description: Returns ready, published, non-rejected images with web/thumbnail previews only. Text searches use blended relevance ordering, not strictly descending confidence. Without session_id, page selects the slice of the result list, nothing is stored, and the response session_id is null. With session_id (cursor mode), page is ignored and each request returns the next unseen matches for that session until the session expires or a shuffle cycle restarts. Omit query to browse newest-first with filters. A metered account whose balance is below the search's credit cost gets 402 before any search work is done. Credits are charged only after a valid search completes with at least one result; invalid input, service failures, timeouts, and zero-result searches do not consume search credits. Requests are not idempotent; do not automatically retry an uncertain request. Current costs are available at GET /v1/pricing (search_credits).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequest'
            example:
              query: coffee
              filters:
                orientation: landscape
                min_width: 1600
              page: 1
              per_page: 1
      responses:
        '200':
          description: Preview-only matches in retrieval order; browse results without a query may omit match_score and match_confidence
          headers:
            X-Request-Id: { $ref: '#/components/headers/RequestId' }
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SearchResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503':
          description: The search did not complete and nothing was charged. error.code is search_timeout (the search exceeded its 25-second budget) or catalog_unavailable (the catalog engine is unavailable or did not answer; a timeout while waiting for the engine is also reported this way). Retry later.
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/images/{id}:
    get:
      operationId: getImage
      summary: Fetch one published image by id
      description: Returns one published image with its web/thumbnail preview paths, never an original URL. Store the image id. No search or download credits are charged by this endpoint; the API-key rate limit still applies. Only published images resolve; unknown, unpublished, still-certifying, rejected, and previewless images are reported as not found.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: The requested image
          headers:
            X-Request-Id: { $ref: '#/components/headers/RequestId' }
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data: { $ref: '#/components/schemas/CatalogImage' }
                  request_id: { type: string, format: uuid }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/CatalogUnavailable' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/images:
    get:
      operationId: browseImages
      summary: Browse the library without a query
      description: The API twin of the website's library browse, free of credits (API-key rate limits apply). Returns 12 published images per page from a shuffled walk over the newest images. Send the returned seed back with the next page to keep the same order; without a seed the order changes every ten minutes. To browse with filters or a collection, use POST /v1/images/search without a query instead (search pricing applies).
      parameters:
        - name: page
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 10000, default: 1 }
        - name: seed
          in: query
          required: false
          schema: { type: string, pattern: '^[A-Za-z0-9_-]{1,64}$' }
      responses:
        '200':
          description: One page of the shuffled library
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination, seed, request_id]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Image' } }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  seed: { type: string, description: Send back as seed to page through the same order. }
                  request_id: { type: string, format: uuid }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/CatalogUnavailable' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/images/random:
    post:
      operationId: randomImage
      summary: Draw one random published image
      description: The API twin of the website's "I feel lucky". Free of credits; each draw uses the account's free monthly random-draw allowance, shared with the website (429 random_quota_exceeded when it is used up, with error.details.reset_at). Send the returned session_id back to avoid repeats within a walk. The body is optional.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                session_id: { type: string, format: uuid }
      responses:
        '200':
          description: Zero or one image
          content:
            application/json:
              schema:
                type: object
                required: [data, session_id, quota, request_id]
                properties:
                  data: { type: array, maxItems: 1, items: { $ref: '#/components/schemas/Image' } }
                  session_id: { type: string, format: uuid }
                  quota:
                    type: object
                    required: [unlimited, limit, remaining, reset_at]
                    properties:
                      unlimited: { type: boolean }
                      limit: { type: [integer, 'null'] }
                      remaining: { type: [integer, 'null'] }
                      reset_at: { type: string, format: date-time }
                  request_id: { type: string, format: uuid }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '429':
          description: The per-minute rate limit (rate_limit_exceeded, see RateLimited) or the monthly random-draw allowance (random_quota_exceeded) is used up.
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '503': { $ref: '#/components/responses/CatalogUnavailable' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/images/{id}/upscale:
    get:
      operationId: getImageUpscale
      summary: Check whether a 4K upscale of an image exists
      description: Free. When available is true, POST /v1/images/{id}/download?rendition=upscaled_4k delivers it at the 4K tier price (or only the tier difference when the original is already owned).
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Upscale availability
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: object
                    required: [id, available, upscale]
                    properties:
                      id: { type: string }
                      available: { type: boolean }
                      upscale:
                        type: [object, 'null']
                        properties:
                          width: { type: integer }
                          height: { type: integer }
                          format: { type: string }
                          bytes: { type: [integer, 'null'] }
                  request_id: { type: string, format: uuid }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/CatalogUnavailable' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/collections:
    get:
      operationId: listCollections
      summary: List published collections
      description: Free. Collections with at least one published image, as listed on the website's /collections page. Pass a slug as filters.collection in POST /v1/images/search. downloads_available is false for collections whose images are shown but not sold.
      responses:
        '200':
          description: Published collections
          content:
            application/json:
              schema:
                type: object
                required: [data, request_id]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      required: [slug, name, description, image_count, downloads_available]
                      properties:
                        slug: { type: string }
                        name: { type: string }
                        description: { type: string }
                        image_count: { type: integer, minimum: 1 }
                        downloads_available: { type: boolean }
                  request_id: { type: string, format: uuid }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/AccountSuspended' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/images/{id}/download:
    post:
      operationId: downloadImage
      summary: Authorize a full-resolution download
      description: Returns a short-lived signed original-image URL after entitlement checks. Metered accounts are charged once per account per photo at the price-book rate for the delivered file's quality tier (download_credits_by_tier in GET /v1/pricing); already-owned photos are not charged again and remain downloadable at zero credit balance. The 4K upscale of an original the account already owns costs only the difference between the two tiers, charged once; a 402 then reports that difference as credits_required. Payment messages quote USD amounts. Authentication, rate limits, and current image deliverability still apply; images in collections whose downloads are withheld return 403 download_unavailable. Unlimited accounts are exempt from charges. No request body is required. The success field owned means access is authorized after this request, not that the request was free. Persist the image id and request a fresh URL when needed; do not log signed URLs or send the API key to object storage.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: resolution
          in: query
          required: false
          schema: { type: string, enum: [1K, 2K, 4K, standard, hd, 4k, original] }
          description: Legacy alias of rendition, ignored when rendition is sent. 4K or 4k requests the 4K upscale (and delivers the original instead when no upscale exists but the original is already 4K-tier); every other value delivers the original. It never selects a smaller or cheaper file; the original is always priced at its own quality tier.
        - name: rendition
          in: query
          required: false
          schema: { type: string, enum: [original, upscaled_4k], default: original }
          description: original (default) delivers the original file. upscaled_4k delivers the 4K upscale and returns 404 upscale_unavailable when the image has none.
      responses:
        '200':
          description: Authorized original-image URL, not image bytes
          headers:
            X-Request-Id: { $ref: '#/components/headers/RequestId' }
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DownloadResponse' }
        '400':
          description: Unknown rendition or resolution value (error.code invalid_rendition). Nothing is charged.
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403':
          description: Nothing is charged. error.code is account_suspended (the account that owns the API key is suspended) or download_unavailable (downloads of this image are withheld).
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '404':
          description: No published image exists with that id (error.code image_not_found or not_found; an image that is not published, such as one still being certified, is delivered only to an account that already owns it), or the requested 4K upscale is not available (upscale_unavailable).
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/CatalogUnavailable' }
        '500': { $ref: '#/components/responses/ServerError' }
  /v1/pricing:
    get:
      operationId: getPricing
      summary: Read the live price book and plan limits
      description: Public, no authentication or credit charge. Values come from the operational price book and may change; read prices here instead of hardcoding them. The response is cached for up to five minutes and has no data or request_id wrapper. All currency amounts are USD; credit amounts are separate numeric fields. Only models available right now are listed, each with the resolutions it produces natively; 4K is not offered.
      security: []
      responses:
        '200':
          description: Current pricing and subscription plans
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Pricing' }
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key from the developer dashboard. Authorization Bearer is also accepted.
    BearerAuth:
      type: http
      scheme: bearer
      description: The same dashboard API key, kept server-side. X-API-Key takes precedence when both headers are present.
  headers:
    RequestId:
      description: Request correlation UUID
      schema: { type: string, format: uuid }
    RateLimit:
      description: Per-minute request limit of this API key, or unlimited for unlimited accounts. On a 429 it is the limit that was exceeded (error.details.limit).
      schema: { type: string, pattern: '^([0-9]+|unlimited)$' }
    RateLimitRemaining:
      description: Requests left in the current minute, the smaller of this key's remaining requests and the account's remaining requests across all of its API keys. 0 on a 429, and unlimited for unlimited accounts.
      schema: { type: string, pattern: '^([0-9]+|unlimited)$' }
    RateLimitReset:
      description: Unix timestamp when the current window resets
      schema: { type: integer }
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Optional client-generated key (1-255 printable ASCII characters, e.g. a UUID) that makes a charged POST safe to retry. Keys are scoped to the API key's account and honoured for 24 hours. A retry with the same key and the same method, path and JSON body replays the stored response with the header Idempotent-Replayed true and no new charge. Successes and deterministic client errors (such as 400) are stored; 5xx, 402, 409 and 429 responses are not, so the same key can be retried after them. Reusing a key with a different body returns 422 idempotency_key_reused; a retry while the first request is still running returns 409 idempotency_in_progress. An invalid key returns 400 invalid_idempotency_key.
      schema: { type: string, minLength: 1, maxLength: 255 }
  responses:
    BadRequest:
      description: Invalid JSON body, parameters, or IDs (error.code invalid_request unless a more specific code is documented). Nothing is charged.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    Unauthorized:
      description: API key missing (missing_api_key), invalid, or revoked (invalid_api_key)
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    AccountSuspended:
      description: The account that owns this API key is suspended or deleted (error.code account_suspended). Every authenticated endpoint can return this. Contact support.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    NotFound:
      description: No published image exists with that id
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    PaymentRequired:
      description: Insufficient credits, or a generation model outside trial_generation_models requested without paid standing; error.code is payment_required. error.details.credits_required is what this request needs (for the 4K upscale of an owned original, only the difference between tiers) and error.message quotes the USD amount. Top up using error.details.topup_url before making another chargeable request.
      content: { application/json: { schema: { $ref: '#/components/schemas/PaymentRequiredResponse' } } }
    IdempotencyInProgress:
      description: A request with the same Idempotency-Key is still being processed (error.code idempotency_in_progress). Nothing new was created; retry after Retry-After seconds.
      headers:
        Retry-After: { schema: { type: integer } }
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    IdempotencyKeyReused:
      description: The Idempotency-Key was already used within 24 hours for a different request body or endpoint (error.code idempotency_key_reused). Nothing was created or charged; use a new key.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    ServerError:
      description: Server or storage failure. Search failures are not charged, but requests are not idempotent; inspect the response before retrying.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    RateLimited:
      description: Per-minute request limit exceeded (error.code rate_limit_exceeded). Each API key has its own limit, and all keys of one account share an account-wide limit; when the account-wide limit is exceeded, error.details.scope is account. error.details.limit is the limit that was exceeded and error.details.reset_at is when the window resets.
      headers:
        Retry-After: { schema: { type: integer } }
        X-RateLimit-Limit: { $ref: '#/components/headers/RateLimit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
      content: { application/json: { schema: { $ref: '#/components/schemas/RateLimitErrorResponse' } } }
    CreationLimited:
      description: Too many requests; nothing is created or charged. error.code is rate_limit_exceeded (the per-minute API-key or account limit, with Retry-After; see RateLimited), creation_rate_limited (too many creations started this minute or hour, with Retry-After and error.details.reset_at), or a creation limit without Retry-After, namely concurrent_limit_exceeded (too many standard jobs in progress, 1 for trial accounts and 3 with paid standing; wait for one to finish or cancel it), batch_active_limit_exceeded (batch_max_active_items_per_user in GET /v1/pricing), or monthly_creation_limit_exceeded (monthly_creations_by_plan for the current cycle).
      headers:
        Retry-After: { schema: { type: integer }, description: Present for rate_limit_exceeded and creation_rate_limited only }
        X-RateLimit-Limit: { $ref: '#/components/headers/RateLimit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    CatalogUnavailable:
      description: The catalog engine is unavailable or did not answer in time (error.code catalog_unavailable; error.details.circuit is open while requests are refused fast). Nothing is charged; retry later.
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
  schemas:
    GenerationRequest:
      type: object
      additionalProperties: false
      required: [query]
      properties:
        query:
          type: string
          minLength: 1
          maxLength: 2000
          description: Nonblank description, trimmed before enforcing the model's prompt limit in UTF-16 code units (generation_max_prompt_length_by_model in GET /v1/pricing; longer prompts return 400 invalid_request without charging). Search filters and batch counts are not accepted.
        resolution:
          type: string
          enum: [1K, 2K]
          default: 1K
          description: Requested pixel-area tier; dimensions vary by aspect ratio. 1K (default) is about 1 megapixel and 2K about 4 megapixels. The model must produce the resolution natively (generation_resolutions_by_model in GET /v1/pricing); otherwise the request returns 400 unsupported_resolution without charging. 4K is not offered on any model and also returns 400 unsupported_resolution.
        preset:
          type: string
          minLength: 1
          maxLength: 100
          description: Optional style preset ID, either a built-in preset id (editorial, architectural, cinematic, still-life, documentary; see GET /v1/presets) or the id of one of your custom presets. Style text is not accepted directly; save it as a custom preset first. Unknown IDs return 400 unknown_preset without charging.
        style_preset:
          type: string
          minLength: 1
          maxLength: 100
          description: Alias for preset; preset wins when both are sent.
        aspect_ratio:
          type: string
          enum: ['16:9', '1:1', '4:3', '9:16', '4:5']
          default: '16:9'
          description: Optional framing/composition aspect ratio.
        negative_prompt:
          type: string
          maxLength: 500
          description: Optional attributes or elements to exclude from the image (trimmed; up to 500 characters).
        delivery_tier:
          type: string
          enum: [standard, batch]
          default: standard
          description: standard (default) runs as soon as capacity allows at standard prices. batch delivers within 24 hours at batch prices (generation_credits_batch_by_resolution in GET /v1/pricing) and is only available for pix4less-cluster.
        model:
          type: string
          minLength: 1
          maxLength: 100
          default: pix4less-cluster
          description: Model id; defaults to pix4less-cluster. GET /v1/pricing lists the models available right now (generation_credits_by_model) and the ones trial credits can use (trial_generation_models). Unknown ids return 400 unknown_model, and known models that are not available right now return 400 model_unavailable. Models outside trial_generation_models need paid standing (402 otherwise).
    GenerationStatus:
      type: string
      enum: [pending, generating, preview_ready, certifying, fulfilled, failed, rejected]
      description: pending, generating, preview_ready (a preview exists while the final file is prepared) and certifying (the image is being published to the catalog) are nonterminal. fulfilled, failed and rejected are terminal. Cancelled and timed-out jobs are failed; prompt-gate rejections are rejected.
    GenerationFailureCode:
      type: string
      enum: [sexual_content, violent_content, real_person, trademark, unusable_query, insufficient_credits, cancelled, timed_out, content_declined, prompt_refused, generation_failed]
      description: Stable public failure code. sexual_content, violent_content, real_person, trademark and unusable_query are prompt-gate decisions (status rejected, never charged). insufficient_credits means nothing was charged. cancelled means the requester cancelled; timed_out means the job passed its deadline_at; content_declined means the image service declined the prompt; prompt_refused means the model declined the prompt under its usage policy; generation_failed covers every other failure. Charges for failed jobs are refunded, except prompt_refused, which keeps its charge. New codes may be added.
    GenerationJob:
      type: object
      additionalProperties: false
      required: [id, status, query, created_at, updated_at, image, status_url]
      properties:
        id: { type: string, format: uuid }
        status: { $ref: '#/components/schemas/GenerationStatus' }
        query: { type: string }
        resolution:
          type: string
          enum: [1K, 2K, 4K]
          description: Requested resolution tier. 4K appears only on jobs created before 4K was withdrawn.
        target_resolution: { type: string, enum: [1K, 2K, 4K, 1080p] }
        preview_url:
          type: [string, 'null']
          description: Relative path of the on-screen JPEG preview (GET /v1/generation-requests/{id}/preview, authenticated with the same API key) while status is preview_ready or fulfilled, otherwise null.
        download_url:
          type: [string, 'null']
          description: Relative website path of the file once status is fulfilled, otherwise null. It needs a signed-in pix4less.com browser session and does not accept an API key; API clients call POST /v1/images/{id}/download with image.id instead.
        upscale_status: { type: string, enum: [none, pending, processing, completed, failed] }
        aspect_ratio: { type: string, enum: ['16:9', '1:1', '4:3', '9:16', '4:5'] }
        negative_prompt: { type: [string, 'null'] }
        style_preset:
          type: [string, 'null']
          description: Preset ID used for this job (a built-in id or a custom preset id), never the preset text.
        model:
          type: string
          description: Model used for this job.
        delivery_tier: { type: string, enum: [standard, batch] }
        batch_id: { type: [string, 'null'], format: uuid }
        custom_id: { type: [string, 'null'] }
        reject_reason:
          description: Public failure code for failed and rejected jobs, otherwise null. Never a raw provider or worker error.
          anyOf:
            - { $ref: '#/components/schemas/GenerationFailureCode' }
            - { type: 'null' }
        error_message:
          type: [string, 'null']
          description: Plain-language explanation for failed and rejected jobs, otherwise null.
        deadline_at:
          type: [string, 'null']
          format: date-time
          description: When an unfinished job is stopped and refunded with reject_reason timed_out; 30 minutes after enqueue for standard delivery and 24 hours for batch delivery. Null for prompt-gate rejections.
        cancellable:
          type: boolean
          description: True while status is pending or generating; see POST /v1/generation-requests/{id}/cancel.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        image:
          anyOf:
            - { $ref: '#/components/schemas/CatalogImage' }
            - { type: 'null' }
        image_id:
          type: [string, 'null']
          description: Id of the produced image once status is fulfilled, otherwise null. Can be set while image is still null (previews not servable yet); POST /v1/images/{id}/download accepts it.
        queue_position:
          type: [integer, 'null']
          minimum: 1
          description: 1-based position among pending jobs while pending, otherwise null.
        queue_total: { type: integer, minimum: 0 }
        estimated_wait_seconds:
          type: integer
          minimum: 0
          description: Rough estimate while pending; not a guarantee.
        started_at: { type: [string, 'null'], format: date-time }
        estimated_remaining_seconds:
          type: [integer, 'null']
          minimum: 0
          description: Rough estimate; not a guarantee.
        assigned_operator:
          type: [string, 'null']
          enum: [Pix4Less Standard, Partner API, null]
          description: Generic engine label from generating onward, otherwise null. Never an internal worker or account name.
        status_url: { type: string, pattern: '^/v1/generation-requests/' }
    GenerationResponse:
      type: object
      additionalProperties: false
      required: [data, request_id]
      properties:
        data: { $ref: '#/components/schemas/GenerationJob' }
        request_id: { type: string, format: uuid }
        billing:
          type: object
          additionalProperties: false
          required: [charged_credits, balance]
          properties:
            charged_credits: { type: integer, minimum: 0 }
            balance: { type: integer }
    CancelGenerationResponse:
      type: object
      additionalProperties: false
      required: [data, cancelled, scope, billing, request_id]
      properties:
        data:
          anyOf:
            - { $ref: '#/components/schemas/GenerationJob' }
            - { $ref: '#/components/schemas/CancelledSubscription' }
        cancelled: { const: true }
        scope:
          type: string
          enum: [job, subscription]
          description: job means the job was stopped. subscription means the job is shared with other accounts and keeps running for them; only this account was unsubscribed and refunded.
        billing:
          type: object
          additionalProperties: false
          required: [refunded_credits, balance]
          properties:
            refunded_credits:
              type: integer
              minimum: 0
              description: Credits returned to this account by this request; 0 when the job was already cancelled.
            balance: { type: integer }
        request_id: { type: string, format: uuid }
    CancelledSubscription:
      type: object
      additionalProperties: false
      required: [id, status, reject_reason, error_message, status_url]
      description: The data of a cancel with scope subscription. The shared job continues for its other subscribers and is no longer visible to this account.
      properties:
        id: { type: string, format: uuid }
        status: { const: failed }
        reject_reason: { const: cancelled }
        error_message: { type: string }
        status_url: { type: string, pattern: '^/v1/generation-requests/' }
    SearchRequest:
      type: object
      additionalProperties: false
      properties:
        query:
          type: string
          maxLength: 2000
          description: Plain-language image description; the server trims whitespace before enforcing its 2000 UTF-16-code-unit limit. Keep requests within this limit even for non-BMP characters. Omit or leave blank to browse newest-first; filters still apply.
        session_id:
          type: string
          format: uuid
          description: Cursor mode. Send a UUID (generate one, or reuse the session_id from the previous response) and each request returns the next unseen matches for that session; page is then ignored. A session expires 24 hours after its last use. Omit session_id for page-based pagination; nothing is stored.
        shuffle:
          type: boolean
          default: false
          description: Requires a nonblank query containing a letter or digit. Shuffle is session-based; without session_id the server starts a new session and returns it as session_id. With a reused session_id, return the next unseen matches in retrieval order. When qualifying matches are exhausted, restart the cycle and set shuffle_cycle_restarted. Use per_page 1 for a one-photo-at-a-time carousel.
        filters:
          $ref: '#/components/schemas/SearchFilters'
        page:
          type: integer
          minimum: 1
          default: 1
          description: 1-based page of the result list, used only without session_id. Ignored in cursor mode.
        per_page:
          type: integer
          minimum: 1
          maximum: 12
          default: 12
          description: A search returns at most 12 images per request.
        search_type:
          type: string
          enum: [fast, deep]
          default: fast
          description: Search depth tier. fast (default) has the ranking model review about 30 candidates; deep reviews about 100. Each tier has its own credit cost, published as search_credits in GET /v1/pricing.
        safe_search:
          type: boolean
          default: true
          description: On by default. Images with a content warning (today partial_nudity, sheer lingerie or swimwear that shows the body) are left out of results and counts. Send false to include them; each such image lists its warnings in content_warnings. Browse, random and collection listings always apply safe search.
    SearchFilters:
      type: object
      additionalProperties: false
      description: String values are trimmed before validation. Each minimum must be less than or equal to its corresponding maximum.
      properties:
        tags:
          type: array
          maxItems: 20
          items: { type: string, minLength: 1, maxLength: 80 }
        collection:
          type: string
          minLength: 1
          maxLength: 100
          description: Collection UUID or slug
        orientation:
          type: string
          enum: [landscape, portrait, square, horizontal, vertical]
          description: Horizontal and vertical are accepted aliases; responses use landscape and portrait.
        min_width: { type: integer, minimum: 1 }
        max_width: { type: integer, minimum: 1 }
        min_height: { type: integer, minimum: 1 }
        max_height: { type: integer, minimum: 1 }
        min_aspect_ratio: { type: number, exclusiveMinimum: 0, maximum: 10, description: Minimum width divided by height }
        max_aspect_ratio: { type: number, exclusiveMinimum: 0, maximum: 10, description: Maximum width divided by height }
        min_megapixels: { type: number, exclusiveMinimum: 0, maximum: 500 }
        max_megapixels: { type: number, exclusiveMinimum: 0, maximum: 500 }
        format: { type: string, enum: [jpg, jpeg, png, webp, avif], description: Original asset format }
        aspect_ratio:
          type: string
          enum: ["16:9", "1:1", "4:3", "3:2", "9:16", "4:5", "all"]
          description: Aspect ratio preset option
        resolution:
          type: string
          enum: ["1K", "2K", "4K", "standard", "hd", "4k", "all"]
          description: Resolution tier option
    CatalogImage:
      type: object
      description: Public preview-only projection. Original resolution metadata is included, but original URLs and private generation provenance are not. Search results add relevance fields; see Image.
      additionalProperties: false
      required: [id, url, url_expires_at, title, description, tags, collection, style, orientation, width, height, aspect_ratio, megapixels, format, renditions, generation, license, created_at]
      properties:
        id: { type: string, format: uuid }
        url:
          type: string
          format: uri-reference
          description: Preferred preview as a relative path on https://pix4less.com (for example /media/previews/{id}/web.webp); resolve it against that origin. Uses the web rendition when available, otherwise the thumbnail; never the original. Preview paths are stable and unsigned.
        url_expires_at:
          type: [string, 'null']
          format: date-time
          description: Always null today because preview paths do not expire. Kept for compatibility.
        download_path:
          type: string
          pattern: '^/api/images/[^/]+/download$'
          description: Optional website-session helper, NOT the API-key download route. API clients must call POST /v1/images/{id}/download using id.
        title: { type: string }
        description: { type: string }
        tags: { type: array, items: { type: string } }
        collection:
          oneOf:
            - $ref: '#/components/schemas/CollectionReference'
            - type: 'null'
        style: { type: string }
        orientation: { type: string, enum: [landscape, portrait, square] }
        width: { type: integer, minimum: 1 }
        height: { type: integer, minimum: 1 }
        aspect_ratio: { type: number, exclusiveMinimum: 0, description: Original width divided by height }
        megapixels: { type: number, minimum: 0, description: Original resolution in megapixels }
        format: { type: string, enum: [jpg, jpeg, png, webp, avif] }
        renditions:
          type: object
          additionalProperties: false
          minProperties: 1
          properties:
            web: { $ref: '#/components/schemas/ImageRendition' }
            thumbnail: { $ref: '#/components/schemas/ImageRendition' }
        generation:
          type: object
          additionalProperties: false
          required: [model, date, provenance]
          properties:
            model: { type: [string, 'null'] }
            date: { type: [string, 'null'], format: date }
            provenance: { type: 'null', description: Private generation details are never exposed. }
        license:
          type: object
          additionalProperties: false
          required: [code, url, status]
          properties:
            code: { type: string }
            url: { type: [string, 'null'], format: uri }
            status: { type: string }
        created_at: { type: string, format: date-time }
        quality_tier:
          type: string
          enum: [standard, hd, 4k]
          description: Quality and resolution tier based on asset dimensions and megapixels.
        download_credits:
          type: integer
          minimum: 1
          description: Indicative default credit cost for the original's quality tier, a fixed default that can differ from the live price book. The actual charge is download_credits_by_tier[quality_tier] from GET /v1/pricing, and nothing is charged again for images you already own.
        match_score:
          type: number
          description: Relative blended relevance score; compare only within the current response. Present on text-search results; absent on browse results without a query and on single-image lookups.
        match_confidence:
          type: [number, 'null']
          minimum: 0
          maximum: 1
          description: Retrieval estimate between 0 and 1, not a probability, quality guarantee, or strict ordering key. Present on text-search results; absent on browse results without a query and on single-image lookups.
        search_match:
          type: string
          enum: [match, alternative]
          description: Present on text-search results when the ranking model judged them. match means the image matches the query; alternative means it is a related alternative. Absent otherwise.
        content_warnings:
          type: array
          items: { type: string, enum: [partial_nudity] }
          description: Content warnings for this image; empty for almost every image. partial_nudity marks sheer lingerie or swimwear that shows the body. Such images appear in search results only with safe_search set to false, and remain reachable by id.
    Image:
      description: A search result. Text searches add match_score and match_confidence (and search_match when judged); browse results without a query may omit them.
      allOf:
        - $ref: '#/components/schemas/CatalogImage'
    LibraryImage:
      type: object
      description: An owned image. When available is true it carries every CatalogImage field as well; when false the image is still owned and downloadable by id, but the catalog cannot describe it right now.
      required: [id, available, source, acquired_at, generation_request_id]
      properties:
        id: { type: string, format: uuid }
        available: { type: boolean }
        source: { type: string, enum: [generated, downloaded] }
        acquired_at: { type: string, format: date-time }
        generation_request_id:
          type: [string, 'null']
          format: uuid
          description: The generation job that produced the image (GET /v1/generation-requests/{id}); null for downloaded images.
        owned_tier:
          type: string
          enum: [standard, hd, 4k]
          description: Highest quality tier the account holds for the image; asking for a tier at or below it is free.
        page_url: { type: string, format: uri, description: Public image page on pix4less.com. }
    ImageRendition:
      type: object
      additionalProperties: false
      required: [kind, url, url_expires_at, width, height, format, byte_size]
      properties:
        kind: { type: string, enum: [web, thumbnail] }
        url:
          type: string
          format: uri-reference
          description: Relative preview path on https://pix4less.com; stable and unsigned.
        url_expires_at:
          type: [string, 'null']
          format: date-time
          description: Always null today because preview paths do not expire.
        width: { type: integer, minimum: 1 }
        height: { type: integer, minimum: 1 }
        format: { type: string, enum: [jpg, jpeg, png, webp, avif] }
        byte_size: { type: [integer, 'null'], minimum: 0 }
    CollectionReference:
      type: object
      additionalProperties: false
      required: [id, slug, name]
      properties:
        id: { type: string, format: uuid }
        slug: { type: string }
        name: { type: string }
    Pagination:
      type: object
      required: [page, per_page, total, total_pages, has_next]
      properties:
        page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
        total_pages: { type: integer }
        has_next: { type: boolean }
    SearchResponse:
      type: object
      required: [data, pagination, shuffle_cycle_restarted, session_id, request_id]
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/Image' } }
        pagination: { $ref: '#/components/schemas/Pagination' }
        shuffle_cycle_restarted:
          type: boolean
          description: True only when shuffle exhausted the qualifying matches and restarted from the best match in this response.
        search_type: { type: string, enum: [fast, deep] }
        session_id:
          type: [string, 'null']
          format: uuid
          description: The cursor-mode session to send as session_id on the next request. Null when the request carried no session_id and did not shuffle.
        request_id: { type: string, format: uuid }
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              description: Stable machine-readable code. Codes by status are 400 invalid_request, unknown_model, model_unavailable, unsupported_resolution, unknown_preset, invalid_model, invalid_item, items_rejected, webhook_not_supported, batch_too_large, invalid_preset, preset_rejected, invalid_rendition, invalid_dimensions, invalid_aspect_ratio, invalid_resolution, invalid_shuffle and invalid_idempotency_key; 401 missing_api_key and invalid_api_key; 402 payment_required; 403 account_suspended and download_unavailable; 404 not_found, image_not_found, upscale_unavailable and preview_unavailable; 409 not_cancellable, preview_not_ready and idempotency_in_progress; 422 idempotency_key_reused; 429 rate_limit_exceeded, creation_rate_limited, random_quota_exceeded, concurrent_limit_exceeded, batch_active_limit_exceeded and monthly_creation_limit_exceeded; 503 generation_disabled, pricing_unavailable, creation_limits_unavailable, search_timeout and catalog_unavailable; 500 internal_error. New codes may be added.
            message: { type: string }
            details:
              description: Optional structured context whose shape depends on code.
            request_id: { type: string, format: uuid }
    PaymentRequiredResponse:
      allOf:
        - $ref: '#/components/schemas/ErrorResponse'
        - type: object
          properties:
            error:
              type: object
              required: [details]
              properties:
                code: { const: payment_required }
                details:
                  type: object
                  required: [credits_required, balance, topup_url, pricing_url]
                  properties:
                    credits_required: { type: number, minimum: 0 }
                    balance: { type: number }
                    topup_url: { type: string, format: uri }
                    pricing_url: { type: string, format: uri }
    RateLimitErrorResponse:
      allOf:
        - $ref: '#/components/schemas/ErrorResponse'
        - type: object
          properties:
            error:
              type: object
              required: [details]
              properties:
                code: { const: rate_limit_exceeded }
                details:
                  type: object
                  required: [limit, reset_at]
                  properties:
                    limit:
                      type: integer
                      minimum: 1
                      description: The per-minute limit that was exceeded.
                    reset_at: { type: string, format: date-time }
                    scope:
                      const: account
                      description: Present when the account-wide limit across all API keys was exceeded; absent for the per-key limit.
    DownloadResponse:
      type: object
      additionalProperties: false
      required: [data, request_id]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [url, expires_at, owned]
          properties:
            url: { type: string, format: uri, description: Authorized original-image URL. Do not log or put in analytics. }
            expires_at: { type: [string, 'null'], format: date-time }
            owned: { const: true, description: Entitlement after authorization; not an indication that no credits were charged. }
        request_id: { type: string, format: uuid }
    ResolutionCredits:
      type: object
      additionalProperties: false
      description: Credits per image by resolution tier. Only offered resolutions appear; 4K is not offered.
      properties:
        1K: { type: number, minimum: 0 }
        2K: { type: number, minimum: 0 }
    Pricing:
      type: object
      additionalProperties: false
      description: The live price book. All currency amounts are USD.
      required: [currency, usd_per_credit, download_credits, generation_credits, generation_credits_by_model, generation_resolutions_by_model, generation_max_prompt_length_by_model, trial_generation_models, api_search_credits, search_credits, trial_credits, generation_requires_paid_account, public_catalog_notice, topup, plans, docs_url]
      properties:
        currency: { type: string, const: usd }
        usd_per_credit:
          type: number
          exclusiveMinimum: 0
          description: USD value of one credit.
        download_credits:
          type: number
          minimum: 0
          description: Credits to download a standard-tier original (same as download_credits_by_tier.standard).
        download_credits_by_tier:
          type: object
          additionalProperties: false
          required: [standard, hd, 4k]
          description: Credits to download an original by its quality tier. The 4K upscale of an original you already own costs the difference between the tiers.
          properties:
            standard: { type: number, minimum: 0 }
            hd: { type: number, minimum: 0 }
            4k: { type: number, minimum: 0 }
        generation_credits:
          type: number
          minimum: 0
          description: Credits for one standard-delivery 1K image with Pix4Less Standard (model id `pix4less-cluster`).
        available_generation_resolutions:
          type: array
          items: { type: string, enum: [1K, 2K] }
          description: Resolutions Pix4Less Standard (model id `pix4less-cluster`) produces natively.
        generation_credits_by_resolution:
          $ref: '#/components/schemas/ResolutionCredits'
          description: Standard-delivery credits per image for Pix4Less Standard (model id `pix4less-cluster`).
        generation_credits_batch_by_resolution:
          $ref: '#/components/schemas/ResolutionCredits'
          description: Batch-delivery credits per image (Pix4Less Standard only).
        batch_discount_percent:
          type: number
          minimum: 0
          maximum: 100
          description: Effective saving of the 1K batch price over the 1K standard price in whole percent, derived from the prices above.
        batch_delivery_window: { type: string }
        batch_max_items_per_batch: { type: integer, minimum: 1 }
        batch_max_active_items_per_user: { type: integer, minimum: 1 }
        batch_supported_models:
          type: array
          items: { type: string }
        monthly_creations_by_plan:
          type: object
          additionalProperties: false
          required: [starter, pro, scale]
          description: Creations allowed per cycle for each subscription plan (see monthly_creations_cycle).
          properties:
            starter: { type: integer, minimum: 1 }
            pro: { type: integer, minimum: 1 }
            scale: { type: integer, minimum: 1 }
        monthly_creations_cycle:
          type: string
          enum: [subscription_billing_period_or_calendar_month]
          description: The cycle monthly_creations_by_plan counts over, which is the current subscription billing period when one is on file and otherwise the calendar month (UTC).
        generation_credits_by_model:
          type: object
          additionalProperties: { $ref: '#/components/schemas/ResolutionCredits' }
          description: Standard-delivery credits per image, keyed by model id, for every model available right now (models whose provider is not configured are not listed) and each resolution it produces natively.
        generation_resolutions_by_model:
          type: object
          additionalProperties:
            type: array
            items: { type: string, enum: [1K, 2K] }
          description: Native resolutions of each model available right now, keyed by model id.
        generation_max_prompt_length_by_model:
          type: object
          additionalProperties: { type: integer, minimum: 1 }
          description: Longest accepted generation query (characters, after trimming) of each model available right now, keyed by model id.
        trial_generation_models:
          type: array
          items: { type: string }
          description: Models that trial credits can use. Other models need paid standing (a completed top-up or plan payment) and otherwise return 402.
        api_search_credits: { type: number, minimum: 0 }
        api_search_credits_by_tier:
          type: object
          additionalProperties: false
          required: [standard, hd, 4k]
          properties:
            standard: { type: number, minimum: 0 }
            hd: { type: number, minimum: 0 }
            4k: { type: number, minimum: 0 }
        search_credits:
          type: object
          additionalProperties: false
          required: [fast, deep]
          description: Credits per completed non-empty search by search_type.
          properties:
            fast: { type: number, minimum: 0 }
            deep: { type: number, minimum: 0 }
        trial_credits: { type: number, minimum: 0 }
        generation_requires_paid_account:
          const: false
          description: Always false. Trial credits can create with the models in trial_generation_models. Kept for compatibility.
        public_catalog_notice:
          type: string
          description: Notice to show before a user's first creation; generated images join the public Pix4Less catalog.
        topup:
          type: object
          additionalProperties: false
          required: [min_usd, max_usd, url]
          properties:
            min_usd: { type: number, minimum: 0 }
            max_usd: { type: number, minimum: 0 }
            url: { type: string, format: uri }
        plans:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [tier, label, usd_per_month, credits_per_month]
            properties:
              tier: { type: string }
              label: { type: string }
              usd_per_month: { type: number, minimum: 0 }
              credits_per_month: { type: number, minimum: 0 }
        docs_url: { type: string, format: uri }
    Preset:
      type: object
      additionalProperties: false
      required: [id, name, label, promptSuffix, isCustom]
      properties:
        id: { type: string }
        name: { type: string }
        label: { type: string }
        promptSuffix: { type: string, maxLength: 200 }
        isCustom: { type: boolean }
        createdAt: { type: string, format: date-time }
    BatchItemRequest:
      type: object
      additionalProperties: false
      required: [query]
      properties:
        custom_id:
          type: string
          maxLength: 100
          description: Your own reference for the item; returned on the created item and in the results.
        query:
          type: string
          minLength: 1
          maxLength: 2000
          description: Nonblank description, up to the pix4less-cluster prompt limit (generation_max_prompt_length_by_model in GET /v1/pricing).
        resolution:
          type: string
          enum: [1K, 2K]
          default: 1K
          description: 4K is not offered; an unsupported resolution fails the whole batch with 400 invalid_item.
        aspect_ratio: { type: string, enum: ['16:9', '1:1', '4:3', '9:16', '4:5'], default: '16:9' }
        negative_prompt: { type: string, maxLength: 500 }
        preset:
          type: string
          minLength: 1
          maxLength: 100
          description: Preset ID (a built-in id or one of your custom preset ids); an unknown ID fails the whole batch with 400 invalid_item.
        style_preset:
          type: string
          minLength: 1
          maxLength: 100
          description: Alias for preset; preset wins when both are sent.
    BatchRequest:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        model:
          type: string
          default: pix4less-cluster
          description: Only pix4less-cluster is accepted.
        completion_window: { type: string, enum: [24h], default: 24h }
        webhook_url:
          type: string
          maxLength: 500
          description: Not supported yet; sending it returns 400 webhook_not_supported. Poll the batch status_url instead.
        items:
          type: array
          minItems: 1
          maxItems: 100
          description: At most batch_max_items_per_batch from GET /v1/pricing, and never more than 100.
          items: { $ref: '#/components/schemas/BatchItemRequest' }
    BatchJob:
      type: object
      required: [id, status, model, total_requests, completed_requests, failed_requests, charged_credits, deadline_at, created_at, updated_at, status_url, results_url]
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [validating, queued, in_progress, completed, partial_failures, failed, cancelled] }
        model: { type: string }
        total_requests: { type: integer }
        completed_requests: { type: integer }
        failed_requests: { type: integer }
        charged_credits:
          type: integer
          description: Credits charged for the whole batch at submission.
        refunded_credits:
          type: integer
          description: Credits refunded so far for items that failed, timed out, or were cancelled.
        completion_window: { type: string }
        deadline_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        status_url: { type: string, pattern: '^/v1/batches/' }
        results_url: { type: string, pattern: '^/v1/batches/' }
    BatchCreatedItem:
      type: object
      additionalProperties: false
      required: [id, custom_id, status, status_url]
      properties:
        id:
          type: string
          format: uuid
          description: Generation request id of the item; poll and cancel it like any generation job.
        custom_id: { type: [string, 'null'] }
        status: { const: pending }
        status_url: { type: string, pattern: '^/v1/generation-requests/' }
    BatchResponse:
      type: object
      required: [data, request_id]
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/BatchJob'
            - type: object
              properties:
                items:
                  type: array
                  description: Present only on the POST /v1/batches response. One entry per submitted item, in request order.
                  items: { $ref: '#/components/schemas/BatchCreatedItem' }
        billing:
          type: object
          properties:
            charged_credits: { type: integer }
            balance: { type: integer }
        request_id: { type: string, format: uuid }
    BatchResultItem:
      type: object
      description: One batch item with the fields of GenerationJob (see that schema), plus custom_id and the credits charged and refunded for this item.
      required: [id, custom_id, status, status_url, charged_credits, refunded_credits]
      properties:
        id: { type: string, description: Generation request id (UUID) of the item. }
        custom_id: { type: [string, 'null'] }
        status: { $ref: '#/components/schemas/GenerationStatus' }
        reject_reason:
          anyOf:
            - { $ref: '#/components/schemas/GenerationFailureCode' }
            - { type: 'null' }
        error_message: { type: [string, 'null'] }
        image:
          anyOf:
            - { $ref: '#/components/schemas/CatalogImage' }
            - { type: 'null' }
        status_url: { type: string, pattern: '^/v1/generation-requests/' }
        charged_credits: { type: integer, minimum: 0 }
        refunded_credits: { type: integer, minimum: 0 }
    BatchResultsResponse:
      type: object
      required: [data, request_id]
      properties:
        data:
          type: object
          required: [batch, items]
          properties:
            batch: { $ref: '#/components/schemas/BatchJob' }
            items:
              type: array
              items: { $ref: '#/components/schemas/BatchResultItem' }
        request_id: { type: string, format: uuid }
