openapi: 3.1.0
info:
  title: Comprestack API
  version: "1.0.0"
  summary: Compress and convert images over HTTPS
  description: |
    Official OpenAPI description of the **public** Comprestack REST API (`/api/v1`).

    Use this document as the source of truth for official SDKs and codegen.
    Compress and convert finish in the same request (synchronous jobs). Results
    expire after about one hour.

    **Not included here:** master-key admin routes (accounts, capacity, metrics,
    room provisioning). Those are internal to Comprestack operators.

    Docs: https://www.comprestack.com/docs
  contact:
    name: Comprestack
    email: hello@comprestack.com
    url: https://www.comprestack.com
  license:
    name: Proprietary
    url: https://www.comprestack.com/terms

servers:
  - url: https://api.comprestack.com/api/v1
    description: Production
  - url: http://127.0.0.1:4320/api/v1
    description: Local development

tags:
  - name: Images
    description: Compress (same format) or convert (change format) one or more uploads.
  - name: Jobs
    description: Retrieve or download completed jobs. Jobs expire after ~1 hour.
  - name: Usage
    description: Plan meters shared by the website and API (self-serve) or negotiated room limits.
  - name: White label
    description: Billing report for provisioned white-label (Rent a Room) keys.

security:
  - bearerAuth: []
  - apiKeyAuth: []

paths:
  /compress:
    post:
      tags: [Images]
      operationId: compressImages
      summary: Compress images
      description: |
        Shrink images while keeping their format. Multipart upload.
        Free: up to 5 files per request. Pro/Business: up to 10.
        A file that cannot shrink returns `unchanged: true` and is not charged
        (except EXIF-only shrink failures → `cannot_shrink`).
      parameters:
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/CompressRequest"
      responses:
        "200":
          description: Job completed successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/FileTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedFormat"
        "422":
          $ref: "#/components/responses/Unprocessable"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Busy"

  /convert:
    post:
      tags: [Images]
      operationId: convertImages
      summary: Convert images
      description: |
        Change format and compress in one step. `format` is required.
        Prefer WebP or AVIF for photos when you can choose the delivery format.
      parameters:
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/ConvertRequest"
      responses:
        "200":
          description: Job completed successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/FileTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedFormat"
        "422":
          $ref: "#/components/responses/Unprocessable"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Busy"

  /jobs/{id}:
    get:
      tags: [Jobs]
      operationId: getJob
      summary: Retrieve a job
      description: |
        Jobs complete before the compress/convert response returns, so status is
        always `done` while the job exists. After expiry (~1 hour) this returns 404.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/DownloadTokenQuery"
        - $ref: "#/components/parameters/DownloadTokenHeader"
      security:
        - bearerAuth: []
        - apiKeyAuth: []
        - downloadToken: []
      responses:
        "200":
          description: Job metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /jobs/{id}/download:
    get:
      tags: [Jobs]
      operationId: downloadJob
      summary: Download job results
      description: |
        ZIP of all output files, or a single file when the job has one image.
        Prefer the `download_url` returned on the job (includes `token`).
        Resolve relative `download_url` paths against `https://api.comprestack.com`.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/DownloadTokenQuery"
        - $ref: "#/components/parameters/DownloadTokenHeader"
      security:
        - bearerAuth: []
        - apiKeyAuth: []
        - downloadToken: []
      responses:
        "200":
          description: Binary download (ZIP or single image).
          content:
            application/zip:
              schema:
                type: string
                format: binary
            application/octet-stream:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
            image/webp:
              schema:
                type: string
                format: binary
            image/avif:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /jobs/{id}/files/{index}:
    get:
      tags: [Jobs]
      operationId: downloadJobFile
      summary: Download one file from a job
      parameters:
        - $ref: "#/components/parameters/JobId"
        - name: index
          in: path
          required: true
          description: Zero-based file index from the job `files` array.
          schema:
            type: integer
            minimum: 0
        - $ref: "#/components/parameters/DownloadTokenQuery"
        - $ref: "#/components/parameters/DownloadTokenHeader"
      security:
        - bearerAuth: []
        - apiKeyAuth: []
        - downloadToken: []
      responses:
        "200":
          description: Single output file.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /usage:
    get:
      tags: [Usage]
      operationId: getUsage
      summary: Check usage
      description: |
        Plan, quota, packs, overage meters, and limits for the authenticated key.
        Self-serve accounts share this allowance with the website.
      responses:
        "200":
          description: Current usage snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Usage"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /rooms/{id}/report:
    get:
      tags: [White label]
      operationId: getRoomBillingReport
      summary: White-label billing report
      description: |
        Period-by-period usage for a provisioned room. A room API key may only
        read its own `id`. Charge **closed** periods only.
        Do not send `X-Account-Id` with a room key.
      parameters:
        - name: id
          in: path
          required: true
          description: White-label room id (e.g. `acme-images`).
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,64}$"
      responses:
        "200":
          description: Billing report.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingReport"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: csk_live
      description: |
        Customer API key from the dashboard (`csk_live_…`) or a provisioned
        white-label room key. Prefer `Authorization: Bearer <key>`.
        Never send the key from browsers or mobile apps.
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Alternative to Bearer auth. Same key material.
    downloadToken:
      type: apiKey
      in: query
      name: token
      description: |
        Job download token from `download_url`. May also be sent as
        `X-Download-Token`. Alone it authorizes get/download for that job only.

  parameters:
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      description: Optional client correlation id (echoed in logs / support).
      schema:
        type: string
        maxLength: 128
    JobId:
      name: id
      in: path
      required: true
      description: Job id returned by compress/convert (e.g. `job_…`).
      schema:
        type: string
    DownloadTokenQuery:
      name: token
      in: query
      required: false
      description: Download token from `download_url` (alternative to API key).
      schema:
        type: string
    DownloadTokenHeader:
      name: X-Download-Token
      in: header
      required: false
      description: Same as `token` query parameter.
      schema:
        type: string

  schemas:
    Error:
      type: object
      required: [error, code, message]
      properties:
        error:
          type: string
          description: Human-readable error (same as `message`).
        code:
          $ref: "#/components/schemas/ErrorCode"
        message:
          type: string

    ErrorCode:
      type: string
      enum:
        - invalid_request
        - cannot_shrink
        - unauthorized
        - limit_reached
        - overage_cap_reached
        - plan_required
        - room_suspended
        - not_found
        - file_too_large
        - unsupported_format
        - too_many_files
        - processing_timeout
        - rate_limited
        - server_error
        - busy
        - shutting_down
        - client_closed

    OutputFormat:
      type: string
      description: Convert target format.
      enum: [webp, avif, jpeg, jpg, png, gif, tiff, tif, bmp, ico]

    Quality:
      description: |
        Preset `balanced` (default), `high`, or `smallest`.
        Pro/Business may also send an integer 1–100.
      oneOf:
        - type: string
          enum: [balanced, high, smallest]
        - type: integer
          minimum: 1
          maximum: 100

    ImageOptions:
      type: object
      properties:
        quality:
          $ref: "#/components/schemas/Quality"
        target_kb:
          type: integer
          minimum: 10
          maximum: 102400
          description: Pro/Business. Aim for this file size in KB.
        lossless:
          type: boolean
          description: Pro/Business. Pixel-perfect where the codec allows.
        maxWidth:
          type: integer
          minimum: 640
          maximum: 2560
          description: Never enlarges; values outside range are clamped.
        maxHeight:
          type: integer
          minimum: 480
          maximum: 2560
        keep_metadata:
          type: boolean
          default: false
          description: Keep EXIF/color profile. Default strips GPS and metadata.

    CompressRequest:
      allOf:
        - $ref: "#/components/schemas/ImageOptions"
        - type: object
          required: [files]
          properties:
            files:
              type: array
              minItems: 1
              maxItems: 10
              items:
                type: string
                format: binary
              description: |
                One or more images. Field name may be `files[]`, `files`, or `image`.
                Batch max: 5 Free, 10 Pro/Business.

    ConvertRequest:
      allOf:
        - $ref: "#/components/schemas/ImageOptions"
        - type: object
          required: [files, format]
          properties:
            format:
              $ref: "#/components/schemas/OutputFormat"
            files:
              type: array
              minItems: 1
              maxItems: 10
              items:
                type: string
                format: binary

    JobFile:
      type: object
      required: [index, name, original_size, size, saved_percent, unchanged, width, height, format]
      properties:
        index:
          type: integer
          minimum: 0
        name:
          type: string
        original_size:
          type: integer
          description: Input bytes.
        size:
          type: integer
          description: Output bytes.
        saved_percent:
          type: number
        unchanged:
          type: boolean
        width:
          type: integer
        height:
          type: integer
        format:
          type: string

    Job:
      type: object
      required: [id, object, mode, status, files, created_at, expires_at]
      properties:
        id:
          type: string
        object:
          type: string
          const: job
        mode:
          type: string
          enum: [compress, convert]
        status:
          type: string
          const: done
        format:
          type: [string, "null"]
          description: Requested convert format, or null for compress.
        account_id:
          type: [string, "null"]
        files:
          type: array
          items:
            $ref: "#/components/schemas/JobFile"
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        images_used:
          type: integer
          description: Files in the job (GET job). Compress/convert responses override with charged count.

    JobResult:
      allOf:
        - $ref: "#/components/schemas/Job"
        - type: object
          required: [images_used, usage, download_url]
          properties:
            images_used:
              type: integer
              description: Images charged for this request (excludes unchanged / failed batch members).
            usage:
              $ref: "#/components/schemas/Usage"
            download_url:
              type: string
              description: |
                Path beginning with `/api/v1/…` including `token`.
                Prefix with `https://api.comprestack.com` before fetching.

    Usage:
      type: object
      required: [object, plan, images_used, images_limit, remaining]
      properties:
        object:
          type: string
          const: usage
        plan:
          type: string
          enum: [free, pro, business]
        status:
          type: string
          enum: [active, suspended]
        display_name:
          type: [string, "null"]
        concurrency_limit:
          type: [integer, "null"]
        reserved_images_per_day:
          type: integer
        reserved_concurrency:
          type: integer
        images_used:
          type: integer
        images_limit:
          type: integer
        pack_images:
          type: integer
        remaining:
          type: integer
          description: Monthly remainder plus pack images (not including overage headroom).
        overage_allowed:
          type: boolean
        overage_images:
          type: integer
        overage_cap:
          type: [integer, "null"]
        overage_per_thousand_usd:
          type: [number, "null"]
        overage_cost_usd:
          type: [number, "null"]
        overage_cost_micros:
          type: integer
        max_file_bytes:
          type: integer
        max_request_bytes:
          type: integer
        max_file_size_mb:
          type: number
        batch_limit:
          type: integer
        lossless:
          type: boolean
        custom_quality:
          type: boolean
        seats:
          type: integer
        rate_limit_per_minute:
          type: integer
        billing_anchor_day:
          type: integer
        period_start:
          type: string
          format: date-time
        resets_at:
          type: string
          format: date-time
        files_expire_after_seconds:
          type: integer
          example: 3600

    BillingPeriod:
      type: object
      properties:
        period_start:
          type: string
        period_end:
          type: string
        closed:
          type: boolean
        images_used:
          type: integer
        overage_images:
          type: integer
        overage_cost_usd:
          type: number
        overage_cost_micros:
          type: integer

    BillingReport:
      type: object
      required: [object, id, usage, periods, billing]
      properties:
        object:
          type: string
          const: billing_report
        id:
          type: string
        status:
          type: string
        display_name:
          type: [string, "null"]
        settings:
          type: object
          additionalProperties: true
        usage:
          $ref: "#/components/schemas/Usage"
        periods:
          type: array
          items:
            $ref: "#/components/schemas/BillingPeriod"
        billing:
          type: object
          properties:
            currency:
              type: string
              example: USD
            amount_unit:
              type: string
            open_period_overage_cost_micros:
              type: integer
            closed_periods_overage_cost_micros:
              type: integer
            closed_periods_overage_cost_usd:
              type: number
            billable_period_starts:
              type: array
              items:
                type: string
            note:
              type: string
        active_keys:
          type: integer

  responses:
    BadRequest:
      description: Missing/invalid parameter, unreadable file, or cannot_shrink.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            invalid:
              value: { error: "format is required", code: invalid_request, message: "format is required" }
            cannot_shrink:
              value:
                {
                  error: "Could not produce a smaller file",
                  code: cannot_shrink,
                  message: "Could not produce a smaller file",
                }
    Unauthorized:
      description: Missing, wrong, or revoked API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example: { error: Unauthorized, code: unauthorized, message: Unauthorized }
    PaymentRequired:
      description: Quota exhausted (`limit_reached`) or overage cap hit (`overage_cap_reached`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Feature requires a higher plan, or room suspended.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Job or room not found / expired.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            { error: "Job not found or expired", code: not_found, message: "Job not found or expired" }
    FileTooLarge:
      description: Upload exceeds plan file size.
      headers:
        X-Request-Id:
          schema: { type: string }
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            {
              error: "team-offsite.jpg is larger than 25 MB",
              code: file_too_large,
              message: "team-offsite.jpg is larger than 25 MB",
            }
    UnsupportedFormat:
      description: Unsupported input/output type.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unprocessable:
      description: Too many files or processing timeout.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Too many requests this minute.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Busy:
      description: At capacity or shutting down.
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
