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

# Patient Summary API Quickstart

> Upload FHIR data, trigger summary generation, and retrieve Patient Summaries on staging

This quickstart walks you through one successful Patient Summary workflow on staging: upload FHIR data to CKG, authenticate, trigger summary generation for an encounter, poll job status, and retrieve the Patient Summary.

Your application owns FHIR upload data orchestration, summary generation triggers, job status polling, and how retrieved summaries are presented or integrated into your product.

**What you will do**

1. **Upload FHIR data** to CKG with an SDP JWT bearer token and poll ingestion status.
2. **Authenticate** to get an `sdp_suki_token` (and **register** the user if needed).
3. **Trigger** summary generation for an encounter.
4. **Poll** job status until complete and **retrieve** the full summary or pre-visit summary.

<Tip>
  **Prefer one paste-ready file?** Use the [Complete staging script](#complete-staging-script) in the preferred language below, then follow the numbered steps for the same flow with full explanations.
</Tip>

<Tip>
  **Using an AI coding tool?**

  Copy the prompt below to point your agent at the Patient Summary skill and [Documentation MCP](/documentation/references/mcp). For every task skill, refer to [AI coding tools](/documentation/references/ai-coding-tools).

  <Prompt description="Fetch the Patient Summary skill and connect the documentation MCP." icon="gear" iconType="regular" actions={["copy", "cursor"]}>
    Build Patient Summary with Suki for Partners.
    Fetch the Patient Summary build skill:
    [https://developer.suki.ai/.well-known/agent-skills/suki-patient-summary/SKILL.md](https://developer.suki.ai/.well-known/agent-skills/suki-patient-summary/SKILL.md)
    Connect the documentation MCP for page search:
    [https://developer.suki.ai/documentation/references/mcp](https://developer.suki.ai/documentation/references/mcp)
  </Prompt>
</Tip>

## Access and credentials

You need partner credentials to use the Suki Patient Summary APIs.

Contact our [Partnership team](https://www.suki.ai/suki-partners/) to get your credentials. They will guide you through the [Onboarding process](/documentation/get-started/partner-onboarding) and provide what you need to get started.

### Prerequisites

To use the Suki Patient Summary APIs, you must have the following:

* Partner credentials for Suki authentication endpoints (`partner_id` and `partner_token`).
* Ability to generate and sign RS256 SDP JWT bearer tokens for CKG Data Ingestion endpoints.
* FHIR R4 bundles containing patient clinical data (conditions, medications, encounters, observations).
* A publicly accessible <Tooltip tip="JSON Web Key Set. A set of keys containing the public keys used to verify any JWT issued by the authorization server." cta="View in Glossary" href="/Glossary/j">JWKS</Tooltip> endpoint (or Okta authorization server) for token validation.

### Environments to use for development and testing

This guide uses **`https://sdp.suki-stage.com`** for API examples (staging).

<Callout icon="code" color="#FFC107" iconType="regular">
  **Important**:

  * The production environment is **`https://sdp.suki.ai`**.
  * The staging environment is **`https://sdp.suki-stage.com`**.
  * Your partnership team will confirm which environment, base URL, and credentials apply for your integration.
</Callout>

## Complete staging script

Replace the credential placeholders, put a FHIR R4 bundle at `patient-fhir-bundle.json`, then run. For detailed explanations of each step, refer to the numbered steps below.

* **Python:** `pip install requests` then `python patient_summary_staging.py`.
* **TypeScript (Node):** `npx tsx patient_summary_staging.ts` (Node 18+).

<CodeGroup>
  ```python Python expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  # patient_summary_staging.py
  # Flow: upload FHIR → login (register once if needed) → trigger generation → poll → retrieve summary
  # pip install requests

  import json
  import time
  from typing import Any, Optional

  import requests

  BASE_URL = "https://sdp.suki-stage.com"

  # Replace these with your credentials
  SDP_TOKEN = "YOUR_SDP_JWT_BEARER_TOKEN"  # RS256 signed, includes sdp_partner_id claim
  PARTNER_ID = "your-partner-id"
  PARTNER_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."  # Keep on your backend only
  PROVIDER_ID = "provider-123"  # Optional; required for some auth flows
  SDP_PROVIDER_ID = ""  # Leave empty unless your partnership requires sdp_provider_id

  # CKG identifiers
  ORG_ID = "your-organization-id"
  CORRELATION_ID = "encounter-abc-123"

  # FHIR identifiers (must match IDs in your FHIR bundle)
  FHIR_ENCOUNTER_ID = "encounter-123"
  FHIR_PRACTITIONER_ID = "practitioner-456"
  FHIR_BUNDLE_PATH = "patient-fhir-bundle.json"


  def expect_status(response: requests.Response, url: str, want: int) -> None:
      if response.status_code == want:
          return
      detail = (response.text or "")[:500]
      try:
          body = response.json()
          if isinstance(body, dict) and body.get("message"):
              detail = str(body["message"])
      except ValueError:
          pass
      raise RuntimeError(f"HTTP {response.status_code} {url}: {detail}")


  def ckg_headers() -> dict[str, str]:
      return {"Authorization": f"Bearer {SDP_TOKEN}"}


  def rest_headers(suki_token: str) -> dict[str, str]:
      headers = {
          "sdp_suki_token": suki_token,
          "Content-Type": "application/json",
      }
      if SDP_PROVIDER_ID:
          headers["sdp_provider_id"] = SDP_PROVIDER_ID
      return headers


  def request_upload_url(org_id: str, correlation_id: str) -> dict[str, str]:
      url = f"{BASE_URL}/api/v1/fhir-push/upload-url"
      r = requests.get(
          url,
          params={"organization_id": org_id, "correlation_id": correlation_id},
          headers=ckg_headers(),
          timeout=60,
      )
      expect_status(r, url, 200)
      data = r.json()
      return {"transaction_id": data["transaction_id"], "upload_url": data["upload_url"]}


  def upload_fhir_bundle(upload_url: str, bundle_path: str) -> None:
      with open(bundle_path, "rb") as f:
          bundle_data = f.read()
      r = requests.put(
          upload_url,
          data=bundle_data,
          headers={
              "Content-Type": "application/json",
              "x-goog-content-length-range": "0,524288000",
          },
          timeout=300,
      )
      if r.status_code not in (200, 204):
          raise RuntimeError(f"Upload failed: {r.status_code} {r.text}")


  def poll_ingestion_status(transaction_id: str) -> str:
      url = f"{BASE_URL}/api/v1/fhir-push/status/{transaction_id}"
      while True:
          r = requests.get(url, headers=ckg_headers(), timeout=60)
          expect_status(r, url, 200)
          status = r.json()["status"]
          print("CKG ingestion status:", status)
          if status == "COMPLETED":
              return status
          if status in ("FAILED", "ARCHIVED"):
              raise RuntimeError(f"Ingestion failed: {status}")
          time.sleep(5)


  def login(partner_id: str, partner_token: str, provider_id: Optional[str] = None) -> str:
      url = f"{BASE_URL}/api/v1/auth/login"
      payload: dict[str, Any] = {"partner_id": partner_id, "partner_token": partner_token}
      if provider_id:
          payload["provider_id"] = provider_id
      r = requests.post(url, json=payload, timeout=60)
      expect_status(r, url, 200)
      token = r.json().get("suki_token")
      if not token:
          raise RuntimeError("login response missing suki_token")
      return token


  def register_provider(
      partner_id: str,
      partner_token: str,
      provider_name: str,
      provider_org_id: str,
      provider_id: Optional[str] = None,
  ) -> None:
      url = f"{BASE_URL}/api/v1/auth/register"
      payload: dict[str, Any] = {
          "partner_id": partner_id,
          "partner_token": partner_token,
          "provider_name": provider_name,
          "provider_org_id": provider_org_id,
      }
      if provider_id:
          payload["provider_id"] = provider_id
      r = requests.post(url, json=payload, timeout=60)
      if r.status_code not in (201, 409):
          expect_status(r, url, 201)


  def login_with_register_fallback(
      partner_id: str,
      partner_token: str,
      provider_id: Optional[str],
      provider_name: str,
      provider_org_id: str,
  ) -> str:
      try:
          return login(partner_id, partner_token, provider_id)
      except RuntimeError as err:
          if "provider_not_registered" not in str(err):
              raise
          register_provider(
              partner_id, partner_token, provider_name, provider_org_id, provider_id
          )
          return login(partner_id, partner_token, provider_id)


  def trigger_summary_generation(
      suki_token: str, fhir_encounter_id: str, fhir_practitioner_id: str
  ) -> None:
      url = f"{BASE_URL}/api/v1/patient-summary/generate/encounter"
      payload = {
          "fhir_encounter_id": fhir_encounter_id,
          "fhir_practitioner_id": fhir_practitioner_id,
      }
      r = requests.post(url, headers=rest_headers(suki_token), json=payload, timeout=60)
      expect_status(r, url, 202)


  def poll_summary_status(
      suki_token: str, fhir_encounter_id: str, fhir_practitioner_id: str
  ) -> str:
      url = f"{BASE_URL}/api/v1/patient-summary/encounter/{fhir_encounter_id}/practitioner/{fhir_practitioner_id}/status"
      while True:
          r = requests.get(url, headers=rest_headers(suki_token), timeout=60)
          expect_status(r, url, 200)
          status = r.json()["status"]
          print("Summary generation status:", status)
          if status == "COMPLETED":
              return status
          if status == "FAILED":
              raise RuntimeError(f"Generation failed: {status}")
          time.sleep(5)


  def get_patient_summary(
      suki_token: str, fhir_encounter_id: str, fhir_practitioner_id: str
  ) -> Any:
      url = f"{BASE_URL}/api/v1/patient-summary/encounter/{fhir_encounter_id}/practitioner/{fhir_practitioner_id}"
      r = requests.get(url, headers=rest_headers(suki_token), timeout=60)
      expect_status(r, url, 200)
      return r.json()


  if __name__ == "__main__":
      print("Step 1: Upload FHIR data to CKG")
      upload_info = request_upload_url(ORG_ID, CORRELATION_ID)
      transaction_id = upload_info["transaction_id"]
      print("Transaction ID:", transaction_id)

      upload_fhir_bundle(upload_info["upload_url"], FHIR_BUNDLE_PATH)
      print("FHIR bundle uploaded")

      poll_ingestion_status(transaction_id)
      print("Ingestion completed")

      print("Step 2: Authenticate")
      suki_token = login_with_register_fallback(
          PARTNER_ID,
          PARTNER_TOKEN,
          PROVIDER_ID,
          provider_name="Dr. Jane Smith",
          provider_org_id="org-456",
      )
      print("Authenticated")

      print("Step 3: Trigger summary generation")
      trigger_summary_generation(suki_token, FHIR_ENCOUNTER_ID, FHIR_PRACTITIONER_ID)
      print("Generation triggered")

      print("Step 4: Poll generation status")
      poll_summary_status(suki_token, FHIR_ENCOUNTER_ID, FHIR_PRACTITIONER_ID)
      print("Generation completed")

      print("Step 5: Retrieve Patient Summary")
      summary = get_patient_summary(suki_token, FHIR_ENCOUNTER_ID, FHIR_PRACTITIONER_ID)
      print(json.dumps(summary, indent=2))
  ```

  ```typescript TypeScript expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  // patient_summary_staging.ts
  // Flow: upload FHIR → login (register once if needed) → trigger generation → poll → retrieve summary
  // Node 18+: npx tsx patient_summary_staging.ts

  import fs from "node:fs";

  const BASE_URL = "https://sdp.suki-stage.com";

  // Replace these with your credentials
  const SDP_TOKEN = "YOUR_SDP_JWT_BEARER_TOKEN"; // RS256 signed, includes sdp_partner_id claim
  const PARTNER_ID = "your-partner-id";
  const PARTNER_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."; // Keep on your backend only
  const PROVIDER_ID = "provider-123"; // Optional; required for some auth flows
  const SDP_PROVIDER_ID = ""; // Leave empty unless your partnership requires sdp_provider_id

  // CKG identifiers
  const ORG_ID = "your-organization-id";
  const CORRELATION_ID = "encounter-abc-123";

  // FHIR identifiers (must match IDs in your FHIR bundle)
  const FHIR_ENCOUNTER_ID = "encounter-123";
  const FHIR_PRACTITIONER_ID = "practitioner-456";
  const FHIR_BUNDLE_PATH = "patient-fhir-bundle.json";

  function ckgHeaders(): Record<string, string> {
    return { Authorization: `Bearer ${SDP_TOKEN}` };
  }

  function restHeaders(sukiToken: string): Record<string, string> {
    const headers: Record<string, string> = {
      sdp_suki_token: sukiToken,
      "Content-Type": "application/json",
    };
    if (SDP_PROVIDER_ID) headers.sdp_provider_id = SDP_PROVIDER_ID;
    return headers;
  }

  async function expectStatus(response: Response, url: string, want: number): Promise<void> {
    if (response.status === want) return;
    const raw = await response.text();
    let detail = raw.slice(0, 500);
    try {
      const body = JSON.parse(raw);
      if (body?.message) detail = String(body.message);
    } catch {
      // keep raw text
    }
    throw new Error(`HTTP ${response.status} ${url}: ${detail}`);
  }

  async function requestUploadUrl(
    orgId: string,
    correlationId: string,
  ): Promise<{ transactionId: string; uploadUrl: string }> {
    const url = `${BASE_URL}/api/v1/fhir-push/upload-url?organization_id=${encodeURIComponent(orgId)}&correlation_id=${encodeURIComponent(correlationId)}`;
    const response = await fetch(url, { headers: ckgHeaders() });
    await expectStatus(response, url, 200);
    const data = (await response.json()) as {
      transaction_id: string;
      upload_url: string;
    };
    return { transactionId: data.transaction_id, uploadUrl: data.upload_url };
  }

  async function uploadFhirBundle(uploadUrl: string, bundlePath: string): Promise<void> {
    const bundleData = fs.readFileSync(bundlePath);
    const response = await fetch(uploadUrl, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
        "x-goog-content-length-range": "0,524288000",
      },
      body: bundleData,
    });
    if (response.status !== 200 && response.status !== 204) {
      throw new Error(`Upload failed: ${response.status} ${await response.text()}`);
    }
  }

  async function pollIngestionStatus(transactionId: string): Promise<string> {
    const url = `${BASE_URL}/api/v1/fhir-push/status/${transactionId}`;
    for (;;) {
      const response = await fetch(url, { headers: ckgHeaders() });
      await expectStatus(response, url, 200);
      const data = (await response.json()) as { status: string };
      console.log("CKG ingestion status:", data.status);
      if (data.status === "COMPLETED") return data.status;
      if (data.status === "FAILED" || data.status === "ARCHIVED") {
        throw new Error(`Ingestion failed: ${data.status}`);
      }
      await new Promise((r) => setTimeout(r, 5000));
    }
  }

  async function login(
    partnerId: string,
    partnerToken: string,
    providerId?: string,
  ): Promise<string> {
    const url = `${BASE_URL}/api/v1/auth/login`;
    const body: Record<string, string> = {
      partner_id: partnerId,
      partner_token: partnerToken,
    };
    if (providerId) body.provider_id = providerId;

    const response = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    await expectStatus(response, url, 200);
    const data = (await response.json()) as { suki_token?: string };
    if (!data.suki_token) throw new Error("login response missing suki_token");
    return data.suki_token;
  }

  async function registerProvider(input: {
    partnerId: string;
    partnerToken: string;
    providerName: string;
    providerOrgId: string;
    providerId?: string;
  }): Promise<void> {
    const url = `${BASE_URL}/api/v1/auth/register`;
    const body: Record<string, string> = {
      partner_id: input.partnerId,
      partner_token: input.partnerToken,
      provider_name: input.providerName,
      provider_org_id: input.providerOrgId,
    };
    if (input.providerId) body.provider_id = input.providerId;

    const response = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (response.status !== 201 && response.status !== 409) {
      await expectStatus(response, url, 201);
    }
  }

  async function loginWithRegisterFallback(
    partnerId: string,
    partnerToken: string,
    providerId: string | undefined,
    providerName: string,
    providerOrgId: string,
  ): Promise<string> {
    try {
      return await login(partnerId, partnerToken, providerId);
    } catch (err) {
      if (!String(err).includes("provider_not_registered")) throw err;
      await registerProvider({
        partnerId,
        partnerToken,
        providerName,
        providerOrgId,
        providerId,
      });
      return login(partnerId, partnerToken, providerId);
    }
  }

  async function triggerSummaryGeneration(
    sukiToken: string,
    fhirEncounterId: string,
    fhirPractitionerId: string,
  ): Promise<void> {
    const url = `${BASE_URL}/api/v1/patient-summary/generate/encounter`;
    const response = await fetch(url, {
      method: "POST",
      headers: restHeaders(sukiToken),
      body: JSON.stringify({
        fhir_encounter_id: fhirEncounterId,
        fhir_practitioner_id: fhirPractitionerId,
      }),
    });
    await expectStatus(response, url, 202);
  }

  async function pollSummaryStatus(
    sukiToken: string,
    fhirEncounterId: string,
    fhirPractitionerId: string,
  ): Promise<string> {
    const url = `${BASE_URL}/api/v1/patient-summary/encounter/${fhirEncounterId}/practitioner/${fhirPractitionerId}/status`;
    for (;;) {
      const response = await fetch(url, { headers: restHeaders(sukiToken) });
      await expectStatus(response, url, 200);
      const data = (await response.json()) as { status: string };
      console.log("Summary generation status:", data.status);
      if (data.status === "COMPLETED") return data.status;
      if (data.status === "FAILED") {
        throw new Error(`Generation failed: ${data.status}`);
      }
      await new Promise((r) => setTimeout(r, 5000));
    }
  }

  async function getPatientSummary(
    sukiToken: string,
    fhirEncounterId: string,
    fhirPractitionerId: string,
  ): Promise<unknown> {
    const url = `${BASE_URL}/api/v1/patient-summary/encounter/${fhirEncounterId}/practitioner/${fhirPractitionerId}`;
    const response = await fetch(url, { headers: restHeaders(sukiToken) });
    await expectStatus(response, url, 200);
    return response.json();
  }

  async function main() {
    console.log("Step 1: Upload FHIR data to CKG");
    const uploadInfo = await requestUploadUrl(ORG_ID, CORRELATION_ID);
    console.log("Transaction ID:", uploadInfo.transactionId);

    await uploadFhirBundle(uploadInfo.uploadUrl, FHIR_BUNDLE_PATH);
    console.log("FHIR bundle uploaded");

    await pollIngestionStatus(uploadInfo.transactionId);
    console.log("Ingestion completed");

    console.log("Step 2: Authenticate");
    const sukiToken = await loginWithRegisterFallback(
      PARTNER_ID,
      PARTNER_TOKEN,
      PROVIDER_ID,
      "Dr. Jane Smith",
      "org-456",
    );
    console.log("Authenticated");

    console.log("Step 3: Trigger summary generation");
    await triggerSummaryGeneration(sukiToken, FHIR_ENCOUNTER_ID, FHIR_PRACTITIONER_ID);
    console.log("Generation triggered");

    console.log("Step 4: Poll generation status");
    await pollSummaryStatus(sukiToken, FHIR_ENCOUNTER_ID, FHIR_PRACTITIONER_ID);
    console.log("Generation completed");

    console.log("Step 5: Retrieve Patient Summary");
    const summary = await getPatientSummary(
      sukiToken,
      FHIR_ENCOUNTER_ID,
      FHIR_PRACTITIONER_ID,
    );
    console.log(JSON.stringify(summary, null, 2));
  }

  main().catch((err) => {
    console.error(err);
    process.exit(1);
  });
  ```
</CodeGroup>

## Steps

<Steps>
  <Step title="Upload FHIR data to CKG">
    **Step 1:**

    Before you can generate Patient Summaries, you must upload FHIR data to Suki's Clinical Knowledge Graph (CKG). Call [Request upload URL API](/patient-summary-api-reference/ckg-data-ingestion/upload) with the following query parameters:

    * **organization\_id** (required): Your organization identifier.
    * **correlation\_id** (optional): Client-supplied identifier for tracing (max 128 characters).

    **Step 2:**

    Upload your FHIR R4 bundle JSON to the `upload_url` using HTTP **PUT** with `Content-Type: application/json` and `x-goog-content-length-range: 0,524288000`.

    <Note>
      You can upload a bundle up to 500 MB in size.
    </Note>

    **Step 3:**

    Poll [Poll ingestion status API](/patient-summary-api-reference/ckg-data-ingestion/ingestion-status) to know the status of the ingestion job.

    <Tip>
      Your FHIR bundle must include patient, encounter, and practitioner identifiers that match what you will use when triggering summary generation in Step 3.
    </Tip>

    **Code example:**

    <CodeGroup>
      ```typescript TypeScript expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      const BASE_URL = "https://sdp.suki-stage.com";
      const SDP_TOKEN = "<YOUR_SDP_JWT_BEARER_TOKEN>";
      const ORG_ID = "your-organization-id";
      const CORRELATION_ID = "encounter-abc-123";

      // Step 1a: Request upload URL
      const uploadUrlResponse = await fetch(
        `${BASE_URL}/api/v1/fhir-push/upload-url?organization_id=${encodeURIComponent(ORG_ID)}&correlation_id=${encodeURIComponent(CORRELATION_ID)}`,
        { headers: { Authorization: `Bearer ${SDP_TOKEN}` } },
      );
      const uploadData = await uploadUrlResponse.json();
      if (!uploadUrlResponse.ok) {
        throw new Error(`Upload URL request failed: ${uploadUrlResponse.status}`);
      }
      const transactionId = uploadData.transaction_id as string;
      const uploadUrl = uploadData.upload_url as string;
      console.log("Transaction ID:", transactionId);

      // Step 1b: Upload FHIR bundle
      // Guard: ensure we received a transaction ID and upload URL before uploading.
      if (!transactionId || !uploadUrl) {
        throw new Error(`Upload URL response missing transaction_id or upload_url: ${JSON.stringify(uploadData)}`);
      }

      // For large bundles (hundreds of MB), prefer streaming the file to the upload URL
      // instead of readFileSync to avoid high memory usage in production.
      const fhirBundle = fs.readFileSync("patient-fhir-bundle.json");
      const uploadResponse = await fetch(uploadUrl, {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
          "x-goog-content-length-range": "0,524288000",
        },
        body: fhirBundle,
      });
      if (uploadResponse.status !== 200 && uploadResponse.status !== 204) {
        throw new Error(`Upload failed: ${uploadResponse.status}`);
      }
      console.log("FHIR bundle uploaded");

      // Step 1c: Poll ingestion status
      for (;;) {
        const statusResponse = await fetch(
          `${BASE_URL}/api/v1/fhir-push/status/${transactionId}`,
          { headers: { Authorization: `Bearer ${SDP_TOKEN}` } },
        );
        const statusData = (await statusResponse.json()) as { status: string };
        console.log("CKG ingestion status:", statusData.status);
        if (statusData.status === "COMPLETED") break;
        if (statusData.status === "FAILED" || statusData.status === "ARCHIVED") {
          throw new Error(`Ingestion failed: ${statusData.status}`);
        }
        await new Promise((r) => setTimeout(r, 5000));
      }
      ```

      ```python Python expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      import requests
      import time

      BASE_URL = "https://sdp.suki-stage.com"
      SDP_TOKEN = "<YOUR_SDP_JWT_BEARER_TOKEN>"
      ORG_ID = "your-organization-id"
      CORRELATION_ID = "encounter-abc-123"

      # Step 1a: Request upload URL
      upload_url_response = requests.get(
          f"{BASE_URL}/api/v1/fhir-push/upload-url",
          params={"organization_id": ORG_ID, "correlation_id": CORRELATION_ID},
          headers={"Authorization": f"Bearer {SDP_TOKEN}"},
          timeout=60,
      )
      if upload_url_response.status_code != 200:
          raise RuntimeError(f"Upload URL request failed: {upload_url_response.status_code}")
      upload_data = upload_url_response.json()
      transaction_id = upload_data["transaction_id"]
      upload_url = upload_data["upload_url"]
      print("Transaction ID:", transaction_id)

      # Step 1b: Upload FHIR bundle
      with open("patient-fhir-bundle.json", "rb") as f:
          fhir_bundle = f.read()
      upload_response = requests.put(
          upload_url,
          data=fhir_bundle,
          headers={
              "Content-Type": "application/json",
              "x-goog-content-length-range": "0,524288000",
          },
          timeout=300,
      )
      if upload_response.status_code not in (200, 204):
          raise RuntimeError(f"Upload failed: {upload_response.status_code}")
      print("FHIR bundle uploaded")

      # Step 1c: Poll ingestion status
      while True:
          status_response = requests.get(
              f"{BASE_URL}/api/v1/fhir-push/status/{transaction_id}",
              headers={"Authorization": f"Bearer {SDP_TOKEN}"},
              timeout=60,
          )
          status_data = status_response.json()
          print("CKG ingestion status:", status_data["status"])
          if status_data["status"] == "COMPLETED":
              break
          if status_data["status"] in ("FAILED", "ARCHIVED"):
              raise RuntimeError(f"Ingestion failed: {status_data['status']}")
          time.sleep(5)
      ```

      ```bash cURL expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      # Set your credentials
      export SDP_TOKEN="<your-sdp-jwt>"
      export ORG_ID="<your-organization-uuid>"
      export BASE="https://sdp.suki-stage.com"

      # Step 1: Request an upload URL
      RESP=$(curl -s -G "${BASE}/api/v1/fhir-push/upload-url" \
        --data-urlencode "organization_id=${ORG_ID}" \
        --data-urlencode "correlation_id=encounter-abc-123" \
        -H "Authorization: Bearer ${SDP_TOKEN}")
      echo "Raw response: $RESP"
      TXN_ID=$(echo "$RESP" | jq -r '.transaction_id')
      UPLOAD_URL=$(echo "$RESP" | jq -r '.upload_url')
      echo "Transaction ID: $TXN_ID"
      if [[ "$TXN_ID" == "null" || -z "$TXN_ID" ]]; then
        echo "ERROR: Failed to get upload URL. Response was:"
        echo "$RESP" | jq .
        exit 1
      fi

      # Step 2: Upload your FHIR bundle
      curl -s -X PUT "$UPLOAD_URL" \
        -H "Content-Type: application/json" \
        -H "x-goog-content-length-range: 0,524288000" \
        --data-binary @patient-fhir-bundle.json
      echo "Upload complete. Polling status..."

      # Step 3: Poll until terminal status
      while true; do
        STATUS=$(curl -s "${BASE}/api/v1/fhir-push/status/${TXN_ID}" \
          -H "Authorization: Bearer ${SDP_TOKEN}")
        STATE=$(echo "$STATUS" | jq -r '.status')
        echo "Current status: $STATE"
        if [[ "$STATE" == "COMPLETED" || "$STATE" == "FAILED" || "$STATE" == "ARCHIVED" ]]; then
          echo "Final result:"
          echo "$STATUS" | jq .
          break
        fi
        sleep 5
      done
      ```
    </CodeGroup>
  </Step>

  <Step title="Authenticate to Get a Suki Token">
    Send a **POST** request to the [Login API](/patient-summary-api-reference/authentication/login) endpoint with the following parameters in the request body:

    * **partner\_id** (required): Your unique <Tooltip tip="A unique identifier assigned by Suki during onboarding that links an application to its configuration in the Suki Developer Platform." cta="View in Glossary" href="/Glossary/p">Partner ID</Tooltip>, which we provide to you securely offline.
    * **partner\_token** (required): The user's OAuth 2.0 ID token (<Tooltip tip="A secure, digitally signed JWT issued by a partner's identity provider after user authentication, passed to Suki SDK for user verification." cta="View in Glossary" href="/Glossary/p">Partner Token</Tooltip>) from your identity provider.
    * **provider\_id** (optional): Unique identifier for the <Tooltip tip="A healthcare professional such as a physician, APP, or nurse who documents care. In Suki integrations, provider identity ties sessions, preferences, and generated notes to the correct clinician." cta="View in Glossary" href="/Glossary/p">provider</Tooltip>. Required for some authentication flows only.

    <Tip>
      **Handling an unregistered user**:

      * If the user is not yet registered in our system, the `/login` request will fail.
      * In this case, you must first call the [Register API](/patient-summary-api-reference/authentication/register) endpoint to create the user, then call `/login` again.
      * You only need to call the register endpoint once for each new user.
      * Refer to the [Register API reference](/patient-summary-api-reference/authentication/register) for the full specification.
    </Tip>

    **Code example:**

    <CodeGroup>
      ```typescript TypeScript expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      const BASE_URL = "https://sdp.suki-stage.com";

      const response = await fetch(`${BASE_URL}/api/v1/auth/login`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          partner_id: "your-partner-id",
          partner_token: "your-jwt-token",
          provider_id: "provider-123", // Omit if your partnership does not require it
        }),
      });

      const data = await response.json();
      if (!response.ok) {
        throw new Error(`Login failed: ${response.status} ${JSON.stringify(data)}`);
      }
      if (!data.suki_token) throw new Error("login response missing suki_token");

      const sukiToken = data.suki_token as string;
      console.log("suki_token:", sukiToken);
      ```

      ```python Python expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      import requests

      BASE_URL = "https://sdp.suki-stage.com"

      response = requests.post(
          f"{BASE_URL}/api/v1/auth/login",
          json={
              "partner_id": "your-partner-id",
              "partner_token": "your-jwt-token",
              "provider_id": "provider-123",  # Omit if your partnership does not require it
          },
          timeout=60,
      )
      if response.status_code != 200:
          raise RuntimeError(f"Login failed: {response.status_code} {response.text}")

      suki_token = response.json()["suki_token"]
      print("suki_token:", suki_token)
      ```

      ```bash cURL expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      curl -X POST https://sdp.suki-stage.com/api/v1/auth/login \
        -H "Content-Type: application/json" \
        -d '{
          "partner_id": "your-partner-id",
          "partner_token": "your-jwt-token",
          "provider_id": "provider-123"
        }'
      ```
    </CodeGroup>

    <Tip>
      Save the `suki_token` from the response. This token is valid for **1 hour**. When it is about to expire, you can get a new one by making the same **POST** request to `/login` with a valid `partner_token`.
    </Tip>
  </Step>

  <Step title="Trigger Patient Summary Generation for an Encounter">
    Send a **POST** request to the [Trigger Patient Summary Generation for an Encounter](/patient-summary-api-reference/summary-generation/encounter-summary) with the following parameters in the request body:

    * **fhir\_encounter\_id** (required): The FHIR encounter identifier from your uploaded FHIR bundle.
    * **fhir\_practitioner\_id** (required): The FHIR practitioner identifier from your uploaded FHIR bundle.

    **Code example:**

    <CodeGroup>
      ```typescript TypeScript expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      const BASE_URL = "https://sdp.suki-stage.com";
      const sukiToken = "<sdp_suki_token from login>";
      const sdpProviderId = ""; // Set only if your partnership requires it
      const FHIR_ENCOUNTER_ID = "encounter-123";
      const FHIR_PRACTITIONER_ID = "practitioner-456";

      const headers: Record<string, string> = {
        "Content-Type": "application/json",
        sdp_suki_token: sukiToken,
      };
      if (sdpProviderId) headers.sdp_provider_id = sdpProviderId;

      const generateResponse = await fetch(
        `${BASE_URL}/api/v1/patient-summary/generate/encounter`,
        {
          method: "POST",
          headers,
          body: JSON.stringify({
            fhir_encounter_id: FHIR_ENCOUNTER_ID,
            fhir_practitioner_id: FHIR_PRACTITIONER_ID,
          }),
        },
      );

      if (generateResponse.status !== 202) {
        const err = await generateResponse.json();
        throw new Error(`Generate failed: ${generateResponse.status} ${JSON.stringify(err)}`);
      }
      console.log("Generation triggered");
      ```

      ```python Python expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      import requests

      BASE_URL = "https://sdp.suki-stage.com"
      suki_token = "<sdp_suki_token from login>"
      sdp_provider_id = ""  # Set only if your partnership requires it
      FHIR_ENCOUNTER_ID = "encounter-123"
      FHIR_PRACTITIONER_ID = "practitioner-456"

      headers = {
          "sdp_suki_token": suki_token,
          "Content-Type": "application/json",
      }
      if sdp_provider_id:
          headers["sdp_provider_id"] = sdp_provider_id

      generate_response = requests.post(
          f"{BASE_URL}/api/v1/patient-summary/generate/encounter",
          headers=headers,
          json={
              "fhir_encounter_id": FHIR_ENCOUNTER_ID,
              "fhir_practitioner_id": FHIR_PRACTITIONER_ID,
          },
          timeout=60,
      )
      if generate_response.status_code != 202:
          raise RuntimeError(
              f"Generate failed: {generate_response.status_code} {generate_response.text}"
          )
      print("Generation triggered")
      ```

      ```bash cURL expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      curl --request POST \
        --url https://sdp.suki-stage.com/api/v1/patient-summary/generate/encounter \
        --header 'Content-Type: application/json' \
        --header 'sdp_suki_token: <sdp_suki_token>' \
        --header 'sdp_provider_id: <sdp_provider_id>' \
        --data '{"fhir_encounter_id":"enc-123","fhir_practitioner_id":"pract-456"}'
      ```
    </CodeGroup>
  </Step>

  <Step title="Poll Summary Generation Status">
    Poll [Summary Generation Status API for an Encounter](/patient-summary-api-reference/summary-jobs/encounter-status) until the `status` field reaches a terminal state. Terminal statuses include `COMPLETED`, `FAILED`, and others documented in the API reference.

    <Tip>
      Poll at reasonable intervals (for example every **5 seconds**). Summary generation typically completes within a few seconds to a few minutes depending on the amount of FHIR data.
    </Tip>

    <CodeGroup>
      ```typescript TypeScript expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      const BASE_URL = "https://sdp.suki-stage.com";
      const sukiToken = "<sdp_suki_token from login>";
      const sdpProviderId = ""; // Set only if your partnership requires it
      const FHIR_ENCOUNTER_ID = "encounter-123";
      const FHIR_PRACTITIONER_ID = "practitioner-456";

      const headers: Record<string, string> = { sdp_suki_token: sukiToken };
      if (sdpProviderId) headers.sdp_provider_id = sdpProviderId;

      for (;;) {
        const statusResponse = await fetch(
          `${BASE_URL}/api/v1/patient-summary/encounter/${FHIR_ENCOUNTER_ID}/practitioner/${FHIR_PRACTITIONER_ID}/status`,
          { headers },
        );
        const statusData = (await statusResponse.json()) as { status: string };
        console.log("Summary generation status:", statusData.status);

        if (statusData.status === "COMPLETED") break;
        if (statusData.status === "FAILED") {
          throw new Error(`Generation failed: ${statusData.status}`);
        }
        await new Promise((r) => setTimeout(r, 5000));
      }
      ```

      ```python Python expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      import requests
      import time

      BASE_URL = "https://sdp.suki-stage.com"
      suki_token = "<sdp_suki_token from login>"
      sdp_provider_id = ""  # Set only if your partnership requires it
      FHIR_ENCOUNTER_ID = "encounter-123"
      FHIR_PRACTITIONER_ID = "practitioner-456"

      headers = {"sdp_suki_token": suki_token}
      if sdp_provider_id:
          headers["sdp_provider_id"] = sdp_provider_id

      while True:
          status_response = requests.get(
              f"{BASE_URL}/api/v1/patient-summary/encounter/{FHIR_ENCOUNTER_ID}/practitioner/{FHIR_PRACTITIONER_ID}/status",
              headers=headers,
              timeout=60,
          )
          status_data = status_response.json()
          print("Summary generation status:", status_data["status"])

          if status_data["status"] == "COMPLETED":
              break
          if status_data["status"] == "FAILED":
              raise RuntimeError(f"Generation failed: {status_data['status']}")
          time.sleep(5)
      ```

      ```bash cURL expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      curl --request GET \
        --url https://sdp.suki-stage.com/api/v1/patient-summary/encounter/enc-123/practitioner/pract-456/status \
        --header 'sdp_suki_token: <sdp_suki_token>' \
        --header 'sdp_provider_id: <sdp_provider_id>'
      ```
    </CodeGroup>
  </Step>

  <Step title="Retrieve Generated Summaries">
    Once generation completes, retrieve the generated summaries. The Patient Summary APIs provide two retrieval endpoints.

    * **Full Patient Summary**: Call [Retrieve Patient Summary API](/patient-summary-api-reference/summaries/encounter-practitioner) to get a comprehensive, structured summary containing four specific sections: About Visit, Patient Summary, Previous Visit, and Problems.

    * **Pre-visit summary**: Call [Retrieve pre-visit summary API](/patient-summary-api-reference/summaries/encounter-pre-visit) to get the exact pre-visit section from inside the full patient summary, which was created specifically for the user interface (UI) to display a preview to the provider.

    <CodeGroup>
      ```typescript TypeScript expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      const BASE_URL = "https://sdp.suki-stage.com";
      const sukiToken = "<sdp_suki_token from login>";
      const sdpProviderId = ""; // Set only if your partnership requires it
      const FHIR_ENCOUNTER_ID = "encounter-123";
      const FHIR_PRACTITIONER_ID = "practitioner-456";

      const headers: Record<string, string> = { sdp_suki_token: sukiToken };
      if (sdpProviderId) headers.sdp_provider_id = sdpProviderId;

      // Full Patient Summary
      const summaryResponse = await fetch(
        `${BASE_URL}/api/v1/patient-summary/encounter/${FHIR_ENCOUNTER_ID}/practitioner/${FHIR_PRACTITIONER_ID}`,
        { headers },
      );
      const summary = await summaryResponse.json();
      if (!summaryResponse.ok) {
        throw new Error(`Summary failed: ${summaryResponse.status} ${JSON.stringify(summary)}`);
      }
      console.log(JSON.stringify(summary, null, 2));

      // Pre-visit summary (optional)
      const preVisitResponse = await fetch(
        `${BASE_URL}/api/v1/patient-summary/encounter/${FHIR_ENCOUNTER_ID}/practitioner/${FHIR_PRACTITIONER_ID}/pre_visit`,
        { headers },
      );
      const preVisit = await preVisitResponse.json();
      console.log(JSON.stringify(preVisit, null, 2));
      ```

      ```python Python expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      import requests

      BASE_URL = "https://sdp.suki-stage.com"
      suki_token = "<sdp_suki_token from login>"
      sdp_provider_id = ""  # Set only if your partnership requires it
      FHIR_ENCOUNTER_ID = "encounter-123"
      FHIR_PRACTITIONER_ID = "practitioner-456"

      headers = {"sdp_suki_token": suki_token}
      if sdp_provider_id:
          headers["sdp_provider_id"] = sdp_provider_id

      # Full Patient Summary
      summary_response = requests.get(
          f"{BASE_URL}/api/v1/patient-summary/encounter/{FHIR_ENCOUNTER_ID}/practitioner/{FHIR_PRACTITIONER_ID}",
          headers=headers,
          timeout=60,
      )
      if summary_response.status_code != 200:
          raise RuntimeError(
              f"Summary failed: {summary_response.status_code} {summary_response.text}"
          )
      print(summary_response.json())

      # Pre-visit summary (optional)
      pre_visit_response = requests.get(
          f"{BASE_URL}/api/v1/patient-summary/encounter/{FHIR_ENCOUNTER_ID}/practitioner/{FHIR_PRACTITIONER_ID}/pre_visit",
          headers=headers,
          timeout=60,
      )
      print(pre_visit_response.json())
      ```

      ```bash cURL expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      # Full Patient Summary
      curl "https://sdp.suki-stage.com/api/v1/patient-summary/encounter/encounter-123/practitioner/practitioner-456" \
        -H "sdp_suki_token: YOUR_SUKI_TOKEN"

      # Pre-visit summary (optional)
      curl "https://sdp.suki-stage.com/api/v1/patient-summary/encounter/encounter-123/practitioner/practitioner-456/pre_visit" \
        -H "sdp_suki_token: YOUR_SUKI_TOKEN"
      ```
    </CodeGroup>

    <Note>
      For complete technical specifications, refer to the relevant API Reference pages.
    </Note>
  </Step>
</Steps>

## Verify your integration

Before you design the full production workflow, confirm that your staging integration can complete this path:

* Upload FHIR data to CKG successfully and poll until ingestion completes.
* Authenticate successfully and use the returned `sdp_suki_token` in follow-up requests.
* Trigger summary generation for an encounter with matching FHIR identifiers from your uploaded bundle.
* Poll job status until the generation completes.
* Retrieve the full Patient Summary or pre-visit summary with structured clinical data.

After this path works end to end on staging, continue with production rollout and error handling.

## Next steps

After you complete your first Patient Summary workflow:

<Icon icon="file-lines" iconType="solid" /> [Authentication API](/patient-summary-api-reference/authentication/login) - Login, register, and authentication configuration.

<Icon icon="file-lines" iconType="solid" /> [CKG Data Ingestion](/patient-summary-api-reference/ckg-data-ingestion) - Upload FHIR bundles and track ingestion status.

<Icon icon="file-lines" iconType="solid" /> [Summary Generation API](/patient-summary-api-reference/summary-generation) - Trigger asynchronous summary generation jobs.

<Icon icon="file-lines" iconType="solid" /> [Patient Summaries API](/patient-summary-api-reference/summaries) - Retrieve full and pre-visit Patient Summaries.
