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

# Upload FHIR Bundle

> Upload your FHIR bundle JSON to the pre-signed URL after requesting it via the Upload FHIR Push Upload URL API

After requesting an upload URL via [Request upload URL API](/patient-summary-api-reference/ckg-data-ingestion/upload), use HTTP PUT to upload your FHIR bundle directly to the pre-signed URL.

<Note>
  This is **Step 2** of the CKG data ingestion workflow. Complete **Step 1** first to obtain the upload URL.
</Note>

## FHIR bundle requirements

Your bundle must satisfy the following requirements:

| Requirement          | Description                                          |
| -------------------- | ---------------------------------------------------- |
| Valid JSON           | Must parse without syntax errors                     |
| Maximum size         | You can upload a bundle up to **500 MB** in size     |
| Resource size        | Each FHIR resource can be up to **10 MB** in size    |
| Binary resource size | Binary resources can be up to **40 MB** in size      |
| Multiple patients    | Bundles can include resources from multiple patients |
| No DELETE operations | Bundle entries must not use the **DELETE** method    |

<Note>
  If the upload URL expires before you upload, request a new URL via [Request upload URL API](/patient-summary-api-reference/ckg-data-ingestion/upload).
</Note>

## Best practices for summary generation

For optimal summary generation keep the following in mind:

<Tip>
  * **Include key identifiers** - Include consistent patient, encounter, and practitioner identifiers in your FHIR resources so they match the `fhir_encounter_id` and `fhir_practitioner_id` values you use later.
  * **Use correlation IDs** - Supply meaningful `correlation_id` values to track related uploads.
</Tip>

<Note>
  Suki maps generation and retrieval requests to the data ingested through CKG using the encounter and practitioner identifiers you provide.
</Note>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Upload URL expired">
    If you see a **403 Forbidden** or **410 Gone** error, the upload URL has expired (15 minutes). Request a new URL via Step 1 and retry the upload.
  </Accordion>

  <Accordion title="File too large">
    If you see a **400 Bad Request** mentioning size, your bundle exceeds 500 MB. Options:

    * Split the bundle into multiple smaller bundles.
    * Remove unnecessary resources.
    * Use separate uploads for each encounter or date range.
  </Accordion>

  <Accordion title="Invalid JSON">
    Ensure your FHIR bundle is valid JSON before uploading. Use a JSON validator or linter to check syntax.
  </Accordion>
</AccordionGroup>

After a successful upload (200 OK response), proceed to Step 3:

<div className="doc-guide-btn-row">
  <a href="/patient-summary-api-reference/ckg-data-ingestion/ingestion-status" className="doc-guide-btn">
    Step 3: Poll Ingestion Status
  </a>

  <a href="/patient-summary-api-reference/ckg-data-ingestion/upload" className="doc-guide-btn">
    Back to Step 1
  </a>
</div>


## OpenAPI

````yaml PUT /fhir-push/upload
openapi: 3.0.1
info:
  title: Suki Developer Platform
  description: >-
    REST and WebSocket APIs for the Suki Developer Platform. Authenticate with
    Login or Register to obtain a Suki access token, then integrate ambient
    clinical documentation, form filling, transcription, and reference metadata
    endpoints.
  contact: {}
  version: '1.0'
servers:
  - url: https://sdp.suki.ai
    description: >-
      Production base URL for Suki Developer Platform REST APIs. WebSocket
      endpoints use the same host with `wss://`.
security:
  - SukiTokenAuth: []
paths:
  /fhir-push/upload:
    put:
      tags:
        - /api/v1/fhir-push
      summary: Upload FHIR bundle to pre-signed URL
      description: >-
        Upload your FHIR bundle JSON directly to the pre-signed URL obtained
        from GET /api/v1/fhir-push/upload-url. This is Step 2 of the CKG Data
        Ingestion workflow. The upload URL is a Google Cloud Storage pre-signed
        URL that expires after 15 minutes. Use HTTP PUT with the required
        headers to upload your FHIR bundle (max 500 MB). A 200 OK response
        confirms the upload was accepted. Any 4xx response indicates an issue
        such as an expired URL, invalid headers, or file size violation.
      parameters:
        - name: upload_url
          in: query
          description: >-
            The complete pre-signed upload URL returned from GET
            /api/v1/fhir-push/upload-url. This is a Google Cloud Storage URL,
            not a Suki API endpoint.
          required: true
          schema:
            type: string
            example: https://storage.googleapis.com/suki-fhir-push/...
      requestBody:
        required: true
        description: Your FHIR bundle as raw JSON (not form-encoded, not base64-encoded).
        content:
          application/json:
            schema:
              type: object
              description: >-
                FHIR Bundle resource containing Patient, Encounter,
                Practitioner, and other clinical resources.
              example:
                resourceType: Bundle
                type: transaction
                entry:
                  - resource:
                      resourceType: Patient
                      id: patient-789
                      name:
                        - family: Doe
                          given:
                            - John
                  - resource:
                      resourceType: Encounter
                      id: enc-123
                      subject:
                        reference: Patient/patient-789
      responses:
        '200':
          description: >-
            Upload successful. The FHIR bundle has been accepted and will be
            processed. Continue to Step 3 to poll ingestion status using the
            transaction_id from Step 1.
          content:
            text/plain:
              schema:
                type: string
                example: ''
        '400':
          description: >-
            Bad Request. Common causes: missing required headers (Content-Type
            or x-goog-content-length-range), file exceeds 500 MB size limit, or
            invalid JSON.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/controllers.BadRequestError'
        '403':
          description: >-
            Forbidden. The upload URL has expired (15 minutes from creation).
            Request a new upload URL via GET /api/v1/fhir-push/upload-url and
            retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/controllers.ForbiddenError'
        '404':
          description: Not found. The upload URL or target resource was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/controllers.NotFoundError'
        '410':
          description: >-
            Gone. The upload URL is no longer valid. Request a new URL and
            retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/controllers.NotFoundError'
      security: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |-
            # Upload URL obtained from Step 1
            UPLOAD_URL="<upload_url_from_step_1>"

            curl -X PUT "$UPLOAD_URL" \
              -H "Content-Type: application/json" \
              -H "x-goog-content-length-range: 0,524288000" \
              --data-binary @my-fhir-bundle.json

            # 200 OK response indicates success
        - lang: python
          label: Python
          source: |-
            import requests

            upload_url = "<upload_url_from_step_1>"

            headers = {
                "Content-Type": "application/json",
                "x-goog-content-length-range": "0,524288000"
            }

            with open("my-fhir-bundle.json", "rb") as f:
                response = requests.put(
                    upload_url,
                    headers=headers,
                    data=f,
                    timeout=300  # 5 minutes for large files
                )

            response.raise_for_status()
            print(f"Upload successful: {response.status_code}")
        - lang: javascript
          label: TypeScript
          source: >-
            import fs from "fs";


            const uploadUrl = "<upload_url_from_step_1>";


            const bundleContent = fs.readFileSync("my-fhir-bundle.json",
            "utf-8");


            const response = await fetch(uploadUrl, {
              method: "PUT",
              headers: {
                "Content-Type": "application/json",
                "x-goog-content-length-range": "0,524288000",
              },
              body: bundleContent,
            });


            if (!response.ok) {
              throw new Error(`Upload failed: ${response.status}`);
            }


            console.log("Upload successful");
components:
  schemas:
    controllers.BadRequestError:
      description: Bad Request Response
      type: object
      properties:
        code:
          type: integer
          example: 400
        message:
          type: string
          example: invalid request
    controllers.ForbiddenError:
      type: object
      properties:
        code:
          type: integer
          example: 403
          description: HTTP status code for the error.
        message:
          type: string
          example: forbidden
          description: Human-readable description of the authorization failure.
      description: Error response when the caller lacks permission.
    controllers.NotFoundError:
      description: Not Found Response
      type: object
      properties:
        code:
          type: integer
          example: 404
        message:
          type: string
          example: not found
  securitySchemes:
    SukiTokenAuth:
      type: apiKey
      in: header
      name: sdp_suki_token
      description: >-
        Suki access token (`suki_token`) from Login or Register. Expires after
        one hour.

````