> ## 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 to CKG and Poll Ingestion

> Request a pre-signed upload URL, PUT a FHIR bundle, then poll ingestion until COMPLETED

export const CbRecipeMeta = ({items = []}) => <div className="cb-recipe-meta">
    {items.map(item => <span className={`cb-recipe-pill cb-recipe-pill--${item.type}${item.tone ? ` cb-recipe-pill--${item.tone}` : ""}`} key={`${item.type}-${item.label}`}>
        {item.type === "time" ? <svg className="cb-recipe-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
            <circle cx="12" cy="12" r="10" />
            <polyline points="12 6 12 12 16 14" />
          </svg> : item.type === "level" ? <svg className="cb-recipe-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
            <path d="M2 20h.01" />
            <path d="M7 20v-4" />
            <path d="M12 20v-8" />
            <path d="M17 20V8" />
            <path d="M22 20V4" />
          </svg> : item.type === "surface" ? <svg className="cb-recipe-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
            <polyline points="16 18 22 12 16 6" />
            <polyline points="8 6 2 12 8 18" />
          </svg> : <svg className="cb-recipe-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
            <path d="M12 3v3" />
            <path d="M12 18v3" />
            <path d="M3 12h3" />
            <path d="M18 12h3" />
            <path d="M5.6 5.6l2.1 2.1" />
            <path d="M16.3 16.3l2.1 2.1" />
            <path d="M5.6 18.4l2.1-2.1" />
            <path d="M16.3 7.7l2.1-2.1" />
          </svg>}
        {item.label}
      </span>)}
  </div>;

export const CbRecipePage = ({title, description, meta, recipeId, sidebar = {}, children}) => {
  const {tutorialHref, tutorialLabel = "Open Full Tutorial", apiHref, apiLabel = "View API Reference", authSections = [], glance = [], headers = []} = sidebar;
  return <div className="hp-wrap api-overview-wrap docs-frame-wrap cb-recipe-wrap" data-cb-recipe data-disable-read-time="true">
      <header className="cb-recipe-hero tut-hub-intro">
        <h1 className="sdk-overview-main-title api-overview-hero-page-title">{title}</h1>
        {description ? <div className="wse-prose">{description}</div> : null}
        {meta ? <div className="cb-recipe-meta-slot">{meta}</div> : null}
      </header>

      <div className="cb-recipe-body">
        <div className="cb-recipe-layout">
          <div className="cb-recipe-main prose prose-gray dark:prose-invert">{children}</div>

          <aside className="cb-recipe-sidebar" aria-label="Recipe actions and metadata">
            {recipeId ? <div className="cb-recipe-sidebar-card cb-recipe-sidebar-card--run">
                <div className="cb-run" data-cb-run data-recipe-id={recipeId} data-recipe-title={title}>
                  <button type="button" className="cb-run-trigger" aria-expanded="false">
                    <svg className="cb-run-trigger-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                      <path d="M8 5v14l11-7L8 5z" />
                    </svg>
                    <span>Run This Recipe</span>
                    <svg className="cb-run-trigger-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
                      <polyline points="6 9 12 15 18 9" />
                    </svg>
                  </button>
                </div>
              </div> : null}

            <div className="cb-recipe-sidebar-card cb-recipe-sidebar-card--actions">
              <div className="cb-recipe-sidebar-actions">
                {tutorialHref ? <a className="cb-recipe-action cb-recipe-action--primary" href={tutorialHref}>
                    <svg className="cb-recipe-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
                      <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
                      <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
                    </svg>
                    <span>{tutorialLabel}</span>
                  </a> : null}
                {apiHref ? <a className="cb-recipe-action cb-recipe-action--secondary" href={apiHref}>
                    <svg className="cb-recipe-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
                      <path d="M8 4H6a2 2 0 0 0-2 2v4" />
                      <path d="M8 20H6a2 2 0 0 1-2-2v-4" />
                      <path d="M16 4h2a2 2 0 0 1 2 2v4" />
                      <path d="M16 20h2a2 2 0 0 0 2-2v-4" />
                    </svg>
                    <span>{apiLabel}</span>
                  </a> : null}
              </div>
              <div className="cb-recipe-sidebar-caption">
                Step-by-step tutorial and API reference for this workflow.
              </div>
            </div>

            {authSections.length ? <div className="cb-recipe-sidebar-card">
                <div className="cb-recipe-sidebar-kicker">Auth</div>
                {authSections.map(section => <div className="cb-recipe-auth-block" key={section.title}>
                    <div className="cb-recipe-auth-title">{section.title}</div>
                    <div className="cb-recipe-sidebar-copy">
                      {section.text}{" "}
                      {section.href ? <a href={section.href}>{section.linkLabel || "Learn more"}</a> : null}
                    </div>
                    {section.scopes?.length ? <div className="cb-recipe-scope-pills cb-recipe-auth-scopes">
                        {section.scopes.map(scope => <span className="cb-recipe-scope-pill" key={scope}>
                            {scope}
                          </span>)}
                      </div> : null}
                  </div>)}
              </div> : null}

            {glance.length ? <div className="cb-recipe-sidebar-card">
                <div className="cb-recipe-sidebar-kicker">At a glance</div>
                <dl className="cb-recipe-glance">
                  {glance.map(row => <div className="cb-recipe-glance-row" key={row.label}>
                      <dt>{row.label}</dt>
                      <dd>{row.value}</dd>
                    </div>)}
                </dl>
              </div> : null}

            {headers.length ? <div className="cb-recipe-sidebar-card">
                <div className="cb-recipe-sidebar-kicker">Required headers</div>
                <div className="cb-recipe-auth-title">Partner API</div>
                <div className="cb-recipe-scope-pills">
                  {headers.map(h => <span className="cb-recipe-scope-pill" key={h}>
                      {h}
                    </span>)}
                </div>
              </div> : null}
          </aside>
        </div>
      </div>
    </div>;
};

<CbRecipePage
  recipeId="upload-fhir-to-ckg-and-poll"
  title="Upload FHIR to CKG and Poll Ingestion"
  description="Ingest FHIR into CKG before Patient Summary generation: request a pre-signed upload URL, PUT the bundle, then poll ingestion status until `COMPLETED`. Starting generation too early returns empty or failed summaries."
  meta={
<CbRecipeMeta
  items={[
    { type: "time", label: "5 min" },
    { type: "level", label: "Beginner" },
    { type: "product", label: "Patient Summary" },
    { type: "surface", label: "API", tone: "api" },
  ]}
/>
}
  sidebar={{
apiHref: "/patient-summary-api-reference/ckg-data-ingestion/upload",
authSections: [
  {
    title: "Partner API",
    text: "Authenticate every REST call with Partner Token headers on each request. See",
    href: "/documentation/get-started/partner-authentication",
    linkLabel: "Partner authentication",
    scopes: ["sdp_suki_token", "sdp_provider_id"],
  },
],
glance: [
  { label: "Product", value: "Patient Summary" },
  { label: "Surface", value: "Partner API" },
  { label: "Time", value: "~5 min" },
  { label: "Level", value: "Beginner" },
],
}}
>
  ## Problem

  Patient Summary generation starts before Clinical Knowledge Graph (CKG) ingestion finishes, so summaries are empty or fail. The generate endpoint expects patient, encounter, and practitioner data to already exist in CKG.

  CKG ingestion is asynchronous. You must request a pre-signed upload URL, PUT the FHIR R4 bundle, then poll ingestion status until `COMPLETED` before triggering Patient Summary generation.

  ## Architecture

  ```mermaid actions={false} theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  %%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#FFFBDE','primaryTextColor':'#1C1C1C','primaryBorderColor':'#FFD147','lineColor':'#6F5410','secondaryColor':'#FFFDF5','tertiaryColor':'#FFEB94','fontSize':'13px'}}}%%
  flowchart LR
    A[Request upload URL] --> B[PUT FHIR bundle]
    B --> C[Poll status]
    C --> D[COMPLETED]
    D --> E[Generate summary]
  ```

  ## Prerequisites

  <Check>Completed [Partner onboarding](/documentation/get-started/partner-onboarding) and can obtain an `sdp_suki_token` from Login for CKG upload endpoints.</Check>

  <Check>`organization_id` and a valid FHIR R4 bundle JSON file with patient, encounter, and practitioner resources. You can use the [Example FHIR R4 bundle](/documentation/concepts/patient-summary/integrate-ckg#example-fhir-r4-bundle) if you need to understand the structure of the bundle.</Check>

  <Check>Matching `fhir_encounter_id` and `fhir_practitioner_id` values you will use later in Patient Summary generation.</Check>

  ## Solution

  Request an upload URL with [Request upload URL](/patient-summary-api-reference/ckg-data-ingestion/upload), PUT the FHIR bundle, then poll [Check ingestion status](/patient-summary-api-reference/ckg-data-ingestion/ingestion-status) until `COMPLETED`. Treat `FAILED` and `ARCHIVED` as terminal.

  <Tabs>
    <Tab title="TypeScript">
      ```typescript theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      import fs from "fs";

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

      async function uploadAndWaitForCkg(organizationId: string, bundlePath: string) {
        const params = new URLSearchParams({
          organization_id: organizationId,
          correlation_id: "encounter-abc-123",
        });

        const urlRes = await fetch(`${BASE_URL}/api/v1/fhir-push/upload-url?${params}`, {
          headers: { Authorization: `Bearer ${sdpSukiToken}` },
        });
        if (!urlRes.ok) {
          throw new Error(`Request upload URL failed: ${urlRes.status}`);
        }

        const { transaction_id: transactionId, upload_url: uploadUrl } = await urlRes.json();
        const bundle = fs.readFileSync(bundlePath, "utf-8");

        const uploadRes = await fetch(uploadUrl, {
          method: "PUT",
          headers: {
            "Content-Type": "application/json",
            "x-goog-content-length-range": "0,524288000",
          },
          body: bundle,
        });
        if (!uploadRes.ok) {
          throw new Error(`FHIR upload failed: ${uploadRes.status}`);
        }

        for (let i = 0; i < 120; i++) {
          const statusRes = await fetch(
            `${BASE_URL}/api/v1/fhir-push/status/${transactionId}`,
            { headers: { Authorization: `Bearer ${sdpSukiToken}` } }
          );
          if (!statusRes.ok) {
            throw new Error(`Get ingestion status failed: ${statusRes.status}`);
          }

          const { status } = await statusRes.json();
          if (status === "COMPLETED") return "COMPLETED";
          if (status === "FAILED" || status === "ARCHIVED") return status;

          await new Promise((r) => setTimeout(r, 5000));
        }

        throw new Error("Timed out waiting for CKG ingestion");
      }

      const outcome = await uploadAndWaitForCkg(organizationId, "my-fhir-bundle.json");
      if (outcome === "COMPLETED") {
        // Generate Patient Summaries
      }
      ```
    </Tab>

    <Tab title="Python">
      ```python theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      import time
      import requests

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

      def upload_and_wait_for_ckg(organization_id: str, bundle_path: str) -> str:
          url_res = requests.get(
              f"{BASE_URL}/api/v1/fhir-push/upload-url",
              params={
                  "organization_id": organization_id,
                  "correlation_id": "encounter-abc-123",
              },
              headers={"Authorization": f"Bearer {sdp_suki_token}"},
              timeout=30,
          )
          url_res.raise_for_status()
          data = url_res.json()
          transaction_id = data["transaction_id"]
          upload_url = data["upload_url"]

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

          for _ in range(120):
              status_res = requests.get(
                  f"{BASE_URL}/api/v1/fhir-push/status/{transaction_id}",
                  headers={"Authorization": f"Bearer {sdp_suki_token}"},
                  timeout=30,
              )
              status_res.raise_for_status()
              status = status_res.json().get("status")
              if status == "COMPLETED":
                  return "COMPLETED"
              if status in ("FAILED", "ARCHIVED"):
                  return status
              time.sleep(5)

          raise TimeoutError("Timed out waiting for CKG ingestion")

      outcome = upload_and_wait_for_ckg(organization_id, "my-fhir-bundle.json")
      if outcome == "COMPLETED":
          # Generate Patient Summaries
          pass
      ```
    </Tab>
  </Tabs>

  <Steps>
    <Step title="Request an upload URL">
      Call [Request upload URL](/patient-summary-api-reference/ckg-data-ingestion/upload) with your `organization_id` (your partner organization ID for that org. It can be a UUID or non-UUID string. But not a Suki UUID.) and a `correlation_id`. Authenticate with the same `sdp_suki_token` from Login.
    </Step>

    <Step title="PUT the FHIR bundle">
      Upload the bundle JSON to the pre-signed URL within **15 minutes**. Include `Content-Type: application/json` and the required content-length header. Refer to [Example FHIR R4 bundle](/documentation/concepts/patient-summary/integrate-ckg#example-fhir-r4-bundle) if you need sample data.
    </Step>

    <Step title="Poll ingestion status">
      Poll [Check ingestion status](/patient-summary-api-reference/ckg-data-ingestion/ingestion-status) with the returned `transaction_id` until status is `COMPLETED`, `FAILED`, or `ARCHIVED`.
    </Step>

    <Step title="Generate Patient Summaries">
      Only after `COMPLETED`, call [Generate a Patient Summary for one encounter](/documentation/cookbooks/generate-patient-summary-for-encounter).
    </Step>
  </Steps>

  ## Try it

  <Card>
    * **Status reaches COMPLETED**

      After a successful PUT, polling should eventually return `COMPLETED`. If status stays pending, confirm the bundle uploaded before the URL expired.

    * **Terminal failures stop the loop**

      `FAILED` and `ARCHIVED` are terminal. Do not retry generation until you fix the bundle or re-upload.

    * **Identifiers match downstream calls**

      Patient, encounter, and practitioner IDs in the FHIR bundle must match the `fhir_encounter_id` and `fhir_practitioner_id` you pass to generate.
  </Card>

  ## Common mistakes

  <Warning>
    * Starting generation before ingestion status is `COMPLETED`.
    * Letting the upload URL expire (**15 minutes**) before the PUT.
    * Using different identifiers in the FHIR bundle than later `fhir_encounter_id` / `fhir_practitioner_id` values.
    * Using a different token for CKG upload than the `sdp_suki_token` from Login.
  </Warning>

  ## Related cookbooks

  <Columns cols={2}>
    <Card title="Generate a Patient Summary for One Encounter" href="/documentation/cookbooks/generate-patient-summary-for-encounter" arrow={true} icon="book-open">
      Trigger generation and save the ID.
    </Card>

    <Card title="Poll Patient Summary Status Before Retrieve" href="/documentation/cookbooks/poll-patient-summary-status-before-retrieve" arrow={true} icon="book-open">
      Poll until status is COMPLETED.
    </Card>
  </Columns>
</CbRecipePage>
