openapi: 3.1.0
info:
  title: MAXScanner Document Processing API
  version: 1.0.0
  description: >-
    Asynchronous document processing for conversion, OCR, image optimization,
    PDF operations, saved-account documents and webpage workflows. Processor
    selection is server-owned.
servers:
  - url: https://maxscanner.app/api/v1
security:
  - bearerAuth: []
tags:
  - name: Jobs
  - name: Processing
  - name: Documents
  - name: Usage
paths:
  /jobs:
    post:
      tags: [Jobs]
      summary: Create an asynchronous document job
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobRequest'
      responses:
        '202': { description: Job accepted, content: { application/json: { schema: { $ref: '#/components/schemas/JobCreated' } } } }
        '200': { description: Existing idempotent job returned, content: { application/json: { schema: { $ref: '#/components/schemas/JobCreated' } } } }
        '401': { description: Authentication required }
        '402': { description: Quota or credits exhausted }
        '422': { description: Unsupported or invalid processing request }
  /jobs/{jobId}:
    get:
      tags: [Jobs]
      summary: Get asynchronous job status
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200': { description: Current job state, content: { application/json: { schema: { $ref: '#/components/schemas/Job' } } } }
        '404': { description: Job not found }
  /documents:
    get:
      tags: [Documents]
      summary: Search and list account-owned documents
      description: Returns canonical saved documents owned by the authenticated account.
      parameters:
        - name: q
          in: query
          schema: { type: string, maxLength: 120 }
          description: Case-insensitive filename search.
        - name: type
          in: query
          schema: { type: string, maxLength: 120 }
        - name: folderId
          in: query
          schema: { type: string, maxLength: 120 }
        - name: groupId
          in: query
          schema: { type: string, maxLength: 120 }
        - name: source
          in: query
          schema: { type: string, maxLength: 80 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        '200': { description: Matching account documents, content: { application/json: { schema: { $ref: '#/components/schemas/DocumentListResponse' } } } }
        '403': { description: Credential lacks documents:read scope }
  /documents/search:
    get:
      tags: [Documents]
      summary: Search the document library with metadata and OCR-token filters
      description: >-
        Searches account-owned documents. Filename, MIME type, folder, group,
        status, tags and date filters are supported. OCR matching uses the
        optional normalized ocrTokens field when available; this endpoint does
        not pretend to provide semantic full-text search until a dedicated
        search index is deployed.
      parameters:
        - { name: q, in: query, schema: { type: string, maxLength: 120 } }
        - { name: type, in: query, schema: { type: string, maxLength: 120 } }
        - { name: status, in: query, schema: { type: string, maxLength: 80 } }
        - { name: folderId, in: query, schema: { type: string, maxLength: 120 } }
        - { name: groupId, in: query, schema: { type: string, maxLength: 120 } }
        - { name: tags, in: query, schema: { type: string, maxLength: 500 }, description: Comma-separated tags; all supplied tags must match. }
        - { name: from, in: query, schema: { type: integer } }
        - { name: to, in: query, schema: { type: integer } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
      responses:
        '200': { description: Filtered document results, content: { application/json: { schema: { $ref: '#/components/schemas/DocumentSearchResponse' } } } }
  /documents/import:
    post:
      tags: [Documents]
      summary: Import a public remote file into the account library
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentImportRequest'
      responses:
        '201': { description: Document imported, content: { application/json: { schema: { $ref: '#/components/schemas/DocumentImportResponse' } } } }
        '400': { description: Invalid request }
        '413': { description: File exceeds import limit }
        '422': { description: Remote source rejected or unavailable }
  /documents/{documentId}:
    get:
      tags: [Documents]
      summary: Get a saved document and its recent versions
      parameters:
        - name: documentId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Document detail and versions, content: { application/json: { schema: { $ref: '#/components/schemas/DocumentDetailResponse' } } } }
        '404': { description: Document not found in the authenticated account }
  /documents/{documentId}/process:
    post:
      tags: [Documents]
      summary: Process an existing saved document
      description: Uses the current private artifact without exposing its R2 object key.
      parameters:
        - name: documentId
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcessSavedDocumentRequest'
      responses:
        '202': { description: Processing job accepted, content: { application/json: { schema: { $ref: '#/components/schemas/SavedDocumentJobCreated' } } } }
        '200': { description: Existing idempotent job returned, content: { application/json: { schema: { $ref: '#/components/schemas/SavedDocumentJobCreated' } } } }
        '404': { description: Document not found in the authenticated account }
        '409': { description: Document has no processable current version }
        '422': { description: Invalid operation or processing steps }
  /documents/{documentId}/pages/{pageId}/process:
    post:
      tags: [Documents]
      summary: Process one saved document page
      description: >-
        Processes a single account-owned page without exposing its private
        storage key. Useful for scan cleanup, crop/resize/compress, OCR and
        page-to-PDF workflows.
      parameters:
        - name: documentId
          in: path
          required: true
          schema: { type: string }
        - name: pageId
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcessSavedDocumentRequest'
      responses:
        '202': { description: Page processing job accepted, content: { application/json: { schema: { $ref: '#/components/schemas/PageJobCreated' } } } }
        '200': { description: Existing idempotent job returned, content: { application/json: { schema: { $ref: '#/components/schemas/PageJobCreated' } } } }
        '404': { description: Document or page not found in the authenticated account }
        '409': { description: Page source is unavailable }
        '422': { description: Invalid operation or processing steps }
  /pipeline:
    post:
      tags: [Processing]
      summary: Run a chained cloud processing pipeline
      description: Account-owned source only. Heavy work runs in the pipeline worker.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PipelineRequest'
      responses:
        '202': { description: Pipeline accepted, content: { application/json: { schema: { $ref: '#/components/schemas/JobCreated' } } } }
        '403': { description: Source does not belong to the account }
        '503': { description: Pipeline processor unavailable }
  /formats:
    get:
      tags: [Processing]
      summary: List server-owned processing capabilities
      security: []
      responses:
        '200': { description: Supported operations and formats, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
  /convert:
    post:
      tags: [Processing]
      summary: Compatibility conversion endpoint
      description: Prefer /jobs for new integrations.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        '200': { description: Converted response }
        '202': { description: Conversion accepted }
  /ocr:
    post:
      tags: [Processing]
      summary: Compatibility OCR endpoint
      description: Prefer /pipeline for new account-owned cloud workflows.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        '200': { description: OCR response }
        '202': { description: OCR accepted }
  /webpage-to-pdf:
    post:
      tags: [Processing]
      summary: Convert a public webpage to PDF
      description: Compatibility endpoint using the dedicated Chromium processor.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                filename: { type: string }
      responses:
        '200': { description: PDF stream }
        '400': { description: Invalid or blocked URL }
        '502': { description: Chromium processor failure }
  /usage:
    get:
      tags: [Usage]
      summary: Inspect current API plan and usage
      responses:
        '200': { description: Usage information, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API_KEY
      description: MAXScanner server-side API key.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Stable key for a logical submission. Retries with the same key do not create duplicate billable jobs.
      schema: { type: string, minLength: 8, maxLength: 200 }
    IdempotencyKeyOptional:
      name: Idempotency-Key
      in: header
      required: false
      description: Stable key for safe retries.
      schema: { type: string, minLength: 8, maxLength: 200 }
    JobId:
      name: jobId
      in: path
      required: true
      schema: { type: string }
  schemas:
    CreateJobRequest:
      type: object
      required: [filename, format]
      properties:
        filename: { type: string, maxLength: 255 }
        inputFormat: { type: string, example: docx }
        format: { type: string, example: pdf }
        sourceKey: { type: string, description: Account-owned private storage key }
        url: { type: string, format: uri, description: Supported public HTTPS source URL }
        operations: { type: array, items: { type: object, additionalProperties: true } }
        metadata: { type: object, additionalProperties: true }
      additionalProperties: false
    DocumentImportRequest:
      type: object
      required: [url]
      properties:
        url: { type: string, format: uri, maxLength: 2048 }
        filename: { type: string, maxLength: 180 }
        contentType: { type: string, maxLength: 120 }
        folderId: { type: string, maxLength: 120 }
        groupId: { type: string, maxLength: 120 }
      additionalProperties: false
    DocumentImportResponse:
      type: object
      required: [documentId, versionId, filename, contentType, sizeBytes, status]
      properties:
        documentId: { type: string }
        versionId: { type: string }
        filename: { type: string }
        contentType: { type: string }
        sizeBytes: { type: integer }
        groupId: { type: string }
        folderId: { type: string }
        downloadUrl: { type: string, format: uri }
        status: { const: imported }
    DocumentSummary:
      type: object
      required: [id, filename, contentType, folderId, versionCount]
      properties:
        id: { type: string }
        filename: { type: string }
        contentType: { type: string }
        sizeBytes: { type: [integer, 'null'] }
        folderId: { type: string }
        groupId: { type: [string, 'null'] }
        currentVersionId: { type: [string, 'null'] }
        versionCount: { type: integer }
        source: { type: [string, 'null'] }
        createdAt: { type: [integer, 'null'] }
        updatedAt: { type: [integer, 'null'] }
    DocumentListResponse:
      type: object
      required: [documents, count, hasMore, scanned]
      properties:
        documents: { type: array, items: { $ref: '#/components/schemas/DocumentSummary' } }
        count: { type: integer }
        hasMore: { type: boolean }
        scanned: { type: integer }
    DocumentSearchResponse:
      type: object
      required: [documents, count, scanned, hasMore, search]
      properties:
        documents: { type: array, items: { $ref: '#/components/schemas/DocumentSummary' } }
        count: { type: integer }
        scanned: { type: integer }
        hasMore: { type: boolean }
        search: { type: object, additionalProperties: true }
    DocumentDetailResponse:
      type: object
      required: [document, versions]
      properties:
        document: { $ref: '#/components/schemas/DocumentSummary' }
        versions:
          type: array
          items:
            type: object
            additionalProperties: true
    ProcessSavedDocumentRequest:
      type: object
      required: [operation]
      properties:
        operation: { type: string, enum: [convert, image, pipeline] }
        format: { type: string, example: pdf }
        pipeline: { type: array, minItems: 1, maxItems: 8, items: { type: object, additionalProperties: true } }
        steps: { type: array, minItems: 1, maxItems: 8, items: { type: object, additionalProperties: true } }
      additionalProperties: false
    SavedDocumentJobCreated:
      allOf:
        - $ref: '#/components/schemas/JobCreated'
        - type: object
          required: [documentId, versionId]
          properties:
            documentId: { type: string }
            versionId: { type: string }
    PageJobCreated:
      allOf:
        - $ref: '#/components/schemas/JobCreated'
        - type: object
          required: [documentId, pageId, versionId]
          properties:
            documentId: { type: string }
            pageId: { type: string }
            versionId: { type: string }
    PipelineRequest:
      type: object
      required: [sourceKey, steps]
      properties:
        sourceKey: { type: string, description: Account-owned private storage key }
        documentId: { type: string }
        versionId: { type: string }
        filename: { type: string, maxLength: 180 }
        steps:
          type: array
          minItems: 1
          maxItems: 8
          items: { type: object, additionalProperties: true }
    JobCreated:
      type: object
      required: [jobId, status, created, statusUrl]
      properties:
        jobId: { type: string }
        status: { type: string, enum: [queued, processing, retrying, completed, failed] }
        created: { type: boolean }
        statusUrl: { type: string }
        resultUrl: { type: string, format: uri }
        processor: { type: string, description: Internal diagnostic only; clients must not depend on it }
    Job:
      allOf:
        - $ref: '#/components/schemas/JobCreated'
        - type: object
          properties:
            artifactUrl: { type: string, format: uri }
            error:
              type: object
              properties:
                code: { type: string }
                message: { type: string }
