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

# Ambient API Quickstart

> Authenticate, create an Ambient session, stream audio, end the session, and retrieve the clinical note on staging

This quickstart walks you through one successful Ambient API session on staging: authenticate, create a session, stream encounter audio, end the session, and retrieve the clinical note. Rely on a webhook when processing finishes to handle notifications.

Your application owns session controls, audio streaming, status handling, note review, and EHR handoff. For product context and when to choose Ambient APIs versus ambient SDKs, refer to [Ambient clinical documentation](/documentation/concepts/ambient-clinical-notes/ambient-documentation).

**What you will do**

1. **Authenticate** to get an `sdp_suki_token` (and **register** the user if needed).
2. **Create** an ambient session and optionally **seed context** for better notes.
3. **Stream audio** over the WebSocket, send control events, and **end** the session when the visit is done.
4. **Retrieve** the clinical note and transcript, or rely on a **webhook** when processing finishes.

<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 following prompt to add the Suki developer documentation as a skill and [MCP server](/documentation/references/mcp) to your tool for better AI-assisted coding during the integration process. For all AI options (contextual menu, `llms.txt`, and `skill.md`), refer to [AI-optimized documentation](/documentation/references/ai-optimized-documentation).

  <Prompt description="Install the Suki developer docs as a skill to get context on Suki's developer tools, APIs, and SDKs. Add the [MCP server](/documentation/references/mcp) for documentation search." icon="gear" iconType="regular" actions={["copy", "cursor"]}>
    Install the Suki developer docs as skill to get context on Suki's developer tools, APIs, and SDKs.

    npx skills add [https://developer.suki.ai](https://developer.suki.ai)

    Then add the Suki developer docs MCP server for access to documentation search. Follow the MCP instructions at [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 Ambient 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 Ambient APIs, you must have the following:

* An OAuth-compliant authentication system
* JWT tokens with consistent user identifiers
* 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

Refer to the [Partner onboarding](/documentation/get-started/partner-onboarding) and [Partner authentication](/documentation/how-to/partner-authentication) guides for more details.

### Environments to use for development and testing

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

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

  * The production environment is **`https://sdp.suki.ai`** and **`wss://sdp.suki.ai`**.
  * The staging environment is **`https://sdp.suki-stage.com`** and **`wss://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 16 kHz mono LINEAR16 WAV (or raw PCM) at `audio.wav`, then run. For detailed explanations of each step, refer to the numbered steps below.

* **Python:** `pip install requests websocket-client` then `python ambient_staging.py`
* **TypeScript (Node):** `npm install ws` then `npx tsx ambient_staging.ts` (Node 18+)

<Warning>
  Ambient sessions must be **at least 1 minute** of audio for note generation. Shorter sessions can return status `skipped` with no note. Use a WAV or raw LINEAR16 PCM file that is long enough when you test.
</Warning>

<CodeGroup>
  ```python Python expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  # ambient_staging.py
  # Flow: login (register once if needed) → create → context → stream → end → poll content
  # pip install requests websocket-client

  import base64
  import json
  import time
  from datetime import datetime, timezone
  from typing import Any, Optional

  import requests
  import websocket

  BASE_URL = "https://sdp.suki-stage.com"
  WS_URL = "wss://sdp.suki-stage.com/ws/stream"
  CHUNK_BYTES = 3200  # ~100 ms of 16 kHz mono 16-bit PCM
  WAV_HEADER_BYTES = 44

  # Replace these with your partner credentials
  PARTNER_ID = "your-partner-id"
  PARTNER_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."  # Keep on your backend only
  PROVIDER_ID = "provider-123"  # Optional; required for Bearer / Single Auth Token partners
  SDP_PROVIDER_ID = ""  # Leave empty unless your partnership requires sdp_provider_id
  AUDIO_PATH = "audio.wav"  # 16 kHz mono LINEAR16 WAV or raw PCM


  def b64(data: bytes) -> str:
      return base64.b64encode(data).decode("ascii")


  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 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 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)
      # New link: 201. Already linked: 409. Both are success for this flow.
      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 create_ambient_session(suki_token: str, body: Optional[dict[str, str]] = None) -> dict[str, str]:
      url = f"{BASE_URL}/api/v1/ambient/session/create"
      r = requests.post(url, headers=rest_headers(suki_token), json=body or {}, timeout=60)
      expect_status(r, url, 201)
      data = r.json()
      sid = data.get("ambient_session_id")
      composition_id = data.get("composition_id")
      if not sid:
          raise RuntimeError("create response missing ambient_session_id")
      return {
          "ambient_session_id": sid,
          "composition_id": composition_id or "",
      }


  def seed_context(suki_token: str, ambient_session_id: str) -> None:
      url = f"{BASE_URL}/api/v1/ambient/session/{ambient_session_id}/context"
      payload = {
          "provider": {"specialty": "CARDIOLOGY", "provider_role": "ATTENDING"},
          "patient": {"dob": "2000-01-01", "sex": "male"},
          "visit": {
              "chief_complaint": "Headache",
              "encounter_type": "AMBULATORY",
              "reason_for_visit": "Follow-up for migraines",
              "visit_type": "ESTABLISHED_PATIENT",
          },
          "sections": [{"loinc": "10164-2"}, {"loinc": "48765-2"}],
          "diagnoses": {
              "values": [
                  {
                      "codes": [
                          {
                              "code": "I10",
                              "description": "Essential hypertension",
                              "type": "ICD10",
                          }
                      ],
                      "diagnosis_note": "Hypertension",
                  }
              ]
          },
          "emr": {"target_emr": "EPIC"},
      }
      r = requests.post(url, headers=rest_headers(suki_token), json=payload, timeout=60)
      expect_status(r, url, 200)


  def strip_wav_header(raw: bytes) -> bytes:
      if len(raw) >= 12 and raw[:4] == b"RIFF" and raw[8:12] == b"WAVE":
          return raw[WAV_HEADER_BYTES:]
      return raw


  def stream_pcm_file(suki_token: str, ambient_session_id: str, path: str) -> None:
      header = [
          f"sdp_suki_token: {suki_token}",
          f"ambient_session_id: {ambient_session_id}",
      ]
      if SDP_PROVIDER_ID:
          header.append(f"sdp_provider_id: {SDP_PROVIDER_ID}")

      ws = websocket.create_connection(WS_URL, header=header, timeout=60)
      try:
          rfc3339 = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
          ws.send(json.dumps({"type": "START_TIME", "data": b64(rfc3339.encode("utf-8"))}))

          with open(path, "rb") as f:
              pcm = strip_wav_header(f.read())

          for i in range(0, len(pcm), CHUNK_BYTES):
              ws.send(json.dumps({"type": "AUDIO", "data": b64(pcm[i : i + CHUNK_BYTES])}))

          # End marker: Base64 of ASCII bytes EOF (literal RU9G)
          ws.send(json.dumps({"type": "AUDIO", "data": "RU9G"}))
      finally:
          ws.close()


  def end_ambient_session(suki_token: str, ambient_session_id: str) -> None:
      url = f"{BASE_URL}/api/v1/ambient/session/{ambient_session_id}/end"
      r = requests.post(url, headers=rest_headers(suki_token), timeout=60)
      expect_status(r, url, 200)


  def get_status(suki_token: str, ambient_session_id: str) -> str:
      url = f"{BASE_URL}/api/v1/ambient/session/{ambient_session_id}/status"
      r = requests.get(url, headers=rest_headers(suki_token), timeout=60)
      expect_status(r, url, 200)
      return r.json()["status"]


  def get_content(suki_token: str, ambient_session_id: str) -> Any:
      url = f"{BASE_URL}/api/v1/ambient/session/{ambient_session_id}/content"
      r = requests.get(
          url,
          headers=rest_headers(suki_token),
          params={"cumulative": "false"},
          timeout=60,
      )
      expect_status(r, url, 200)
      return r.json()


  def wait_for_content(suki_token: str, ambient_session_id: str, poll_sec: float = 2.0) -> dict[str, Any]:
      while True:
          status = get_status(suki_token, ambient_session_id)
          print("status:", status)
          if status == "completed":
              return {"status": status, "content": get_content(suki_token, ambient_session_id)}
          if status in ("failed", "skipped", "aborted"):
              return {"status": status, "content": None}
          time.sleep(poll_sec)


  if __name__ == "__main__":
      suki_token = login_with_register_fallback(
          PARTNER_ID,
          PARTNER_TOKEN,
          PROVIDER_ID,
          provider_name="Dr. John Smith",
          provider_org_id="org-123",
      )
      print("Authenticated")

      created = create_ambient_session(suki_token, {})
      ambient_session_id = created["ambient_session_id"]
      print("ambient_session_id:", ambient_session_id)
      print("composition_id:", created["composition_id"])

      seed_context(suki_token, ambient_session_id)
      print("Context seeded")

      stream_pcm_file(suki_token, ambient_session_id, AUDIO_PATH)
      print("Stream finished")

      end_ambient_session(suki_token, ambient_session_id)
      print("Session ended")

      result = wait_for_content(suki_token, ambient_session_id)
      print(json.dumps(result, indent=2))
  ```

  ```typescript TypeScript expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  // ambient_staging.ts
  // Flow: login (register once if needed) → create → context → stream → end → poll content
  // Node 18+: npm install ws && npx tsx ambient_staging.ts

  import fs from "node:fs";
  import WebSocket from "ws";

  const BASE_URL = "https://sdp.suki-stage.com";
  const WS_URL = "wss://sdp.suki-stage.com/ws/stream";
  const CHUNK_BYTES = 3200; // ~100 ms of 16 kHz mono 16-bit PCM
  const WAV_HEADER_BYTES = 44;

  // Replace these with your partner credentials
  const PARTNER_ID = "your-partner-id";
  const PARTNER_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."; // Keep on your backend only
  const PROVIDER_ID = "provider-123"; // Optional; required for Bearer / Single Auth Token partners
  const SDP_PROVIDER_ID = ""; // Leave empty unless your partnership requires sdp_provider_id
  const AUDIO_PATH = "audio.wav"; // 16 kHz mono LINEAR16 WAV or raw PCM

  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 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),
    });
    // New link: 201. Already linked: 409. Both are success for this flow.
    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 createAmbientSession(
    sukiToken: string,
  ): Promise<{ ambientSessionId: string; compositionId: string }> {
    const url = `${BASE_URL}/api/v1/ambient/session/create`;
    const response = await fetch(url, {
      method: "POST",
      headers: restHeaders(sukiToken),
      body: JSON.stringify({}),
    });
    await expectStatus(response, url, 201);
    const data = (await response.json()) as {
      ambient_session_id?: string;
      composition_id?: string;
    };
    if (!data.ambient_session_id) {
      throw new Error("create response missing ambient_session_id");
    }
    return {
      ambientSessionId: data.ambient_session_id,
      compositionId: data.composition_id ?? "",
    };
  }

  async function seedContext(sukiToken: string, ambientSessionId: string): Promise<void> {
    const url = `${BASE_URL}/api/v1/ambient/session/${ambientSessionId}/context`;
    const response = await fetch(url, {
      method: "POST",
      headers: restHeaders(sukiToken),
      body: JSON.stringify({
        provider: { specialty: "CARDIOLOGY", provider_role: "ATTENDING" },
        patient: { dob: "2000-01-01", sex: "male" },
        visit: {
          chief_complaint: "Headache",
          encounter_type: "AMBULATORY",
          reason_for_visit: "Follow-up for migraines",
          visit_type: "ESTABLISHED_PATIENT",
        },
        sections: [{ loinc: "10164-2" }, { loinc: "48765-2" }],
        diagnoses: {
          values: [
            {
              codes: [
                {
                  code: "I10",
                  description: "Essential hypertension",
                  type: "ICD10",
                },
              ],
              diagnosis_note: "Hypertension",
            },
          ],
        },
        emr: { target_emr: "EPIC" },
      }),
    });
    await expectStatus(response, url, 200);
  }

  function stripWavHeader(buf: Buffer): Buffer {
    if (
      buf.length >= 12 &&
      buf.subarray(0, 4).toString("ascii") === "RIFF" &&
      buf.subarray(8, 12).toString("ascii") === "WAVE"
    ) {
      return buf.subarray(WAV_HEADER_BYTES);
    }
    return buf;
  }

  function streamPcmFile(
    sukiToken: string,
    ambientSessionId: string,
    path: string,
  ): Promise<void> {
    return new Promise((resolve, reject) => {
      const headers: Record<string, string> = {
        sdp_suki_token: sukiToken,
        ambient_session_id: ambientSessionId,
      };
      if (SDP_PROVIDER_ID) headers.sdp_provider_id = SDP_PROVIDER_ID;

      const ws = new WebSocket(WS_URL, { headers });

      ws.on("open", () => {
        try {
          const rfc3339 = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
          ws.send(
            JSON.stringify({
              type: "START_TIME",
              data: Buffer.from(rfc3339, "utf8").toString("base64"),
            }),
          );

          const pcm = stripWavHeader(fs.readFileSync(path));
          for (let i = 0; i < pcm.length; i += CHUNK_BYTES) {
            ws.send(
              JSON.stringify({
                type: "AUDIO",
                data: pcm.subarray(i, i + CHUNK_BYTES).toString("base64"),
              }),
            );
          }
          // End marker: Base64 of ASCII bytes EOF
          ws.send(JSON.stringify({ type: "AUDIO", data: "RU9G" }));
          ws.close();
        } catch (err) {
          reject(err);
        }
      });

      ws.on("message", (data) => console.log("ws message:", data.toString()));
      ws.on("error", reject);
      ws.on("close", () => resolve());
    });
  }

  async function endAmbientSession(sukiToken: string, ambientSessionId: string): Promise<void> {
    const url = `${BASE_URL}/api/v1/ambient/session/${ambientSessionId}/end`;
    const response = await fetch(url, {
      method: "POST",
      headers: restHeaders(sukiToken),
    });
    await expectStatus(response, url, 200);
  }

  async function getStatus(sukiToken: string, ambientSessionId: string): Promise<string> {
    const url = `${BASE_URL}/api/v1/ambient/session/${ambientSessionId}/status`;
    const response = await fetch(url, { headers: restHeaders(sukiToken) });
    await expectStatus(response, url, 200);
    const data = (await response.json()) as { status: string };
    return data.status;
  }

  async function getContent(sukiToken: string, ambientSessionId: string): Promise<unknown> {
    const url = `${BASE_URL}/api/v1/ambient/session/${ambientSessionId}/content?cumulative=false`;
    const response = await fetch(url, { headers: restHeaders(sukiToken) });
    await expectStatus(response, url, 200);
    return response.json();
  }

  async function waitForContent(
    sukiToken: string,
    ambientSessionId: string,
    pollMs = 2000,
  ): Promise<{ status: string; content: unknown }> {
    for (;;) {
      const status = await getStatus(sukiToken, ambientSessionId);
      console.log("status:", status);
      if (status === "completed") {
        return { status, content: await getContent(sukiToken, ambientSessionId) };
      }
      if (status === "failed" || status === "skipped" || status === "aborted") {
        return { status, content: null };
      }
      await new Promise((r) => setTimeout(r, pollMs));
    }
  }

  async function main() {
    const sukiToken = await loginWithRegisterFallback(
      PARTNER_ID,
      PARTNER_TOKEN,
      PROVIDER_ID,
      "Dr. John Smith",
      "org-123",
    );
    console.log("Authenticated");

    const created = await createAmbientSession(sukiToken);
    console.log("ambient_session_id:", created.ambientSessionId);
    console.log("composition_id:", created.compositionId);

    await seedContext(sukiToken, created.ambientSessionId);
    console.log("Context seeded");

    await streamPcmFile(sukiToken, created.ambientSessionId, AUDIO_PATH);
    console.log("Stream finished");

    await endAmbientSession(sukiToken, created.ambientSessionId);
    console.log("Session ended");

    const result = await waitForContent(sukiToken, created.ambientSessionId);
    console.log(JSON.stringify(result, null, 2));
  }

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

## Step 1: Authenticate and get token

Send a **POST** request to the [/api/v1/auth/login](/api-reference/authentication/login) endpoint with the following parameters in the request body:

1. **partner\_id**: 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.
2. **partner\_token**: 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.
3. **provider\_id** (**Optional** for standard partners; **Required** for Bearer partners and for Single Auth Token authentication): 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>.

Suki verifies the `partner_token` using your publicly exposed **JWKS endpoint** (or your Okta authorization server URL if you use Okta). On a successful request, the API returns a <Tooltip tip="The access token returned by Suki's authentication API, used to authorize subsequent API requests to the Suki platform." cta="View in Glossary" href="/Glossary/s">Suki Token</Tooltip> (`suki_token`) that you must include as the `sdp_suki_token` header for all subsequent API calls.

For Register versus Login, Single Auth Token authentication, and Bearer partner rules, refer to [Provider authentication](/api-reference/provider-authentication), [Single Auth Token authentication](/api-reference/single-auth-token-authentication), and [Bearer partner authentication](/api-reference/bearer-partner-authentication).

<Note>
  **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 [/api/v1/auth/register](/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](/api-reference/authentication/register) for the full specification.
</Note>

Each sample below is self-contained (TypeScript, Python, and cURL). Expect HTTP **200**.

<CodeGroup>
  ```typescript TypeScript 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 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 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 2: Create Ambient session

Send a **POST** request to the [/api/v1/ambient/session/create](/api-reference/ambient-sessions/create) endpoint with the following parameters in the request body:

<Note>
  Pass `emr_encounter_id` (UUID) when you need cross-modality ambient interoperability. The create response includes `composition_id`, which you use as `note_id` for note-level APIs. See [Ambient interoperability](/documentation/concepts/ambient-clinical-notes/ambient-interoperability).
</Note>

1. **emr\_encounter\_id** (**Optional** for create, **required** for interoperability): UUID for the partner EMR encounter that anchors the note across modalities.
2. **encounter\_id** (Optional): Session group ID used to group ambient sessions and re-ambient. Any alphanumeric string up to **255** characters.
3. **ambient\_session\_id** (Optional): A UUID v4 to identify the session. If you do not provide one, Suki will generate it and include it in the response.

All three body fields are optional for a first staging session. An empty JSON body is enough. Expect HTTP **201**.

<CodeGroup>
  ```typescript TypeScript 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 headers: Record<string, string> = {
    "Content-Type": "application/json",
    sdp_suki_token: sukiToken,
  };
  if (sdpProviderId) headers.sdp_provider_id = sdpProviderId;

  const createResponse = await fetch(`${BASE_URL}/api/v1/ambient/session/create`, {
    method: "POST",
    headers,
    body: JSON.stringify({}),
  });

  const created = await createResponse.json();
  if (createResponse.status !== 201) {
    throw new Error(`Create failed: ${createResponse.status} ${JSON.stringify(created)}`);
  }

  const ambientSessionId = created.ambient_session_id as string;
  const compositionId = created.composition_id as string;
  console.log({ ambientSessionId, compositionId });
  ```

  ```python Python 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

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

  create_response = requests.post(
      f"{BASE_URL}/api/v1/ambient/session/create",
      headers=headers,
      json={},
      timeout=60,
  )
  if create_response.status_code != 201:
      raise RuntimeError(
          f"Create failed: {create_response.status_code} {create_response.text}"
      )

  created = create_response.json()
  ambient_session_id = created["ambient_session_id"]
  composition_id = created["composition_id"]
  print({"ambient_session_id": ambient_session_id, "composition_id": composition_id})
  ```

  ```bash cURL theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  curl -X POST https://sdp.suki-stage.com/api/v1/ambient/session/create \
    -H "sdp_suki_token: YOUR_SUKI_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{}'
  ```
</CodeGroup>

<Tip>
  Save the `ambient_session_id` and `composition_id` from the response. You need the ambient session ID to stream audio and retrieve session content, and the composition ID as `note_id` for note-level APIs.
</Tip>

## Step 3: Seed context

After creating the session, send a **POST** request to the [/api/v1/ambient/session/{ambient_session_id}/context](/api-reference/ambient-sessions/context) endpoint to provide additional metadata.

<Note>
  Providing **context** significantly improves the accuracy of the generated clinical note.
</Note>

Include fields such as:

* **provider** (Optional): Specialty and role (for example, `CARDIOLOGY` and `ATTENDING`).
* **patient** (Optional): Date of birth (`dob`) and `sex` (`male`, `female`, `other`, or `unknown`).
* **visit** (Optional): Chief complaint, encounter type, reason for visit, and visit type.
* **sections** (Optional): LOINC-coded clinical sections to generate. If you omit this, Suki uses a default set.
* **diagnoses** and **emr** (Optional): Diagnosis codes and target EMR when your workflow needs them.

Refer to the [Context API reference](/api-reference/ambient-sessions/context) for the full request structure. Expect HTTP **200**.

<CodeGroup>
  ```typescript TypeScript 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 ambientSessionId = "<ambient_session_id from create>";
  const sdpProviderId = ""; // Set only if your partnership requires it

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

  const contextResponse = await fetch(
    `${BASE_URL}/api/v1/ambient/session/${ambientSessionId}/context`,
    {
      method: "POST",
      headers,
      body: JSON.stringify({
        provider: { specialty: "CARDIOLOGY", provider_role: "ATTENDING" },
        patient: { dob: "2000-01-01", sex: "male" },
        visit: {
          chief_complaint: "Headache",
          encounter_type: "AMBULATORY",
          reason_for_visit: "Follow-up for migraines",
          visit_type: "ESTABLISHED_PATIENT",
        },
        sections: [{ loinc: "10164-2" }, { loinc: "48765-2" }],
        diagnoses: {
          values: [
            {
              codes: [
                {
                  code: "I10",
                  description: "Essential hypertension",
                  type: "ICD10",
                },
              ],
              diagnosis_note: "Hypertension",
            },
          ],
        },
        emr: { target_emr: "EPIC" },
      }),
    },
  );

  if (!contextResponse.ok) {
    const err = await contextResponse.json();
    throw new Error(`Context failed: ${contextResponse.status} ${JSON.stringify(err)}`);
  }
  console.log("Context seeded");
  ```

  ```python Python 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>"
  ambient_session_id = "<ambient_session_id from create>"
  sdp_provider_id = ""  # Set only if your partnership requires it

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

  context_response = requests.post(
      f"{BASE_URL}/api/v1/ambient/session/{ambient_session_id}/context",
      headers=headers,
      json={
          "provider": {"specialty": "CARDIOLOGY", "provider_role": "ATTENDING"},
          "patient": {"dob": "2000-01-01", "sex": "male"},
          "visit": {
              "chief_complaint": "Headache",
              "encounter_type": "AMBULATORY",
              "reason_for_visit": "Follow-up for migraines",
              "visit_type": "ESTABLISHED_PATIENT",
          },
          "sections": [{"loinc": "10164-2"}, {"loinc": "48765-2"}],
          "diagnoses": {
              "values": [
                  {
                      "codes": [
                          {
                              "code": "I10",
                              "description": "Essential hypertension",
                              "type": "ICD10",
                          }
                      ],
                      "diagnosis_note": "Hypertension",
                  }
              ]
          },
          "emr": {"target_emr": "EPIC"},
      },
      timeout=60,
  )
  if context_response.status_code != 200:
      raise RuntimeError(
          f"Context failed: {context_response.status_code} {context_response.text}"
      )
  print("Context seeded")
  ```

  ```bash cURL theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  curl -X POST "https://sdp.suki-stage.com/api/v1/ambient/session/YOUR_SESSION_ID/context" \
    -H "sdp_suki_token: YOUR_SUKI_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "provider": {"specialty": "CARDIOLOGY", "provider_role": "ATTENDING"},
      "patient": {"dob": "2000-01-01", "sex": "male"},
      "visit": {
        "chief_complaint": "Headache",
        "encounter_type": "AMBULATORY",
        "reason_for_visit": "Follow-up for migraines",
        "visit_type": "ESTABLISHED_PATIENT"
      },
      "sections": [{"loinc": "10164-2"}, {"loinc": "48765-2"}],
      "diagnoses": {
        "values": [{
          "codes": [{"code": "I10", "description": "Essential hypertension", "type": "ICD10"}],
          "diagnosis_note": "Hypertension"
        }]
      },
      "emr": {"target_emr": "EPIC"}
    }'
  ```
</CodeGroup>

## Step 4: Stream audio via WebSocket

After you **create** the ambient session and **seed context** (previous steps), open a **WebSocket** to `wss://sdp.suki-stage.com/ws/stream`. Upgrading before the session exists or before context is ready often fails.

For authentication, use the `Sec-WebSocket-Protocol` header (browser clients) with the comma-separated value `SukiAmbientAuth,<sdp_suki_token>,<ambient_session_id>`, or send `sdp_suki_token` and `ambient_session_id` as separate HTTP headers on the upgrade (non-browser clients). See the [Audio streaming API reference](/api-reference/ambient-sessions/audio-stream), [Stream ambient audio over WebSocket](/documentation/how-to/audio-streaming/ambient-audio-streaming), and [WebSocket streaming wire format](/documentation/how-to/audio-streaming/websocket-streaming-wire-format) for the full wire format and code samples.

**Audio format**: You must stream **raw PCM** in each `AUDIO` message (after Base64 encoding) with:

* **encoding**: LINEAR16
* **sample\_rate**: 16KHz
* **channel**: Mono

<Note>
  `.wav` files are containers; strip the usual **44-byte** WAV header (or decode to PCM) before chunking. Sending RIFF headers as PCM hurts recognition.
</Note>

<Tip>
  For optimal performance, we recommend chunking the audio into **100ms packets**.
</Tip>

**Outbound WebSocket messages** must each be **one JSON text frame** (one object per send). Use proto JSON field names: **`type`** and **`data`** for `START_TIME` and `AUDIO`. The **`data`** field is always **standard Base64** (RFC 4648) of **raw bytes**. Do not send PCM as binary WebSocket frames on this endpoint.

**Required send order** for a stream segment:

1. **`START_TIME`**: `data` is Base64 of the UTF-8 RFC 3339 timestamp string.
2. **`AUDIO`**: one or more messages with `data` set to Base64 of each PCM chunk.
3. **End marker**: an **`AUDIO`** message whose `data` is Base64 of the three bytes **`EOF`** (ASCII), value **`RU9G`**. Do not use `{"type":"end_of_stream"}` for this protocol.

**`EVENT` messages** use **`{"type":"EVENT","event":"<ENUM>"}`** (not `data` for the action). Supported values include:

* **PAUSE**: Pauses the stream.
* **RESUME**: Resumes a paused audio stream.
* **CANCEL**: Immediately mark an ambient session as CANCELLED in our system, it closes the WebSocket. The session cannot be resumed again; create a new session if you want to record again.
* <Badge color="red" size="sm">Deprecated</Badge> **ABORT**: Aborts the stream due to an interruption. A note will be generated from the audio received so far. The session remains active and can be resumed.
* **KEEP\_ALIVE**: Pings the server to keep the connection alive during periods of inactivity (for example, when paused).

When you are done sending audio, **close the WebSocket**, then call the **end session** endpoint (next step) and **poll status and REST content endpoints** for the final note and transcript. Final results are not guaranteed from the WebSocket alone.

Use the [complete staging script](#complete-staging-script) for a full streaming client, or the [Ambient WebSocket code example](/documentation/tutorials/ambient-websocket-code-example) for control events and KEEP\_ALIVE handling.

<Tip>
  **Keep the connection alive**: You must send a **KEEP\_ALIVE** event at least once every **five seconds** when the stream is paused to prevent the connection from closing.
</Tip>

<Note>
  **Stream behavior and timeouts**: 1. **Session Timeouts**: If no audio data is sent for **25 seconds**, Suki will disconnect the stream. 2. **Paused Stream**: If the stream is paused and receiving KEEP\_ALIVE events, Suki will disconnect the stream after **30 minutes** of inactivity.
</Note>

<Note>
  **CANCEL vs. ABORT**: Sending **CANCEL** terminates the session completely. No note will be generated. Sending **ABORT** ends the current stream, but the session remains **active**. Suki will generate a note from the audio received before the interruption. Resume streaming to the same `ambient_session_id` later.
</Note>

## Step 5: End session

To complete the session and begin the note generation process, send a **POST** request to the [/api/v1/ambient/session/{ambient_session_id}/end](/api-reference/ambient-sessions/end) endpoint. This signals that you will not send more audio for this session. Expect HTTP **200**.

<CodeGroup>
  ```typescript TypeScript 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 ambientSessionId = "<ambient_session_id from create>";
  const sdpProviderId = ""; // Set only if your partnership requires it

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

  const endResponse = await fetch(
    `${BASE_URL}/api/v1/ambient/session/${ambientSessionId}/end`,
    { method: "POST", headers },
  );

  if (!endResponse.ok) {
    const err = await endResponse.text();
    throw new Error(`End failed: ${endResponse.status} ${err}`);
  }
  console.log("Session ended");
  ```

  ```python Python 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>"
  ambient_session_id = "<ambient_session_id from create>"
  sdp_provider_id = ""  # Set only if your partnership requires it

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

  end_response = requests.post(
      f"{BASE_URL}/api/v1/ambient/session/{ambient_session_id}/end",
      headers=headers,
      timeout=60,
  )
  if end_response.status_code != 200:
      raise RuntimeError(f"End failed: {end_response.status_code} {end_response.text}")
  print("Session ended")
  ```

  ```bash cURL theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  curl -X POST "https://sdp.suki-stage.com/api/v1/ambient/session/YOUR_SESSION_ID/end" \
    -H "sdp_suki_token: YOUR_SUKI_TOKEN"
  ```
</CodeGroup>

## Step 6: Retrieve generated content

Once the note is ready, Suki notifies your application. The recommended way to know when a note is ready is to use a **webhook**. Suki will send a `session_summary_generated` event to a webhook endpoint that you provide. Details on configuring your webhook and its authentication will be provided during the onboarding process.

**Retrieving content manually**: Alternatively, you can use the following endpoints to check the status and retrieve the session's content:

1. [GET /api/v1/ambient/session/{ambient_session_id}/status](/api-reference/ambient-content/status): Check the processing status of a session.
2. [GET /api/v1/ambient/session/{ambient_session_id}/content](/api-reference/ambient-content/content): Retrieve the final, structured clinical note.
3. [GET /api/v1/ambient/session/{ambient_session_id}/transcript](/api-reference/ambient-content/transcript): Get the full conversation transcript, including **timestamps** for each part of the dialogue.

<CodeGroup>
  ```typescript TypeScript 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 ambientSessionId = "<ambient_session_id from create>";
  const sdpProviderId = ""; // Set only if your partnership requires it

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

  const contentResponse = await fetch(
    `${BASE_URL}/api/v1/ambient/session/${ambientSessionId}/content?cumulative=false`,
    { headers },
  );
  const content = await contentResponse.json();
  if (!contentResponse.ok) {
    throw new Error(`Content failed: ${contentResponse.status} ${JSON.stringify(content)}`);
  }
  console.log(JSON.stringify(content, null, 2));
  ```

  ```python Python 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>"
  ambient_session_id = "<ambient_session_id from create>"
  sdp_provider_id = ""  # Set only if your partnership requires it

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

  content_response = requests.get(
      f"{BASE_URL}/api/v1/ambient/session/{ambient_session_id}/content",
      headers=headers,
      params={"cumulative": "false"},
      timeout=60,
  )
  if content_response.status_code != 200:
      raise RuntimeError(
          f"Content failed: {content_response.status_code} {content_response.text}"
      )
  print(content_response.json())
  ```

  ```bash cURL theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  curl -X GET "https://sdp.suki-stage.com/api/v1/ambient/session/YOUR_SESSION_ID/content?cumulative=false" \
    -H "sdp_suki_token: YOUR_SUKI_TOKEN"

  curl -X GET "https://sdp.suki-stage.com/api/v1/ambient/session/YOUR_SESSION_ID/transcript" \
    -H "sdp_suki_token: YOUR_SUKI_TOKEN"
  ```
</CodeGroup>

<Warning>
  **Session Duration Requirement**

  Your ambient session must be **at least 1 minute long** for note generation to occur. Sessions shorter than 1 minute will receive a `SKIPPED` status, meaning no clinical note was generated.
</Warning>

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

### What you get back

A completed session returns a `summary` array of LOINC-coded note sections and, when enabled, a `structured_data` array. You can assert against these fields to confirm success:

Here is an example of what you get back:

```json theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
{
  "summary": [
    {
      "title": "History of Present Illness",
      "loinc_code": "10164-2",
      "content": "Patient reports a three-day history of..."
    },
    {
      "title": "Assessment and Plan",
      "loinc_code": "51847-2",
      "content": "1. Acute pharyngitis..."
    }
  ],
  "structured_data": [
    {
      "title": "Medication orders",
      "data": { "medication": "Amoxicillin", "dose": "500 mg" }
    }
  ]
}
```

The transcript endpoint returns the conversation text with per-segment timestamps. For the full field list, see [Get ambient session Content](/api-reference/ambient-content/content) and [Get Transcript](/api-reference/ambient-content/transcript).

### Verify your integration

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

* Authenticate successfully and use the returned `sdp_suki_token` in follow-up requests.
* Create an ambient session and store the returned `ambient_session_id` (and `composition_id` when you need note-level APIs).
* Stream encounter audio on **`/ws/stream`** with the required PCM format and message order.
* End the session and confirm that Suki starts processing.
* Retrieve note content (and optionally the transcript), or confirm your webhook receives `session_summary_generated`.

After this path works end to end on staging, continue with context quality, interoperability fields, and production rollout.

## Next steps

After you complete your first Ambient API session:

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

<Icon icon="file-lines" iconType="solid" /> [Context API](/api-reference/ambient-sessions/context) - Seed specialty, sections, and patient info for better note accuracy.

<Icon icon="file-lines" iconType="solid" /> [Audio streaming API](/api-reference/ambient-sessions/audio-stream) - WebSocket connection details and message formats.

<Icon icon="file-lines" iconType="solid" /> [Webhook](/api-reference/asynchronous/webhook) - Configure webhooks to receive notifications when notes are ready.
