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

# Pull one chart document's bytes from the EHR

> Stages the bytes of a document `GET /patients/{id}/documents` reported with `downloaded: false`, so it becomes downloadable. Returns when the pull is done, so there is nothing to poll — one file is typically a sub-second EHR call, but a cold session or a large attachment can take considerably longer and the request is held for as long as the download runs. Set a client timeout accordingly: a retry after one lands on the in-flight 409 rather than starting a second download. The response carries the document exactly as the list endpoint now reports it, with `downloaded: true`, a signed `url` and `expiresInSeconds`. Already-staged bytes are served from cache with no EHR call, so this is safe to call again for a fresh URL. `status: "unavailable"` is NOT a failure: it means this platform cannot pull the bytes on its own and `reason` says what a human has to do (`needs_user_session` — a clinic user must open the patient's Documents panel once; `ehr_auth` — the practice's EHR connection needs reconnecting). A second call while one pull is in flight is normally a 409 — though if the platform's coordination store is briefly unavailable it may instead join the running pull and answer 200 with the same result, or 409 if that pull is still running after a short wait — and a document that has since been removed from the chart and was never staged is a 404. Guarded beyond the shared rate limiter by a cap on LIVE pulls per EHR connection per UTC day (429, resetting at 00:00 UTC) — so one practice location running out does not affect another; documents already staged keep being served after the cap, since they cost the EHR nothing. Requires read:patient_documents scope.



## OpenAPI

````yaml /openapi-v4.json post /patients/{id}/documents/{documentId}/fetch
openapi: 3.0.0
info:
  title: Max AI Public API
  description: API for third-party marketplace apps
  version: '4.0'
  contact: {}
servers:
  - url: https://api.maxcare.ai/v4
security: []
tags: []
paths:
  /patients/{id}/documents/{documentId}/fetch:
    post:
      tags:
        - Patients
      summary: Pull one chart document's bytes from the EHR
      description: >-
        Stages the bytes of a document `GET /patients/{id}/documents` reported
        with `downloaded: false`, so it becomes downloadable. Returns when the
        pull is done, so there is nothing to poll — one file is typically a
        sub-second EHR call, but a cold session or a large attachment can take
        considerably longer and the request is held for as long as the download
        runs. Set a client timeout accordingly: a retry after one lands on the
        in-flight 409 rather than starting a second download. The response
        carries the document exactly as the list endpoint now reports it, with
        `downloaded: true`, a signed `url` and `expiresInSeconds`.
        Already-staged bytes are served from cache with no EHR call, so this is
        safe to call again for a fresh URL. `status: "unavailable"` is NOT a
        failure: it means this platform cannot pull the bytes on its own and
        `reason` says what a human has to do (`needs_user_session` — a clinic
        user must open the patient's Documents panel once; `ehr_auth` — the
        practice's EHR connection needs reconnecting). A second call while one
        pull is in flight is normally a 409 — though if the platform's
        coordination store is briefly unavailable it may instead join the
        running pull and answer 200 with the same result, or 409 if that pull is
        still running after a short wait — and a document that has since been
        removed from the chart and was never staged is a 404. Guarded beyond the
        shared rate limiter by a cap on LIVE pulls per EHR connection per UTC
        day (429, resetting at 00:00 UTC) — so one practice location running out
        does not affect another; documents already staged keep being served
        after the cap, since they cost the EHR nothing. Requires
        read:patient_documents scope.
      operationId: PatientsPublicController_fetchPatientDocument
      parameters:
        - name: id
          required: true
          in: path
          description: Patient ID
          schema:
            type: string
        - name: documentId
          required: true
          in: path
          description: Document ID, as returned by GET /patients/{id}/documents
          schema:
            type: string
        - name: X-Organization-Id
          in: header
          required: true
          schema:
            type: string
          description: Target clinic organization ID
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FetchPatientDocumentSuccessResponse'
        '400':
          description: Missing or invalid request parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBadRequestResponse'
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiUnauthorizedResponse'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiForbiddenResponse'
        '404':
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiNotFoundResponse'
        '409':
          description: Resource conflict (e.g. editing a signed note)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiConflictResponse'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRateLimitResponse'
        '502':
          description: EHR sync failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBadGatewayResponse'
      security:
        - api-key: []
components:
  schemas:
    FetchPatientDocumentSuccessResponse:
      type: object
      properties:
        code:
          type: string
          description: Response code
          example: success
        data:
          $ref: '#/components/schemas/FetchPatientDocumentResponseData'
      required:
        - code
        - data
    PublicApiBadRequestResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: bad_request
        message:
          type: string
          description: Human-readable error message
          example: '''id'' must be a valid UUID'
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiUnauthorizedResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: unauthorized
        message:
          type: string
          description: Human-readable error message
          example: Invalid or missing API key
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiForbiddenResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: forbidden
        message:
          type: string
          description: Human-readable error message
          example: Insufficient scope
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiNotFoundResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: not_found
        message:
          type: string
          description: Human-readable error message
          example: Resource not found
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiConflictResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: conflict
        message:
          type: string
          description: Human-readable error message
          example: Cannot edit a signed note
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiRateLimitResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: rate_limit_exceeded
        message:
          type: string
          description: Human-readable error message
          example: Rate limit exceeded. Maximum 1000 requests per 60 seconds.
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    PublicApiBadGatewayResponse:
      type: object
      properties:
        code:
          type: string
          description: Error code
          example: unexpected_integration_error
        message:
          type: string
          description: Human-readable error message
          example: EHR sync failed
        trace_id:
          type: string
          description: Trace ID for debugging
          example: 550e8400-e29b-41d4-a716-446655440000
      required:
        - code
        - message
        - trace_id
    FetchPatientDocumentResponseData:
      type: object
      properties:
        status:
          type: string
          description: >-
            `completed` means the bytes are staged and `document` describes the
            document, with `downloaded: true` and a signed `url`. `unavailable`
            means there are no servable bytes — see `reason`. `document` is null
            for `unavailable` EXCEPT when `reason` is `not_staged`, where the
            document is still described (with `downloaded: false` and no `url`)
            because it exists on the chart and its bytes simply went away.
          enum:
            - completed
            - unavailable
          example: completed
        reason:
          type: string
          description: >-
            Why the bytes are unavailable, as a stable marker.
            `needs_user_session` — the organization has no live system connector
            this platform may read the chart through, so ONLY a clinic user
            opening the patient's Documents panel in the dashboard can stage
            these bytes; tell the operator to do that. `ehr_auth` — there is a
            connector, but its EHR session is not usable and someone at the
            practice has to reconnect it. Neither of those is retryable.
            `not_staged` — the bytes were pulled but the stored copy went away
            before this response was built (a concurrent cleanup); that one IS
            worth retrying. Null when `status` is `completed`.
          nullable: true
          enum:
            - needs_user_session
            - ehr_auth
            - not_staged
          example: null
        document:
          description: >-
            The document as `GET /patients/{id}/documents` now reports it — same
            fields, same meanings, so nothing has to be re-fetched to act on it.
            Null when `status` is `unavailable`, except for `not_staged` (see
            `status`), where it is present but carries no `url`.
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ExternalPatientDocumentResponse'
      required:
        - status
        - reason
        - document
    ExternalPatientDocumentResponse:
      type: object
      properties:
        id:
          type: string
          description: >-
            Document reference identifier. An opaque UUID on every version —
            unlike the top-level resources, sub-resource ids are not
            prefix-encoded from v3, because the one place that accepts them back
            (`POST /faxes/send`'s `documentIds`) takes them raw too. Treat it as
            an opaque string.
          example: 9c1f0f7a-2f4b-4f2e-8a1e-6b0f2d3c4a5b
        kind:
          type: string
          description: >-
            Best-effort classification derived from the EHR's own category and
            the document title. `other` means we could not classify it — NOT
            that it is unimportant. Always cross-check `sourceCategory`.
          enum:
            - insurance_card
            - identification
            - prior_auth
            - clinical
            - other
          example: insurance_card
        sourceCategory:
          type: string
          description: >-
            The EHR's own category for the document, verbatim (ModMed
            `firmCategoryTab.title`, the EZDerm DMS folder path). Slash-joined
            when the EHR nests categories. Null when the EHR reports none.
          nullable: true
          example: Insurance Card Front
        title:
          type: string
          description: Display title in the EHR
          example: Insurance Card Front
        filename:
          type: string
          description: Original filename when the EHR reports one distinct from the title
          nullable: true
          example: insurance_card_front.jpg
        mimeType:
          type: string
          description: MIME type
          nullable: true
          example: image/jpeg
        bytes:
          type: number
          description: Size in bytes
          nullable: true
          example: 184320
        createdAt:
          type: string
          description: When the document was created/uploaded in the EHR
          nullable: true
          example: '2026-02-27T15:04:05.000Z'
        updatedAt:
          type: string
          description: >-
            The newest thing that happened to this document — created in the
            EHR, last returned by a chart refresh, downloaded here, or removed
            from the chart. This is what `updatedSince` compares against. Always
            present: every document carries a last-seen stamp even when the EHR
            dated nothing else.
          example: '2026-08-14T10:15:00.000Z'
        downloaded:
          type: boolean
          description: >-
            True when the bytes are staged in this platform's storage and `url`
            is therefore populated. False means the document exists on the chart
            but has never been pulled from the EHR — pulling it needs a clinic
            user's own EHR session, which an API key does not have.
          example: true
        url:
          type: string
          description: Short-lived signed download URL. Null when `downloaded` is false.
          nullable: true
          example: https://storage.maxcare.ai/…?X-Amz-Signature=…
        expiresInSeconds:
          type: number
          description: Lifetime of `url` in seconds. Null when there is no URL.
          nullable: true
          example: 3600
        deletedFromEhrAt:
          type: string
          description: >-
            Set when the document has since disappeared from the EHR chart. Null
            while it is still there.
          nullable: true
          example: null
      required:
        - id
        - kind
        - sourceCategory
        - title
        - filename
        - mimeType
        - bytes
        - createdAt
        - updatedAt
        - downloaded
        - url
        - expiresInSeconds
        - deletedFromEhrAt
  securitySchemes:
    api-key:
      scheme: bearer
      bearerFormat: JWT
      type: http
      description: Marketplace API key

````