openapi: 3.1.0
info:
  title: Flimpa File Processing API
  version: 1.0.0
  summary: Async PDF and image processing API
  description: |
    Flimpa API provides asynchronous file-processing services.

    Typical flow:
    1. Upload a file via `POST /tasks`
    2. Receive `taskId` and `accessToken`
    3. Poll `GET /tasks/{id}` until `COMPLETED`
    4. Download the result via `resultUrl` or `GET /tasks/{id}/download`

    Base URL: `https://api.flimpa.com/api/v1`
  contact:
    name: Flimpa API Support
    url: https://dev.flimpa.com
    email: api@flimpa.com
  license:
    name: Proprietary
externalDocs:
  description: Developer portal
  url: https://dev.flimpa.com
servers:
  - url: https://api.flimpa.com/api/v1
    description: Production
tags:
  - name: Tasks
    description: Create, poll, and download async file-processing tasks
  - name: Errors
    description: Standard error responses
paths:
  /tasks:
    post:
      tags: [Tasks]
      summary: Create processing task
      description: |
        Upload a file and enqueue an asynchronous processing job.
        Returns immediately with a `taskId` and `accessToken`.
        Poll status until `COMPLETED`, then download the result.
      operationId: createTask
      parameters:
        - $ref: "#/components/parameters/ApiKeyHeader"
        - $ref: "#/components/parameters/IdempotencyKeyHeader"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/CreateTaskRequest"
            encoding:
              file:
                contentType: application/pdf, image/png, image/jpeg, image/gif, image/webp
      responses:
        "202":
          description: Task accepted and queued
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Daily-Remaining:
              $ref: "#/components/headers/XRateLimitDailyRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateTaskResponse"
              examples:
                compressPdf:
                  summary: PDF compression accepted
                  value:
                    taskId: 550e8400-e29b-41d4-a716-446655440000
                    status: PENDING
                    pollUrl: /api/v1/tasks/550e8400-e29b-41d4-a716-446655440000
                    accessToken: "1735689600000.a1b2c3d4e5f6..."
        "200":
          description: Idempotent replay of an existing task
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Daily-Remaining:
              $ref: "#/components/headers/XRateLimitDailyRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateTaskResponse"
              examples:
                replay:
                  value:
                    taskId: 550e8400-e29b-41d4-a716-446655440000
                    status: PENDING
                    pollUrl: /api/v1/tasks/550e8400-e29b-41d4-a716-446655440000
                    accessToken: "1735689600000.a1b2c3d4e5f6..."
                    idempotentReplay: true
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /tasks/{id}:
    get:
      tags: [Tasks]
      summary: Get task status
      description: |
        Poll task status. Requires `X-Task-Access-Token` from the create response.
        When `status` is `COMPLETED`, the response includes `resultUrl`.
      operationId: getTaskStatus
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/TaskAccessTokenHeader"
        - $ref: "#/components/parameters/ApiKeyHeader"
      responses:
        "200":
          description: Current task status
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskStatusResponse"
              examples:
                pending:
                  summary: Still processing
                  value:
                    taskId: 550e8400-e29b-41d4-a716-446655440000
                    status: PROCESSING
                    progress: 40
                    operation: compress-pdf
                    expiresAt: "2026-07-22T13:00:00.000Z"
                    createdAt: "2026-07-22T12:00:00.000Z"
                    updatedAt: "2026-07-22T12:00:15.000Z"
                    attemptCount: 1
                completed:
                  summary: Ready to download
                  value:
                    taskId: 550e8400-e29b-41d4-a716-446655440000
                    status: COMPLETED
                    progress: 100
                    operation: compress-pdf
                    resultUrl: /api/v1/tasks/550e8400-e29b-41d4-a716-446655440000/download
                    expiresAt: "2026-07-22T13:00:00.000Z"
                    createdAt: "2026-07-22T12:00:00.000Z"
                    updatedAt: "2026-07-22T12:00:42.000Z"
                    attemptCount: 1
                failed:
                  summary: Processing failed
                  value:
                    taskId: 550e8400-e29b-41d4-a716-446655440000
                    status: FAILED
                    progress: 0
                    operation: compress-pdf
                    errorMessage: Unsupported or corrupt PDF
                    expiresAt: "2026-07-22T13:00:00.000Z"
                    createdAt: "2026-07-22T12:00:00.000Z"
                    updatedAt: "2026-07-22T12:00:20.000Z"
                    attemptCount: 3
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /tasks/{id}/download:
    get:
      tags: [Tasks]
      summary: Download task result
      description: |
        Download the processed file. Requires `X-Task-Access-Token`.
        Only available when the task status is `COMPLETED`.
        Returns the raw file bytes (not JSON).
      operationId: downloadTaskResult
      parameters:
        - $ref: "#/components/parameters/TaskId"
        - $ref: "#/components/parameters/TaskAccessTokenHeader"
        - $ref: "#/components/parameters/ApiKeyHeader"
      responses:
        "200":
          description: Processed file stream
          headers:
            Content-Disposition:
              description: Attachment filename
              schema:
                type: string
                example: attachment; filename="document.pdf"
            Cache-Control:
              schema:
                type: string
                example: private, no-store
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            image/webp:
              schema:
                type: string
                format: binary
            application/octet-stream:
              schema:
                type: string
                format: binary
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Optional API key (`flp_live_…`). Raises rate limits and enables daily quotas.
        Omit for anonymous IP-based access (lower limits).
  parameters:
    ApiKeyHeader:
      name: X-API-Key
      in: header
      required: false
      description: Optional API key. Format `flp_live_` + base64url secret.
      schema:
        type: string
        pattern: "^flp_live_[A-Za-z0-9_-]+$"
        example: flp_live_xxxxxxxxxxxxxxxxxxxxxxxx
    IdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Optional client-generated key (1–128 characters). Scoped per API key or IP.
        Replaying the same key returns the original task (`200` + `idempotentReplay: true`).
      schema:
        type: string
        minLength: 1
        maxLength: 128
        example: compress-invoice-2026-07-22-001
    TaskAccessTokenHeader:
      name: X-Task-Access-Token
      in: header
      required: true
      description: |
        Task-bound access token from the create response.
        Required for status and download. Expires with the task TTL (1 hour).
      schema:
        type: string
        example: "1735689600000.a1b2c3d4e5f6..."
    TaskId:
      name: id
      in: path
      required: true
      description: Task UUID returned by `POST /tasks`
      schema:
        type: string
        format: uuid
        example: 550e8400-e29b-41d4-a716-446655440000
  headers:
    XRateLimitLimit:
      description: Maximum requests allowed in the current window
      schema:
        type: integer
        example: 60
    XRateLimitRemaining:
      description: Remaining requests in the current window
      schema:
        type: integer
        example: 58
    XRateLimitDailyRemaining:
      description: Remaining create requests for the UTC day (API key only)
      schema:
        type: integer
        example: 1997
    RetryAfter:
      description: Seconds to wait before retrying (429 responses)
      schema:
        type: integer
        example: 12
  schemas:
    Operation:
      type: string
      enum:
        - compress-pdf
        - image-to-webp
      description: |
        Processing operation.
        - `compress-pdf` — input PDF → compressed PDF
        - `image-to-webp` — png/jpg/jpeg/gif/webp → WebP
    TaskStatus:
      type: string
      enum:
        - PENDING
        - PROCESSING
        - COMPLETED
        - FAILED
    CreateTaskRequest:
      type: object
      required:
        - operation
        - file
      properties:
        operation:
          $ref: "#/components/schemas/Operation"
        file:
          type: string
          format: binary
          description: Single input file. Max 20 MB.
        files:
          type: array
          maxItems: 1
          items:
            type: string
            format: binary
          description: Alternative to `file`. Still limited to 1 file in v1.
    CreateTaskResponse:
      type: object
      required:
        - taskId
        - status
        - pollUrl
        - accessToken
      properties:
        taskId:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/TaskStatus"
        pollUrl:
          type: string
          description: Relative URL to poll task status
          example: /api/v1/tasks/550e8400-e29b-41d4-a716-446655440000
        accessToken:
          type: string
          description: Pass as X-Task-Access-Token for status and download
        idempotentReplay:
          type: boolean
          description: True when an existing task was returned for the same Idempotency-Key
    TaskStatusResponse:
      type: object
      required:
        - taskId
        - status
        - progress
        - operation
        - expiresAt
        - createdAt
        - updatedAt
      properties:
        taskId:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/TaskStatus"
        progress:
          type: integer
          minimum: 0
          maximum: 100
        operation:
          $ref: "#/components/schemas/Operation"
        resultUrl:
          type: string
          description: Present only when COMPLETED. Relative download path or short-lived presigned URL.
        errorMessage:
          type: string
          description: Present only when FAILED
        expiresAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        attemptCount:
          type: integer
          minimum: 0
    ErrorResponse:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code
          enum:
            - validation_error
            - unauthorized
            - forbidden
            - not_found
            - conflict
            - payload_too_large
            - unsupported_media_type
            - unprocessable_entity
            - rate_limit_exceeded
            - internal_error
            - service_unavailable
  responses:
    ValidationError:
      description: Invalid request parameters or file
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: 'Field "operation" must be one of: compress-pdf, image-to-webp'
            code: validation_error
    Unauthorized:
      description: Invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Invalid API key
            code: unauthorized
    Forbidden:
      description: Access denied (missing/invalid task token, or anonymous without resolvable IP)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Access denied
            code: forbidden
    NotFound:
      description: Task not found, expired, or result not ready
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Task not found
            code: not_found
    Conflict:
      description: Resource conflict
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Concurrent limit exceeded
            code: conflict
    PayloadTooLarge:
      description: File or body exceeds 20 MB limit
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Request body too large
            code: payload_too_large
    UnsupportedMediaType:
      description: Unsupported Content-Type or file format
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Unsupported media type
            code: unsupported_media_type
    UnprocessableEntity:
      description: Semantically invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Unable to process uploaded file
            code: unprocessable_entity
    RateLimited:
      description: Rate limit or daily quota exceeded
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Too many requests
            code: rate_limit_exceeded
    InternalError:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Internal server error
            code: internal_error
    ServiceUnavailable:
      description: |
        Temporary unavailability — worker offline, queue full, storage or malware scan unavailable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: Processing worker offline
            code: service_unavailable
