> ## Documentation Index
> Fetch the complete documentation index at: https://docs.healos.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload a document

> Upload a document for a patient. Accepts PDF, JPEG, PNG, and DOCX files up to 10MB. Pass an optional `external_id` form field to attach a caller-supplied identifier (immutable once set; unique per organization).

**Text extraction runs asynchronously.** This endpoint returns as soon as the file is stored — it does not wait for extraction (which can take 10–30s for a PDF). The returned document is typically in `pending` status (extraction may occasionally finish before the response is built). Track progress by polling `GET /patients/{patientId}/documents` (pass `?external_id=<your id>` to fetch just this document) until its `status` is `completed` (`has_extracted_text` becomes `true`) or `failed`. Note: these endpoints report extraction status and availability (`status`, `has_extracted_text`, `extracted_text_length`) — the extracted text content itself is not returned. You can (re-)trigger extraction at any time with `POST /patients/{patientId}/documents/{documentId}/extract`.



## OpenAPI

````yaml /openapi/ext-api-v1.json post /patients/{patientId}/documents
openapi: 3.0.0
info:
  title: Healos External API
  version: 1.0.0
  description: >-
    External API for managing patients, documents, and appointments, listing
    providers, and for pulling clinical notes (with transcript and patient
    context).
servers:
  - url: https://api.healos.ai/ext-api/v1
    description: Production
  - url: http://localhost:8787/ext-api/v1
    description: Local development
security:
  - ApiKeyAuth: []
paths:
  /patients/{patientId}/documents:
    post:
      tags:
        - Documents
      summary: Upload a document
      description: >-
        Upload a document for a patient. Accepts PDF, JPEG, PNG, and DOCX files
        up to 10MB. Pass an optional `external_id` form field to attach a
        caller-supplied identifier (immutable once set; unique per
        organization).


        **Text extraction runs asynchronously.** This endpoint returns as soon
        as the file is stored — it does not wait for extraction (which can take
        10–30s for a PDF). The returned document is typically in `pending`
        status (extraction may occasionally finish before the response is
        built). Track progress by polling `GET /patients/{patientId}/documents`
        (pass `?external_id=<your id>` to fetch just this document) until its
        `status` is `completed` (`has_extracted_text` becomes `true`) or
        `failed`. Note: these endpoints report extraction status and
        availability (`status`, `has_extracted_text`, `extracted_text_length`) —
        the extracted text content itself is not returned. You can (re-)trigger
        extraction at any time with `POST
        /patients/{patientId}/documents/{documentId}/extract`.
      parameters:
        - schema:
            type: string
            description: Patient ID
            example: '1743552000000'
          required: true
          name: patientId
          in: path
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  nullable: true
                  description: The file to upload
                external_id:
                  type: string
                  minLength: 1
                  maxLength: 255
                  description: >-
                    Optional caller-supplied identifier. Unique per
                    organization; immutable once set.
                  example: PARTNER-DOC-456
      responses:
        '201':
          description: >-
            Document uploaded. The file is stored and, in the normal case, text
            extraction is queued to run asynchronously — the returned document
            is then typically `pending`. If extraction could not be queued, the
            file is still saved and `message` reflects that (trigger extraction
            later via the extract endpoint). Poll the GET endpoint to track
            extraction status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Document'
                  message:
                    type: string
                    description: >-
                      Human-readable status. `text extraction started` when
                      extraction was queued; `Document uploaded successfully` if
                      the file was stored but extraction could not be queued
                      (retry via the extract endpoint).
                    example: Document uploaded successfully; text extraction started
                required:
                  - data
                  - message
        '400':
          description: Invalid file type or size
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Patient not found or not accessible
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: external_id already in use within this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
components:
  schemas:
    Document:
      type: object
      properties:
        id:
          type: string
          description: Document ID
          example: d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a
        filename:
          type: string
          description: Original uploaded filename
          example: intake_form_maria_santos.pdf
        file_type:
          type: string
          enum:
            - pdf
            - image
            - docx
          description: Type of the file
        file_size:
          type: number
          description: File size in bytes
          example: 245760
        public_url:
          type: string
          nullable: true
          description: Public URL if available
        upload_date:
          type: string
          description: ISO 8601 upload timestamp
          example: '2026-03-20T11:15:00.000Z'
        status:
          type: string
          enum:
            - pending
            - processing
            - completed
            - failed
          description: Document processing status
        extracted_date:
          type: string
          nullable: true
          description: ISO 8601 timestamp when extraction completed, if available
          example: '2026-03-20T11:16:00.000Z'
        extraction_error:
          type: string
          nullable: true
          description: Last extraction error, if extraction failed
          example: No text could be extracted from the PDF
        has_extracted_text:
          type: boolean
          description: Whether extracted text is stored for this document
          example: true
        extracted_text_length:
          type: number
          description: Length of the extracted text stored for this document
          example: 2048
        external_id:
          type: string
          nullable: true
          description: Caller-supplied external identifier (immutable once set)
          example: PARTNER-DOC-456
      required:
        - id
        - filename
        - file_type
        - file_size
        - public_url
        - upload_date
        - status
        - extracted_date
        - extraction_error
        - has_extracted_text
        - extracted_text_length
        - external_id
    Error:
      type: object
      properties:
        error:
          type: string
      required:
        - error
    ConflictResponse:
      type: object
      properties:
        error:
          type: string
          example: external_id already in use
        existing:
          type: object
          properties:
            id:
              type: string
              description: HealOS internal ID of the existing record
            external_id:
              type: string
              description: The conflicting external_id
          required:
            - id
            - external_id
      required:
        - error
        - existing
    RateLimitError:
      type: object
      properties:
        error:
          type: string
          example: Rate limit exceeded. Please retry after the window resets.
      required:
        - error
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key obtained from /api/v1/ext-api-keys

````