openapi: 3.1.0
info:
  title: Kapuí Remove Fundo API
  version: 1.0.0
  license:
    name: Proprietary
    identifier: LicenseRef-Kapui-Proprietary
  description: >-
    API autenticada para remoção de fundo e composição de imagens. Aplicação
    v0.2.0 com o contrato externo v1 preservado, processamento autônomo,
    seleção com contexto integral e canal alfa único na recomposição da máscara.
servers:
  - url: https://ks.api.br
    description: Produção KS API
tags:
  - name: Serviço
    description: Saúde e capacidades operacionais.
  - name: Processamento
    description: Envio, consulta e download de imagens.
security:
  - bearerAuth: []
paths:
  /api/v1/health:
    get:
      tags: [Serviço]
      operationId: getHealth
      summary: Verifica banco e armazenamento
      security: []
      responses:
        "200":
          description: Serviço disponível
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
        "400":
          description: HTTPS obrigatório
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "503":
          description: Serviço degradado ou desativado
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/Health"
                  - $ref: "#/components/schemas/ErrorResponse"
  /api/v1/capabilities:
    get:
      tags: [Serviço]
      operationId: getCapabilities
      summary: Lista capacidades e disponibilidade dos workers
      description: Exige a permissão read_job.
      responses:
        "200":
          description: Capacidades atuais
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Capabilities"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /api/v1/process:
    post:
      tags: [Processamento]
      operationId: processImage
      summary: Envia uma imagem para remoção de fundo
      description: >-
        Exige process_image. Com sync=true, aguarda até o limite configurado.
        Mesmo com response_mode=image, pode responder 202 JSON quando o job
        não terminar dentro da janela síncrona.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/ProcessRequest"
      responses:
        "200":
          description: Job concluído em JSON ou imagem direta
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
            image/png:
              schema: { type: string, contentEncoding: binary }
            image/webp:
              schema: { type: string, contentEncoding: binary }
            image/jpeg:
              schema: { type: string, contentEncoding: binary }
        "202":
          description: Job recebido e ainda não concluído
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          description: Arquivo acima do limite
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "415":
          description: Content-Type inválido
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "422":
          description: Imagem/parâmetro inválido ou job que falhou durante a espera síncrona
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/Job"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /api/v1/jobs/{job_id}:
    parameters:
      - $ref: "#/components/parameters/JobId"
    get:
      tags: [Processamento]
      operationId: getJob
      summary: Consulta um job do próprio usuário
      description: Exige read_job.
      responses:
        "200":
          description: Estado atual
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Job" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          description: Job com falha de processamento
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Job" }
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /api/v1/jobs/{job_id}/result:
    parameters:
      - $ref: "#/components/parameters/JobId"
    get:
      tags: [Processamento]
      operationId: downloadResult
      summary: Baixa o resultado de um job concluído
      description: Exige read_result.
      responses:
        "200":
          description: Arquivo processado
          headers:
            Content-Disposition:
              schema: { type: string }
          content:
            image/png:
              schema: { type: string, contentEncoding: binary }
            image/webp:
              schema: { type: string, contentEncoding: binary }
            image/jpeg:
              schema: { type: string, contentEncoding: binary }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Resultado ainda não disponível
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: KRF API Key
  parameters:
    JobId:
      name: job_id
      in: path
      required: true
      schema:
        type: string
        pattern: ^KRF-[0-9]{8}-[A-F0-9]{8}$
      example: KRF-20260812-AB12CD34
  responses:
    Unauthorized:
      description: Credencial ausente ou inválida
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Forbidden:
      description: Credencial desativada ou sem permissão
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    NotFound:
      description: Job não encontrado para o usuário autenticado
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    RateLimited:
      description: Limite por usuário excedido
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    InternalError:
      description: Erro interno com X-KRF-Request-ID para diagnóstico
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    ServiceUnavailable:
      description: API desativada ou temporariamente indisponível
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
  schemas:
    Health:
      type: object
      required: [status, service, api_version]
      properties:
        status:
          type: string
          enum: [ok, degraded]
        service:
          type: string
          const: Kapui Remove Fundo
        api_version:
          type: string
          const: "1.0"
    Capabilities:
      type: object
      required:
        - success
        - formats
        - transparent_output
        - processing_backend
        - processor_available
        - online_workers
        - max_upload_mb
      properties:
        success: { type: boolean, const: true }
        formats:
          type: array
          items: { type: string, enum: [png, webp, jpeg] }
        transparent_output: { type: boolean }
        trim: { type: boolean }
        margin: { type: boolean }
        margin_percent: { type: boolean }
        resize: { type: boolean }
        resize_modes:
          type: array
          items: { type: string, enum: [original, contain, fit] }
        canvas: { type: boolean }
        input_crop: { type: boolean }
        input_crop_modes:
          type: array
          items: { type: string, enum: [rectangle, polygon] }
        crop_coordinate_space:
          type: string
          enum: [normalized_source_pixels]
        position: { type: boolean }
        rotation:
          type: array
          items: { type: integer, enum: [0, 90, 180, 270] }
        normalize_orientation: { type: boolean }
        auto_rotate: { type: boolean }
        background_removal: { type: boolean }
        processing_backend: { type: string, example: node_worker }
        processor_available: { type: boolean }
        online_workers: { type: integer, minimum: 0 }
        max_upload_mb: { type: integer, minimum: 1 }
    ProcessRequest:
      type: object
      required: [image]
      properties:
        image:
          type: string
          contentEncoding: binary
        output_format:
          type: string
          enum: [png, webp, jpeg]
          default: png
        background:
          type: string
          default: transparent
          description: transparent ou #RRGGBB; JPEG exige cor.
        trim: { type: boolean, default: true }
        margin: { type: integer, minimum: 0, maximum: 1000, default: 20 }
        position:
          type: string
          enum:
            - center
            - left
            - right
            - top
            - bottom
            - top-left
            - top-center
            - top-right
            - center-left
            - center-right
            - bottom-left
            - bottom-center
            - bottom-right
          description: Atalho opcional. Os campos horizontal_position e vertical_position, quando enviados, têm precedência no próprio eixo.
        horizontal_position:
          type: string
          enum: [left, center, right]
          default: center
        vertical_position:
          type: string
          enum: [top, center, bottom]
          default: center
        normalize_orientation: { type: boolean, default: true }
        auto_rotate: { type: boolean, default: false }
        rotation:
          type: integer
          enum: [0, 90, 180, 270]
          default: 0
        canvas_width: { type: integer, minimum: 0, maximum: 10000, default: 0 }
        canvas_height: { type: integer, minimum: 0, maximum: 10000, default: 0 }
        resize_mode:
          type: string
          enum: [original, contain, fit]
          default: contain
        max_width: { type: integer, minimum: 0, maximum: 10000, default: 0 }
        max_height: { type: integer, minimum: 0, maximum: 10000, default: 0 }
        preserve_aspect_ratio: { type: boolean, const: true, default: true }
        quality: { type: integer, minimum: 1, maximum: 100, default: 95 }
        crop_mode:
          type: string
          enum: [none, rectangle, polygon]
          default: none
          description: Seleção definida após a orientação; a IA conserva o contexto integral e o corte é aplicado à máscara pronta.
        crop_x:
          type: integer
          minimum: 0
          maximum: 10000
          default: 0
          description: Coordenada X do retângulo na imagem normalizada.
        crop_y:
          type: integer
          minimum: 0
          maximum: 10000
          default: 0
          description: Coordenada Y do retângulo na imagem normalizada.
        crop_width:
          type: integer
          minimum: 0
          maximum: 10000
          default: 0
        crop_height:
          type: integer
          minimum: 0
          maximum: 10000
          default: 0
        crop_polygon:
          type: string
          maxLength: 8192
          description: JSON com 3 a 64 objetos {x,y}; obrigatório quando crop_mode=polygon.
          examples:
            - '[{"x":120,"y":80},{"x":920,"y":100},{"x":860,"y":720},{"x":150,"y":690}]'
        response_mode:
          type: string
          enum: [json, image]
          default: json
        sync: { type: boolean, default: true }
        source_system: { type: string, maxLength: 80 }
        job_reference: { type: string, maxLength: 120 }
        product_id: { type: string, maxLength: 80 }
        client_reference: { type: string, maxLength: 160 }
    Job:
      type: object
      required: [success, job_id, status, references]
      properties:
        success: { type: boolean }
        job_id:
          type: string
          pattern: ^KRF-[0-9]{8}-[A-F0-9]{8}$
        status:
          type: string
          enum: [received, queued, processing, completed, failed, expired]
        created_at: { type: [string, "null"] }
        updated_at: { type: [string, "null"] }
        references:
          $ref: "#/components/schemas/References"
        poll_endpoint: { type: string }
        result:
          $ref: "#/components/schemas/Result"
        processing:
          type: object
          additionalProperties: true
        duration_ms: { type: [integer, "null"], minimum: 0 }
        error:
          $ref: "#/components/schemas/Error"
    References:
      type: object
      required: [source_system, job_reference, product_id, client_reference]
      properties:
        source_system: { type: [string, "null"] }
        job_reference: { type: [string, "null"] }
        product_id: { type: [string, "null"] }
        client_reference: { type: [string, "null"] }
    Result:
      type: object
      properties:
        format: { type: string, enum: [png, webp, jpeg] }
        width: { type: integer, minimum: 1 }
        height: { type: integer, minimum: 1 }
        size_bytes: { type: integer, minimum: 1 }
        transparent: { type: boolean }
        original_width: { type: integer, minimum: 1 }
        original_height: { type: integer, minimum: 1 }
        product_box:
          $ref: "#/components/schemas/Box"
        margin: { type: integer, minimum: 0 }
        download_endpoint: { type: string }
    Box:
      type: object
      required: [x, y, width, height]
      properties:
        x: { type: integer, minimum: 0 }
        y: { type: integer, minimum: 0 }
        width: { type: integer, minimum: 1 }
        height: { type: integer, minimum: 1 }
    ErrorResponse:
      type: object
      required: [success, error]
      properties:
        success: { type: boolean, const: false }
        error:
          $ref: "#/components/schemas/Error"
    Error:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }
        details:
          type: object
          additionalProperties: true
