> ## 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.

# Request FHIR Bundle Upload URL

> Request a pre-signed upload URL for uploading FHIR bundles to Suki's Clinical Knowledge Graph (CKG)

Use this endpoint to generate a **pre-signed upload URL** for your FHIR bundle. This is **Step 1** of the CKG data ingestion workflow.

The response includes:

* `upload_url`: A Google Cloud Storage pre-signed URL that is valid for 15 minutes and can be used to upload the FHIR bundle.
* `transaction_id`: A unique identifier for the ingestion job. Use this value to check the ingestion status in **Step 3**.

<Note>
  To generate accurate Patient Summaries, include consistent patient, encounter, and practitioner identifiers in your FHIR resources so they match the `fhir_encounter_id` and `fhir_practitioner_id` values used for generation and retrieval.
</Note>

## CKG data ingestion steps

| Step | Action                                                                                                                    |
| ---- | :------------------------------------------------------------------------------------------------------------------------ |
| 1    | **Request upload URL** - This API is the first step in the three-step CKG Data Ingestion workflow.                        |
| 2    | **Upload FHIR bundle** - Use [PUT to upload your bundle](/patient-summary-api-reference/ckg-data-ingestion/fhir-data) API |
| 3    | **Poll status** - Check [ingestion status](/patient-summary-api-reference/ckg-data-ingestion/ingestion-status) API        |

Once you have the upload URL, use it to **upload** your FHIR bundle and **check** the ingestion status by using the following endpoints:

<div className="doc-guide-btn-row">
  <a href="/patient-summary-api-reference/ckg-data-ingestion/fhir-data" className="doc-guide-btn">
    Step 2: Upload FHIR Bundle
  </a>

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


## OpenAPI

````yaml GET /api/v1/fhir-push/upload-url
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:
  /api/v1/fhir-push/upload-url:
    get:
      tags:
        - /api/v1/fhir-push
      summary: >-
        GET
        /api/v1/fhir-push/upload-url?organization_id={uuid}&correlation_id={optional}
      description: >-
        Request a pre-signed upload URL for uploading a FHIR bundle to Suki's
        Clinical Knowledge Graph (CKG) ingestion pipeline. This is Step 1 of the
        three-step CKG Data Ingestion workflow. The returned `upload_url` is
        valid for 15 minutes and accepts a PUT request with your FHIR bundle
        JSON (max 500 MB). Use the `transaction_id` from the response to poll
        processing status via GET /api/v1/fhir-push/status/{transaction_id}.
        FHIR data ingested through this API powers Patient Summary generation.
        Include consistent patient, encounter, and practitioner identifiers in
        your FHIR resources so they match the `fhir_encounter_id` and
        `fhir_practitioner_id` values used for generation and retrieval.
      parameters:
        - name: organization_id
          in: query
          description: Your organization UUID provided during onboarding (required).
          required: true
          schema:
            type: string
        - name: correlation_id
          in: query
          description: >-
            **Optional** client-defined identifier for ordering or correlating
            uploads (max 128 chars). If omitted the server generates one.
          required: false
          schema:
            type: string
            maxLength: 128
      responses:
        '200':
          description: Upload URL returned successfully.
          content:
            application/json:
              schema:
                type: object
                required:
                  - transaction_id
                  - upload_url
                  - expires_at
                properties:
                  transaction_id:
                    type: string
                    description: Unique identifier for this upload transaction.
                  upload_url:
                    type: string
                    description: >-
                      Pre-signed URL to PUT your FHIR bundle to (expires in ~15
                      minutes).
                  expires_at:
                    type: string
                    format: date-time
                    description: RFC 3339 timestamp when the upload_url expires.
                  correlation_id:
                    type: string
                    description: >-
                      Echo of client-supplied correlation_id or server-generated
                      value.
              examples:
                success:
                  value:
                    transaction_id: dea454fe-fecb-4cf0-aaa7-fa84d19c28f8
                    upload_url: https://storage.googleapis.com/...
                    expires_at: '2026-05-18T06:55:42Z'
                    correlation_id: encounter-12345-upload-1
        '400':
          description: Bad request. Missing or invalid query parameters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/controllers.BadRequestError'
        '401':
          description: Unauthorized. Missing or invalid SDP JWT bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/controllers.AuthenticationError'
        '403':
          description: >-
            Forbidden. Token lacks required scopes or access to the
            organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/controllers.ForbiddenError'
        '404':
          description: Not found. The requested resource or organization was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/controllers.NotFoundError'
        '500':
          description: Internal server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/controllers.InternalServerError'
      security:
        - SukiTokenAuth: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -G "https://sdp.suki-stage.com/api/v1/fhir-push/upload-url" \
              --data-urlencode "organization_id=your-organization-id" \
              --data-urlencode "correlation_id=encounter-abc-123" \
              -H "Authorization: Bearer YOUR_SDP_JWT_BEARER_TOKEN"
        - lang: python
          label: Python
          source: >-
            import requests

            import time

            import json


            BASE_URL = "https://sdp.suki.ai"

            sdp_token = "<your-sdp-jwt>"

            org_id = "<your-organization-uuid>"


            # Step 1: Request upload URL

            params = {"organization_id": org_id, "correlation_id":
            "encounter-abc-123"}

            headers = {"Authorization": f"Bearer {sdp_token}"}


            resp = requests.get(
                f"{BASE_URL}/api/v1/fhir-push/upload-url",
                params=params,
                headers=headers,
                timeout=30
            )

            resp.raise_for_status()

            data = resp.json()


            transaction_id = data["transaction_id"]

            upload_url = data["upload_url"]


            print(f"Transaction ID: {transaction_id}")


            # Step 2: Upload FHIR bundle

            with open("my-fhir-bundle.json", "rb") as f:
                upload_resp = requests.put(
                    upload_url,
                    headers={
                        "Content-Type": "application/json",
                        "x-goog-content-length-range": "0,524288000"
                    },
                    data=f,
                    timeout=300
                )
            upload_resp.raise_for_status()

            print("Upload complete. Polling status...")


            # Step 3: Poll status

            while True:
                status_resp = requests.get(
                    f"{BASE_URL}/api/v1/fhir-push/status/{transaction_id}",
                    headers=headers,
                    timeout=30
                )
                status_data = status_resp.json()
                state = status_data["status"]
                print(f"Status: {state}")
                
                if state in ["COMPLETED", "FAILED", "ARCHIVED"]:
                    print(json.dumps(status_data, indent=2))
                    break
                
                time.sleep(5)
        - lang: javascript
          label: TypeScript
          source: >-
            import fs from "fs";


            const BASE_URL = "https://sdp.suki.ai";

            const sdpToken = "<your-sdp-jwt>";

            const orgId = "<your-organization-uuid>";


            // Step 1: Request upload URL

            const params = new URLSearchParams({
              organization_id: orgId,
              correlation_id: "encounter-abc-123",
            });


            const resp = await
            fetch(`${BASE_URL}/api/v1/fhir-push/upload-url?${params}`, {
              headers: { Authorization: `Bearer ${sdpToken}` },
            });


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


            const data = await resp.json();

            const transactionId = data.transaction_id;

            const uploadUrl = data.upload_url;


            console.log(`Transaction ID: ${transactionId}`);


            // Step 2: Upload FHIR bundle

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


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


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


            console.log("Upload complete. Polling status...");


            // Step 3: Poll status

            while (true) {
              const statusResp = await fetch(
                `${BASE_URL}/api/v1/fhir-push/status/${transactionId}`,
                { headers: { Authorization: `Bearer ${sdpToken}` } }
              );
              
              const statusData = await statusResp.json();
              console.log(`Status: ${statusData.status}`);
              
              if (["COMPLETED", "FAILED", "ARCHIVED"].includes(statusData.status)) {
                console.log(JSON.stringify(statusData, null, 2));
                break;
              }
              
              await new Promise((resolve) => setTimeout(resolve, 5000));
            }
components:
  schemas:
    controllers.BadRequestError:
      description: Bad Request Response
      type: object
      properties:
        code:
          type: integer
          example: 400
        message:
          type: string
          example: invalid request
    controllers.AuthenticationError:
      description: Authentication Failure Response
      type: object
      properties:
        code:
          type: integer
          example: 401
        message:
          type: string
          example: invalid token
    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
    controllers.InternalServerError:
      description: Internal Server Error Response
      type: object
      properties:
        code:
          type: integer
          example: 500
        message:
          type: string
          example: internal server error
  securitySchemes:
    SukiTokenAuth:
      type: apiKey
      in: header
      name: sdp_suki_token
      description: >-
        Suki access token (`suki_token`) from Login or Register. Expires after
        one hour.

````