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

# Generate a Patient Summary

> Trigger encounter Patient Summary generation and store the returned patient_summary_id

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="generate-patient-summary-for-encounter"
  title="Generate a Patient Summary"
  description="Trigger Patient Summary generation for one encounter, store `patient_summary_id`, then poll status before retrieve. The finished summary is not in the generate response."
  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/summary-generation/encounter-summary",
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

  You need a summary for one upcoming visit, but generation is asynchronous and the summary is not in the create response. Calling retrieve immediately after generate returns empty or missing content.

  Patient Summary jobs run in the background. The generate endpoint returns a `patient_summary_id` you must store, then poll status until `COMPLETED` before retrieve APIs return the finished summary.

  ## Architecture

  ```mermaid actions={false} theme={"theme":{"light":"one-light","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[CKG COMPLETED] --> B[POST generate]
    B --> C[Store patient_summary_id]
    C --> D[Poll status]
    D --> E[Retrieve summary]
  ```

  ## Prerequisites

  <Check>CKG ingestion for the patient, encounter, and practitioner is `COMPLETED`. See [Ingest FHIR into CKG](/documentation/cookbooks/upload-fhir-to-ckg-and-poll).</Check>

  <Check>Completed [Partner onboarding](/documentation/get-started/partner-onboarding) with `sdp_suki_token` and `sdp_provider_id` headers ready.</Check>

  <Check>`fhir_encounter_id` and `fhir_practitioner_id` values that match the ingested FHIR bundle.</Check>

  ## Solution

  <Columns cols={3}>
    <Card title="Before">
      CKG ingestion for this encounter is `COMPLETED`.
    </Card>

    <Card title="This Recipe">
      POST generate, accept `201` or `202`, and store `patient_summary_id` when returned.
    </Card>

    <Card title="Next">
      [Wait for Patient Summary before Fetch](/documentation/cookbooks/poll-patient-summary-status-before-retrieve)
    </Card>
  </Columns>

  Call [Generate Patient Summary for an encounter](/patient-summary-api-reference/summary-generation/encounter-summary) with `fhir_encounter_id` and `fhir_practitioner_id`. Store `patient_summary_id` when returned. Poll status next, then retrieve.

  <Tabs>
    <Tab title="CURL">
      ```bash theme={"theme":{"light":"one-light","dark":"material-theme-darker"}}
      curl --request POST \
        --url "${SUKI_BASE_URL:-https://sdp.suki.ai}/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"
        }'
      ```
    </Tab>

    <Tab title="TypeScript">
      ```typescript theme={"theme":{"light":"one-light","dark":"material-theme-darker"}}
      // generate-patient-summary-for-encounter.ts

      const BASE_URL = process.env.SUKI_BASE_URL ?? "https://sdp.suki.ai";
      const SDP_SUKI_TOKEN = process.env.SDP_SUKI_TOKEN!;
      const SDP_PROVIDER_ID = process.env.SDP_PROVIDER_ID ?? "";
      const FHIR_ENCOUNTER_ID = process.env.FHIR_ENCOUNTER_ID ?? "enc-123";
      const FHIR_PRACTITIONER_ID = process.env.FHIR_PRACTITIONER_ID ?? "pract-456";

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

      async function generatePatientSummaryForEncounter(
        fhirEncounterId: string,
        fhirPractitionerId: string
      ): Promise<string | undefined> {
        const res = await fetch(`${BASE_URL}/api/v1/patient-summary/generate/encounter`, {
          method: "POST",
          headers: restHeaders(),
          body: JSON.stringify({
            fhir_encounter_id: fhirEncounterId,
            fhir_practitioner_id: fhirPractitionerId,
          }),
        });

        // OpenAPI documents 201 with patient_summary_id; quickstart also accepts 202.
        if (res.status !== 201 && res.status !== 202) {
          throw new Error(`Generate for encounter failed: ${res.status}`);
        }

        const data = (await res.json().catch(() => ({}))) as {
          patient_summary_id?: string;
        };
        // Store when present. You can also poll/retrieve by FHIR encounter + practitioner ids.
        return data.patient_summary_id;
      }

      const patientSummaryId = await generatePatientSummaryForEncounter(
        FHIR_ENCOUNTER_ID,
        FHIR_PRACTITIONER_ID
      );
      console.log("patient_summary_id:", patientSummaryId ?? "(poll by FHIR ids)");
      // Next: poll status before retrieve
      ```
    </Tab>

    <Tab title="Python">
      ```python theme={"theme":{"light":"one-light","dark":"material-theme-darker"}}
      # generate_patient_summary_for_encounter.py
      # pip install requests

      import os
      from typing import Optional

      import requests

      BASE_URL = os.environ.get("SUKI_BASE_URL", "https://sdp.suki.ai")
      SDP_SUKI_TOKEN = os.environ["SDP_SUKI_TOKEN"]
      SDP_PROVIDER_ID = os.environ.get("SDP_PROVIDER_ID", "")
      FHIR_ENCOUNTER_ID = os.environ.get("FHIR_ENCOUNTER_ID", "enc-123")
      FHIR_PRACTITIONER_ID = os.environ.get("FHIR_PRACTITIONER_ID", "pract-456")


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


      def generate_patient_summary_for_encounter(
          fhir_encounter_id: str,
          fhir_practitioner_id: str,
      ) -> Optional[str]:
          res = requests.post(
              f"{BASE_URL}/api/v1/patient-summary/generate/encounter",
              headers=rest_headers(),
              json={
                  "fhir_encounter_id": fhir_encounter_id,
                  "fhir_practitioner_id": fhir_practitioner_id,
              },
              timeout=30,
          )
          # OpenAPI documents 201 with patient_summary_id; quickstart also accepts 202.
          if res.status_code not in (201, 202):
              res.raise_for_status()
          data = res.json() if res.content else {}
          return data.get("patient_summary_id")


      patient_summary_id = generate_patient_summary_for_encounter(
          FHIR_ENCOUNTER_ID,
          FHIR_PRACTITIONER_ID,
      )
      print("patient_summary_id:", patient_summary_id or "(poll by FHIR ids)")
      # Next: poll status before retrieve
      ```
    </Tab>
  </Tabs>

  <Steps>
    <Step title="Confirm CKG ingestion is COMPLETED">
      Do not generate until [Ingest FHIR into CKG](/documentation/cookbooks/upload-fhir-to-ckg-and-poll) returns `COMPLETED` for the same patient and encounter.
    </Step>

    <Step title="Call generate for the encounter">
      POST to [Generate Patient Summary for an encounter](/patient-summary-api-reference/summary-generation/encounter-summary) with matching `fhir_encounter_id` and `fhir_practitioner_id`.
    </Step>

    <Step title="Store patient_summary_id">
      Persist the returned ID. The generate response does not contain the finished summary text.
    </Step>

    <Step title="Poll before retrieve">
      Poll status until `COMPLETED`, then call retrieve APIs. Use [Wait for Patient Summary before fetch](/documentation/cookbooks/poll-patient-summary-status-before-retrieve) next.
    </Step>
  </Steps>

  ## Try it

  <Card>
    * **Generate returns patient\_summary\_id**

      A successful POST returns a job ID, not the summary body. Store it for status polling and retrieve calls.

    * **Retrieve waits for COMPLETED**

      Calling retrieve while status is `READY` or `RUNNING` returns empty or partial data. Poll first.

    * **Existing summaries avoid duplicate jobs**

      Check whether a usable summary already exists for the appointment before re-generating.
  </Card>

  ## Common mistakes

  <Warning>
    * Generating before CKG ingestion is `COMPLETED`.
    * Mismatched `fhir_encounter_id` or `fhir_practitioner_id` versus ingested FHIR data.
    * Treating the generate response as the finished summary instead of saving `patient_summary_id` and polling.
    * Re-generating when a usable summary for the appointment already exists.
  </Warning>

  ## Related cookbooks

  <Columns cols={2}>
    <Card title="Ingest FHIR into CKG" href="/documentation/cookbooks/upload-fhir-to-ckg-and-poll" arrow={true} icon="book-open">
      Upload FHIR, then wait for COMPLETED.
    </Card>

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.