openapi: 3.1.0
info:
  title: DocuHelix API
  version: v1
  description: |
    The DocuHelix API enables external systems to submit documents for
    automated ingestion, classification, and storage within a DocuHelix tenant.

    Processing is fully asynchronous. The API accepts documents and returns
    immediately with a tracking identifier. Documents are classified and
    promoted to the appropriate cabinet in the background.
  contact:
    name: DocuHelix Support
    url: https://docuhelix.com
    email: support@docuhelix.com

servers:
  - url: https://api.docuhelix.com
    description: Production

security: []

paths:
  /api/v1/auth/token:
    post:
      operationId: issueToken
      summary: Exchange credentials for an access token
      description: |
        Authenticate with your API client credentials to obtain a short-lived
        JWT access token. The token is valid for 30 minutes.
      tags:
        - Authentication
      parameters:
        - $ref: "#/components/parameters/XTenantID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TokenRequest"
            example:
              client_id: "01J9ABC123DEF456GHI789JKLM"
              client_secret: "sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
      responses:
        "200":
          description: Token issued successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
              example:
                access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkb2N1aGVsaXgtYXBpLXNlcnZpY2UiLCJzdWIiOiIwMUo5QUJDMTIzREVGNDU2R0hJNzg5SktMTSIsIm9yZyI6IjAxSjhLUDRRWFJOM01WNVlaV1Q2SDJBQkNEIiwic2NvcGVzIjpbImRvY3VtZW50czppbmdlc3QiXX0.signature"
                token_type: "bearer"
                expires_in: 1800
        "400":
          description: Missing tenant header
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: "missing_tenant"
                  message: "The X-Tenant-ID header is required."
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: "invalid_credentials"
                  message: "Invalid client credentials."
        "403":
          description: Client disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: "client_disabled"
                  message: "API client has been deactivated."
        "429":
          description: Rate limit exceeded (10 requests/minute)

  /api/v1/ingestion/documents:
    post:
      operationId: ingestDocument
      summary: Submit a document for ingestion
      description: |
        Upload a document file for asynchronous processing. The API stages the
        file, queues it for classification and promotion, and returns immediately
        with a tracking identifier.

        **Routing behavior:**
        - Full routing (industry + cabinet + module): validated and promoted directly.
        - Partial routing: system resolves missing fields via classification.
        - No routing: full classification is performed.
        - Low confidence: document enters intake for manual review.
      tags:
        - Ingestion
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/XTenantID"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/IngestDocumentRequest"
      responses:
        "201":
          description: Document accepted for processing
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/IngestionRequest"
              example:
                data:
                  uid: "01JNQR7V3KXMSW8YBT4FH6PZEA"
                  org_uid: "01J8KP4QXRN3MV5YZWT6H2ABCD"
                  api_client_uid: "01J9ABC123DEF456GHI789JKLM"
                  correlation_uid: "01JNQR7V3KXMSW8YBT4FH6PZEB"
                  status: "processing"
                  content_sha256: "a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1"
                  original_filename: "closing-disclosure.pdf"
                  external_source: "loan-origination-system"
                  external_reference_id: "LOAN-2026-04-00183"
                  external_entity_type: "Loan"
                  external_entity_id: "loan-78432"
                  raw_document_type: "Closing Disclosure"
                  industry_key: "mortgage"
                  industry_uid: null
                  cabinet_uid: "01JABC123CABINET456UIDXYZ"
                  module_key: null
                  received_at: "2026-04-03T21:15:00.000000Z"
                  processed_at: null
                  created_at: "2026-04-03T21:15:00.000000Z"
                  updated_at: "2026-04-03T21:15:00.000000Z"
        "400":
          description: Bad request (missing tenant header or invalid file)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: "validation_error"
                  message: "Validation error"
                  details:
                    file:
                      - "The file field is required."
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                missing_token:
                  summary: No Authorization header
                  value:
                    error:
                      code: "missing_token"
                      message: "Bearer token is required."
                invalid_token:
                  summary: Expired or malformed JWT
                  value:
                    error:
                      code: "invalid_token"
                      message: "Token is invalid or expired."
        "403":
          description: Insufficient scope or tenant mismatch
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                insufficient_scope:
                  summary: Token lacks required scope
                  value:
                    error:
                      code: "insufficient_scope"
                      message: "Token does not have the documents:ingest scope."
                tenant_mismatch:
                  summary: X-Tenant-ID does not match token
                  value:
                    error:
                      code: "tenant_mismatch"
                      message: "Invalid tenant identifier."
        "422":
          description: Routing validation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: "validation_error"
                  message: "Validation error"
                  details:
                    cabinet_uid:
                      - "Cabinet not found in this organization."
                    module_key:
                      - "Module key not found for this organization."
        "429":
          description: Rate limit exceeded (60 requests/minute)
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: "server_error"
                  message: "An unexpected error occurred."

  /api/v1/org:
    get:
      operationId: getOrgContext
      summary: Get organization context
      description: |
        Returns the authenticated organization context including industries,
        scopes, and client identity.
      tags:
        - Discovery
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/XTenantID"
      responses:
        "200":
          description: Organization context
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/OrgContext"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/InsufficientScope"

  /api/v1/ingestions/{uid}:
    get:
      operationId: getIngestionStatus
      summary: Get ingestion status
      description: |
        Check the asynchronous processing status of a previously submitted
        document. Returns classification progress, confidence scores,
        and promotion details when available.
      tags:
        - Discovery
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/XTenantID"
        - name: uid
          in: path
          required: true
          schema:
            type: string
          description: Ingestion request UID
      responses:
        "200":
          description: Ingestion status
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/IngestionStatus"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/InsufficientScope"
        "404":
          description: Ingestion not found

  /api/v1/cabinets:
    get:
      operationId: listCabinets
      summary: List cabinets
      description: |
        Search and list document cabinets for the authenticated organization.
      tags:
        - Discovery
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/XTenantID"
        - name: industry_uid
          in: query
          schema: { type: string }
          description: Filter by industry ULID
        - name: q
          in: query
          schema: { type: string, maxLength: 255 }
          description: Search by cabinet name
        - name: page
          in: query
          schema: { type: integer, minimum: 1 }
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 100 }
      responses:
        "200":
          description: Cabinet list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CabinetListResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/InsufficientScope"

  /api/v1/document-types:
    get:
      operationId: listDocumentTypes
      summary: List document types
      description: |
        List available document types (module keys) for the authenticated organization.
      tags:
        - Discovery
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/XTenantID"
        - name: q
          in: query
          schema: { type: string, maxLength: 255 }
          description: Search by name or key
      responses:
        "200":
          description: Document type list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/DocumentTypeSummary"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/InsufficientScope"

  /api/v1/documents:
    get:
      operationId: listDocuments
      summary: List documents
      description: |
        Search and list documents for the authenticated organization.
      tags:
        - Discovery
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/XTenantID"
        - name: q
          in: query
          schema: { type: string, maxLength: 255 }
          description: Search by title
        - name: cabinet_uid
          in: query
          schema: { type: string }
          description: Filter by cabinet ULID
        - name: module_key
          in: query
          schema: { type: string }
          description: Filter by document type key
        - name: page
          in: query
          schema: { type: integer, minimum: 1 }
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 100 }
      responses:
        "200":
          description: Document list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentListResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/InsufficientScope"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT access token obtained from the /api/v1/auth/token endpoint.

  responses:
    Unauthenticated:
      description: Missing or invalid Bearer token
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: "missing_token"
              message: "Bearer token is required."
    InsufficientScope:
      description: Token does not have the required scope
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: "insufficient_scope"
              message: "Token does not have the required scope."

  parameters:
    XTenantID:
      name: X-Tenant-ID
      in: header
      required: true
      description: Your organization UID. Must match the token's org claim on authenticated requests.
      schema:
        type: string
        example: "01J8KP4QXRN3MV5YZWT6H2ABCD"

    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Unique key to prevent duplicate submissions. If a successful request
        with the same key already exists, the API returns the existing result.
      schema:
        type: string
        example: "import-batch-2026-04-03-doc-00183"

  schemas:
    TokenRequest:
      type: object
      required:
        - client_id
        - client_secret
      properties:
        client_id:
          type: string
          description: API client identifier issued by your DocuHelix admin.
          example: "01J9ABC123DEF456GHI789JKLM"
        client_secret:
          type: string
          description: API client secret.
          example: "sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"

    TokenResponse:
      type: object
      properties:
        access_token:
          type: string
          description: JWT access token.
        token_type:
          type: string
          enum:
            - bearer
          description: Always "bearer".
        expires_in:
          type: integer
          description: Token lifetime in seconds.
          example: 1800

    IngestDocumentRequest:
      type: object
      required:
        - file
      properties:
        file:
          type: string
          format: binary
          description: |
            The document file. Max 500 MB. Supported types: PDF, Word, Excel,
            PowerPoint, plain text, CSV, PNG, JPEG, TIFF.
        filename:
          type: string
          maxLength: 255
          description: Override the original filename.
        industry_key:
          type: string
          maxLength: 80
          description: Industry identifier (e.g. "mortgage", "healthcare").
        industry_uid:
          type: string
          minLength: 26
          maxLength: 26
          description: Industry ULID. Validated against your tenant configuration.
        cabinet_uid:
          type: string
          minLength: 26
          maxLength: 26
          description: Target cabinet ULID.
        cabinet_name:
          type: string
          maxLength: 255
          description: Target cabinet name. Resolved to UID if cabinet_uid is not provided.
        module_key:
          type: string
          maxLength: 80
          description: Metadata module key. Validated against your tenant configuration.
        external_source:
          type: string
          maxLength: 200
          description: Name of the sending system (e.g. "salesforce", "workday").
        external_reference_id:
          type: string
          maxLength: 200
          description: Unique ID for this document in your system.
        external_entity_type:
          type: string
          maxLength: 100
          description: Entity type in your system (e.g. "Employee", "Loan").
        external_entity_id:
          type: string
          maxLength: 200
          description: Entity ID in your system.
        raw_document_type:
          type: string
          maxLength: 100
          description: Your classification label (e.g. "W-2", "Invoice").
        metadata:
          type: string
          description: Custom key-value metadata as a JSON string.
          example: '{"loan_number":"2026-04-00183","borrower":"Jane Doe"}'
        notes:
          type: string
          maxLength: 5000
          description: Internal notes attached to the ingestion request.

    IngestionRequest:
      type: object
      properties:
        uid:
          type: string
          description: Ingestion request tracking identifier (ULID).
        org_uid:
          type: string
          description: Organization UID.
        api_client_uid:
          type: string
          description: API client that submitted the request.
        correlation_uid:
          type: string
          description: Correlation identifier for distributed tracing.
        status:
          type: string
          enum:
            - received
            - processing
            - processed
            - rejected
            - failed
          description: Current processing status.
        content_sha256:
          type: string
          description: SHA-256 hash of the uploaded file content.
        original_filename:
          type: string
          description: Original filename of the uploaded document.
        external_source:
          type: string
          nullable: true
        external_reference_id:
          type: string
          nullable: true
        external_entity_type:
          type: string
          nullable: true
        external_entity_id:
          type: string
          nullable: true
        raw_document_type:
          type: string
          nullable: true
        industry_key:
          type: string
          nullable: true
        industry_uid:
          type: string
          nullable: true
        cabinet_uid:
          type: string
          nullable: true
        module_key:
          type: string
          nullable: true
        received_at:
          type: string
          format: date-time
          description: When the request was received.
        processed_at:
          type: string
          format: date-time
          nullable: true
          description: When the request was published to downstream processing.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    OrgContext:
      type: object
      properties:
        org_uid: { type: string }
        api_client_uid: { type: string }
        scopes:
          type: array
          items: { type: string }
        industries:
          type: array
          items:
            type: object
            properties:
              uid: { type: string }
              key: { type: string }
              name: { type: string }

    IngestionStatus:
      type: object
      properties:
        ingestion_request_uid: { type: string }
        status: { type: string }
        original_filename: { type: string }
        content_sha256: { type: string, nullable: true }
        received_at: { type: string, format: date-time }
        processed_at: { type: string, format: date-time, nullable: true }
        classification:
          type: object
          nullable: true
          properties:
            status: { type: string, enum: [pending, classified, needs_review, unresolved, failed] }
            suggested_industry:
              type: object
              nullable: true
              properties:
                uid: { type: string }
                name: { type: string, nullable: true }
            suggested_cabinet:
              type: object
              nullable: true
              properties:
                uid: { type: string }
                name: { type: string, nullable: true }
            suggested_module_key: { type: string, nullable: true }
            confidence:
              type: object
              properties:
                industry: { type: number, nullable: true }
                cabinet: { type: number, nullable: true }
                module: { type: number, nullable: true }
        promotion:
          type: object
          nullable: true
          properties:
            status: { type: string, enum: [not_promoted, promoted, failed] }
            document_uid: { type: string, nullable: true }
            version_uid: { type: string, nullable: true }
        last_error: { type: string, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    CabinetSummary:
      type: object
      properties:
        uid: { type: string }
        name: { type: string }
        industry_uid: { type: string, nullable: true }
        description: { type: string, nullable: true }

    PaginationMeta:
      type: object
      properties:
        current_page: { type: integer }
        last_page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }

    CabinetListResponse:
      type: object
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/CabinetSummary" }
        meta: { $ref: "#/components/schemas/PaginationMeta" }

    DocumentTypeSummary:
      type: object
      properties:
        uid: { type: string }
        key: { type: string }
        name: { type: string }

    DocumentSummary:
      type: object
      properties:
        uid: { type: string }
        title: { type: string, nullable: true }
        cabinet_uid: { type: string, nullable: true }
        cabinet_name: { type: string, nullable: true }
        module_key: { type: string, nullable: true }
        created_by_type: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }

    DocumentListResponse:
      type: object
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/DocumentSummary" }
        meta: { $ref: "#/components/schemas/PaginationMeta" }

    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Machine-readable error code.
            message:
              type: string
              description: Human-readable error description.
            details:
              type: object
              nullable: true
              additionalProperties:
                type: array
                items:
                  type: string
              description: Field-level validation errors (when applicable).
