openapi: 3.1.0
info:
  title: ADA PDF Gatekeeper API
  version: "1.0.0"
  description: >
    REST API for in-memory PDF accessibility analysis and remediation. Designed for zero-storage,
    privacy-first integrations (CMS plugins, workers, backends).
    Recommended single-step flow: POST /v1/process returns analysis plus remediation in one response,
    with an optional temporary download URL for an accessibility-enhanced PDF.
    This process improves accessibility and helps meet ADA/WCAG guidelines, but does not constitute
    a formal compliance certification. Outputs are not PDF/UA guarantees and OCR-rebuilt documents
    may differ visually from the source.
    Error responses use `{ "error": true, "message": "..." }` unless noted.
    When `REQUIRE_API_KEY_FOR_BATCH_AND_REMEDIATE=false` on the server, `POST /v1/batch`,
    `POST /v1/remediate`, and `GET /v1/batch/{batch_id}` may be called without an API key
    (development only).

servers:
  - url: https://api.example.com
    description: Production
  - url: http://127.0.0.1:3002
    description: Local development

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key

  schemas:
    ProcessingStatus:
      type: string
      enum: [processing, completed, failed]

    ComplianceStatus:
      type: string
      nullable: true
      enum: [compliant, partially_compliant, not_compliant]

    ScanIssue:
      type: object
      properties:
        id:
          type: string
        severity:
          type: string
          enum: [critical, warning, info]
        title:
          type: string
        description:
          type: string
        recommendation:
          type: string
      required: [id, severity, title, description, recommendation]

    ScanIssuesBySeverity:
      type: object
      properties:
        critical:
          type: array
          items:
            $ref: "#/components/schemas/ScanIssue"
        warning:
          type: array
          items:
            $ref: "#/components/schemas/ScanIssue"
        info:
          type: array
          items:
            $ref: "#/components/schemas/ScanIssue"
      required: [critical, warning, info]

    ScanSource:
      type: object
      properties:
        type:
          type: string
          enum: [upload, url]
        label:
          type: string
      required: [type, label]

    ScanResult:
      type: object
      properties:
        id:
          type: string
        source:
          $ref: "#/components/schemas/ScanSource"
        processing_status:
          $ref: "#/components/schemas/ProcessingStatus"
        compliance_status:
          $ref: "#/components/schemas/ComplianceStatus"
        score:
          type: integer
          format: int32
          nullable: true
        summary:
          type: string
          nullable: true
        issues:
          allOf:
            - $ref: "#/components/schemas/ScanIssuesBySeverity"
          nullable: true
        createdAt:
          type: string
          format: date-time
      required:
        [id, source, processing_status, compliance_status, score, summary, issues, createdAt]

    ScanListItem:
      type: object
      properties:
        id:
          type: string
        source:
          $ref: "#/components/schemas/ScanSource"
        processing_status:
          $ref: "#/components/schemas/ProcessingStatus"
        compliance_status:
          $ref: "#/components/schemas/ComplianceStatus"
        score:
          type: integer
          format: int32
          nullable: true
        createdAt:
          type: string
          format: date-time
      required: [id, source, processing_status, compliance_status, score, createdAt]

    ErrorResponse:
      type: object
      properties:
        error:
          type: boolean
          example: true
        message:
          type: string
      required: [error, message]

    RemediateBatchItem:
      oneOf:
        - type: object
          required: [source, url, filename]
          properties:
            source:
              type: string
              enum: [url]
            url:
              type: string
              format: uri
            filename:
              type: string
        - type: object
          required: [source, scan_id, filename]
          properties:
            source:
              type: string
              enum: [upload_scan]
            scan_id:
              type: string
            filename:
              type: string

    RemediateBatchConnection:
      type: object
      required: [type, host, username, password, basePath]
      properties:
        type:
          type: string
          enum: [ftp, sftp]
        host:
          type: string
        port:
          type: integer
          minimum: 1
          maximum: 65535
        username:
          type: string
        password:
          type: string
        basePath:
          type: string
          description: Absolute path on the remote server (POSIX, no .. segments)

    RemediateBatchRequest:
      type: object
      required: [mode, items]
      properties:
        mode:
          type: string
          enum: [download, overwrite]
        items:
          type: array
          items:
            $ref: "#/components/schemas/RemediateBatchItem"
        connection:
          $ref: "#/components/schemas/RemediateBatchConnection"
        options:
          type: object
          properties:
            backup_original:
              type: boolean
            concurrency:
              type: integer
              description: Clamped between 3 and 5 on the server

    RemediateBatchItemResult:
      type: object
      required: [status, error]
      properties:
        url:
          type: string
          nullable: true
        scan_id:
          type: string
        status:
          type: string
          enum: [success, failed]
        error:
          nullable: true
          type: object
          properties:
            code:
              type: string
            message:
              type: string

    RemediateBatchResponse:
      type: object
      required: [total, success, failed, mode, items, planContext]
      properties:
        total:
          type: integer
        success:
          type: integer
        failed:
          type: integer
        mode:
          type: string
          enum: [download, overwrite]
        items:
          type: array
          items:
            $ref: "#/components/schemas/RemediateBatchItemResult"
        planContext:
          $ref: "#/components/schemas/PlanContext"
        artifact:
          type: object
          properties:
            type:
              type: string
              enum: [zip, pdf]
              description: Single successful item returns pdf; multiple return zip
            download_url:
              type: string
            expires_in_ms:
              type: integer
            filename:
              type: string
              description: Suggested filename for Content-Disposition on download

    PlanContextUsage:
      type: object
      required: [plan, remaining, is_enforced]
      properties:
        plan:
          type: string
          enum: [trial, premium]
        remaining:
          nullable: true
          type: object
          properties:
            remediations_remaining:
              type: integer
              nullable: true
            scans_remaining:
              type: integer
              nullable: true
        is_enforced:
          type: boolean

    PlanContextFeatures:
      type: object
      required: [overwrite_remote, batch_enabled]
      properties:
        overwrite_remote:
          type: boolean
        batch_enabled:
          type: boolean

    PlanContextLimits:
      type: object
      required: [hard, soft]
      properties:
        hard:
          type: object
          required:
            - max_pdfs_per_scan
            - max_pdfs_per_remediation_batch
            - max_pages_per_pdf
            - max_file_bytes
          properties:
            max_pdfs_per_scan:
              type: integer
            max_pdfs_per_remediation_batch:
              type: integer
            max_pages_per_pdf:
              type: integer
            max_file_bytes:
              type: integer
        soft:
          type: object
          required: [warning_at_selected_count]
          properties:
            warning_at_selected_count:
              type: integer

    PlanContext:
      type: object
      required: [limits, features, usage]
      properties:
        limits:
          $ref: "#/components/schemas/PlanContextLimits"
        features:
          $ref: "#/components/schemas/PlanContextFeatures"
        usage:
          $ref: "#/components/schemas/PlanContextUsage"

    PlanViolationError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, type]
          properties:
            code:
              type: string
              enum: [trial_limit_exceeded, upgrade_required]
            message:
              type: string
            type:
              type: string
              enum: [validation, entitlement]

    HealthOkResponse:
      type: object
      properties:
        status:
          type: string
          enum: [ok]
      required: [status]

    HtmlPdfLink:
      type: object
      properties:
        url:
          type: string
          format: uri
        label:
          type: string
      required: [url, label]

    UrlAnalysisResult:
      type: object
      properties:
        type:
          type: string
          enum: [url_analysis]
        classification:
          type: string
          enum: [pdf, single_page, site_scan, protected]
        pdf_links:
          type: array
          items:
            $ref: "#/components/schemas/HtmlPdfLink"
        pdf_count:
          type: integer
          format: int32
        requires_auth:
          type: boolean
        domain:
          type: string
      required:
        [type, classification, pdf_links, pdf_count, requires_auth, domain]

    RemediationStatus:
      type: string
      description: Outcome of remediation (generation of an accessibility-enhanced document) for this request or batch item.
      enum: [completed, failed, unavailable]

    BatchMode:
      type: string
      enum: [audit, remediate]

    BatchStatus:
      type: string
      enum: [processing, completed, partial, failed]

    BatchItemStatus:
      type: string
      enum: [processing, completed, failed]

    BatchItem:
      type: object
      properties:
        url:
          type: string
        scan_id:
          type: string
          nullable: true
        status:
          $ref: "#/components/schemas/BatchItemStatus"
        score:
          type: integer
          format: int32
          nullable: true
        compliance_status:
          $ref: "#/components/schemas/ComplianceStatus"
        remediation_status:
          type: string
          nullable: true
          description: >
            In remediate mode, typically completed, failed, or unavailable; null in audit mode.
        remediation_summary:
          type: string
          nullable: true
          description: Human-readable remediation outcome when applicable.
        download_url:
          type: string
          nullable: true
          description: >
            Temporary link to download the accessibility-enhanced PDF produced by remediation, when available.
      required:
        [
          url,
          scan_id,
          status,
          score,
          compliance_status,
          remediation_status,
          remediation_summary,
          download_url,
        ]

    RemediateResponse:
      type: object
      properties:
        remediation_status:
          $ref: "#/components/schemas/RemediationStatus"
        remediation_summary:
          type: string
          description: Human-readable summary of remediation (accessibility-enhanced document generation) or why it did not complete.
        download_url:
          type: string
          format: uri
          nullable: true
          description: >
            Temporary link to download the accessibility-enhanced PDF generated by the system when remediation succeeds.
            Set API_PUBLIC_URL so clients receive a browser-reachable origin.
      required: [remediation_status, remediation_summary, download_url]

    ProcessUnifiedStatus:
      type: string
      enum: [completed, partial, failed]
      description: >
        completed: analysis succeeded and remediation produced a download_url for an accessibility-enhanced PDF.
        partial: analysis succeeded but remediation did not yield a downloadable document (or the remediation request failed).
        failed: analysis did not complete (remediation is skipped).

    ProcessAnalysisBlock:
      type: object
      nullable: true
      properties:
        score:
          type: integer
          format: int32
        compliance_status:
          $ref: "#/components/schemas/ComplianceStatus"
        issues:
          allOf:
            - $ref: "#/components/schemas/ScanIssuesBySeverity"
          nullable: true
        summary:
          type: string
      required: [score, compliance_status, issues, summary]

    ProcessRemediationBlock:
      type: object
      properties:
        status:
          type: string
          description: Processor remediation_status, failed if the API could not call the processor, or skipped when analysis failed.
        summary:
          type: string
        download_url:
          type: string
          format: uri
          nullable: true
          description: >
            Temporary link to download the accessibility-enhanced PDF generated by the system when present.
      required: [status, summary, download_url]

    ProcessResponse:
      type: object
      properties:
        status:
          $ref: "#/components/schemas/ProcessUnifiedStatus"
        analysis:
          allOf:
            - $ref: "#/components/schemas/ProcessAnalysisBlock"
          nullable: true
        remediation:
          $ref: "#/components/schemas/ProcessRemediationBlock"
      required: [status, analysis, remediation]

    Batch:
      type: object
      properties:
        batch_id:
          type: string
        mode:
          $ref: "#/components/schemas/BatchMode"
        status:
          $ref: "#/components/schemas/BatchStatus"
        total:
          type: integer
          format: int32
        processed:
          type: integer
          format: int32
        createdAt:
          type: string
          format: date-time
        items:
          type: array
          items:
            $ref: "#/components/schemas/BatchItem"
      required: [batch_id, mode, status, total, processed, createdAt, items]

    BatchCreateResponse:
      type: object
      properties:
        batch_id:
          type: string
        status:
          $ref: "#/components/schemas/BatchStatus"
      required: [batch_id, status]

paths:
  /health:
    get:
      summary: Liveness check
      description: No API key required. Intended for load balancers and orchestrators.
      security: []
      responses:
        "200":
          description: Service is alive
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthOkResponse"

  /v1/status:
    get:
      summary: Service identification
      description: No API key required.
      security: []
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  service:
                    type: string
                required: [ok, service]

  /v1/scans/upload:
    post:
      summary: Scan a PDF uploaded as a file
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
              required: [file]
      responses:
        "200":
          description: Scan completed successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScanResult"
        "400":
          description: Invalid request (missing file, not a PDF, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: PDF exceeds max size (default 25 MB; `MAX_PDF_BYTES`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "504":
          description: Processor analyze request timed out (`PROCESSOR_ANALYZE_TIMEOUT_MS`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Processor error or scan could not be completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/scans/url:
    post:
      summary: Scan a PDF available at a public URL
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
              required: [url]
      responses:
        "200":
          description: Direct PDF scan result or HTML page analysis for batch/UI flows
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ScanResult"
                  - $ref: "#/components/schemas/UrlAnalysisResult"
        "400":
          description: Invalid URL or the URL did not return a PDF
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: Downloaded PDF exceeds max size (`MAX_PDF_BYTES`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Scan did not complete (e.g. processor error)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"


  /scan-url:
    post:
      summary: Discover PDFs at a URL and analyze each (selector + batch flows)
      description: >
        Returns a list of discovered PDFs with per-row `ui.selectable` / `ui.selectionReason` for clients.
        Includes `planContext` for trial/premium limits. Plan violations use `trial_limit_exceeded` or `upgrade_required`.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
      responses:
        "200":
          description: Discovery stats, PDF rows, and planContext
          content:
            application/json:
              schema:
                type: object
                required: [stats, pdfs, planContext]
                properties:
                  stats:
                    type: object
                  pdfs:
                    type: array
                    items:
                      type: object
                  warning:
                    type: object
                  planContext:
                    $ref: "#/components/schemas/PlanContext"
        "400":
          description: Validation or plan limit (trial_limit_exceeded)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/PlanViolationError"
        "413":
          description: Entry or document exceeds size limit for plan (trial_limit_exceeded)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlanViolationError"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/scans/async:
    post:
      summary: Start an asynchronous scan from a file upload or URL
      description: >
        Starts a scan and returns immediately. Poll GET /v1/scans/{scanId} to
        retrieve the final result.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
      responses:
        "202":
          description: Scan accepted for processing
          content:
            application/json:
              schema:
                type: object
                properties:
                  scan_id:
                    type: string
                  processing_status:
                    $ref: "#/components/schemas/ProcessingStatus"
                required: [scan_id, processing_status]
        "400":
          description: Invalid request (missing file or URL, invalid URL, not a PDF)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: PDF exceeds max size (`MAX_PDF_BYTES`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/process:
    post:
      summary: Single-step analysis + remediation (recommended)
      description: >
        Runs the same analysis as POST /v1/scans/upload, then remediation as POST /v1/remediate on the same PDF.
        Returns one JSON object with the analysis report and remediation outcome, plus an optional temporary
        download URL for an accessibility-enhanced PDF (not a compliance certificate).
        Does not create a scan record in GET /v1/scans. Requires a valid API key (same as scan routes).
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: PDF file (use this or url field, not both).
                url:
                  type: string
                  format: uri
                  description: Direct PDF URL (multipart alternative to file).
                options:
                  type: string
                  description: Optional JSON string, e.g. {"forceOcr":true} to force the OCR rebuild path on the processor.
          application/json:
            schema:
              type: object
              properties:
                pdfBase64:
                  type: string
                  description: Base64-encoded PDF (use this or url, not both).
                url:
                  type: string
                  format: uri
                  description: Direct PDF URL.
                options:
                  type: object
                  properties:
                    forceOcr:
                      type: boolean
                      description: When true, processor uses the OCR rebuild path even for text-classified PDFs (requires Tesseract + Poppler).
      responses:
        "200":
          description: Unified result (completed, partial, or failed envelope)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessResponse"
        "400":
          description: Invalid request (missing input, not a PDF, URL not a direct PDF, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: PDF exceeds max size
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/ProcessResponse"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Analyze or remediate processor error (body may be ProcessResponse or ErrorResponse)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/ProcessResponse"
        "504":
          description: Processor timeout
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/ProcessResponse"

  /v1/remediate:
    post:
      summary: Remediation — accessibility-enhanced PDF (OCR for image-heavy docs; metadata pass for text-based)
      description: >
        Generates an accessibility-enhanced PDF when possible: text-based inputs are processed for better metadata;
        image-heavy inputs use OCR where dependencies are installed. Does not certify ADA/WCAG compliance or full PDF/UA structure.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                pdfBase64:
                  type: string
                url:
                  type: string
                  format: uri
      responses:
        "200":
          description: Remediation result (analysis is not included; use POST /v1/process for combined analysis + remediation).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RemediateResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid or missing API key (unless server disables key for this route)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: PDF exceeds max size (`MAX_PDF_BYTES`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "504":
          description: Processor remediate timed out (`PROCESSOR_REMEDIATE_TIMEOUT_MS`)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Processor unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/remediate/download/{token}:
    get:
      summary: Download a temporarily stored accessibility-enhanced PDF (token + TTL; no API key for browser compatibility)
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Accessibility-enhanced PDF binary (processed for better accessibility; not a compliance certificate).
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "404":
          description: Expired or unknown token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/remediate-batch:
    post:
      summary: Batch remediation — multiple PDFs (ZIP download or FTP/SFTP overwrite)
      description: >
        Accepts URLs (SSRF-safe fetch) or upload_scan scan_ids (requires prior POST /v1/scans/upload and in-memory artifact TTL).
        Each item is processed independently. Download mode returns a JSON summary plus a short-lived ZIP download URL.
        Overwrite mode requires connection credentials (not stored); validates host is not a private/reserved address.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RemediateBatchRequest"
      responses:
        "200":
          description: Per-item outcomes; download mode includes artifact with ZIP URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RemediateBatchResponse"
        "400":
          description: Validation, remote precheck failure, or trial_limit_exceeded
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/PlanViolationError"
        "403":
          description: Disallowed remote host (SSRF guard), entitlement, or upgrade_required
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - $ref: "#/components/schemas/PlanViolationError"
        "413":
          description: ZIP exceeds configured maximum or file size limits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/remediate-batch/download/{token}:
    get:
      summary: Download remediate-batch ZIP (token + TTL; no API key)
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: application/zip binary
          content:
            application/zip:
              schema:
                type: string
                format: binary
        "404":
          description: Expired or unknown token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/batch:
    post:
      summary: Start batch analysis or analysis+remediation for up to 20 direct PDF URLs
      description: >
        `audit` returns analysis per URL; `remediate` adds remediation when possible.
        Download URLs point to accessibility-enhanced PDFs, not certified compliance documents.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                mode:
                  $ref: "#/components/schemas/BatchMode"
                urls:
                  type: array
                  maxItems: 20
                  items:
                    type: string
              required: [mode, urls]
      responses:
        "200":
          description: Batch accepted for processing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchCreateResponse"
        "400":
          description: Invalid request (validation, max URL limit, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid or missing API key (unless server disables key for this route)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/batch/{batch_id}:
    get:
      summary: Get the full state of a batch job (analysis and optional remediation per item)
      security:
        - apiKey: []
      parameters:
        - name: batch_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Batch found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Batch"
        "401":
          description: Invalid or missing API key (unless server disables key for this route)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Batch not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/scans/{scanId}:
    get:
      summary: Get a single scan by id
      security:
        - apiKey: []
      parameters:
        - name: scanId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Scan found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScanResult"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Scan not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/scans:
    get:
      summary: List recent scans for the current API key
      security:
        - apiKey: []
      responses:
        "200":
          description: List of scans
          content:
            application/json:
              schema:
                type: object
                properties:
                  scans:
                    type: array
                    items:
                      $ref: "#/components/schemas/ScanListItem"
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

