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

# Form Filling API Quickstart

> Authenticate, create a Form filling session, stream audio, end the session, and retrieve structured form output on staging

This guide walks you through the steps to use the Form filling APIs to authenticate, create a session, provide context, stream audio through the shared Partner WebSocket (GET /ws/stream), end the session, and retrieve structured form output.

The WebSocket endpoint and message format are the same as ambient audio streaming. Use the Form filling session ID when establishing the WebSocket connection.

**What you will do**

1. **Authenticate** to get an `sdp_suki_token` (and **register** the user if needed).
2. **Create** a Form filling session and optionally **seed context** for template metadata.
3. **Stream audio** over **`/ws/stream`** using your **Form filling** **`ambient_session_id`**, then **end** the session when the visit is done.
4. **Retrieve** structured form output by polling **status** and **structured-data**, 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 prompt below to point your agent at the Form filling 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 Form filling skill and connect the documentation MCP." icon="gear" iconType="regular" actions={["copy", "cursor"]}>
    Build Form filling with Suki for Partners.
    Fetch the Form filling build skill:
    [https://developer.suki.ai/.well-known/agent-skills/suki-form-filling/SKILL.md](https://developer.suki.ai/.well-known/agent-skills/suki-form-filling/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 Form filling API.

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 Form filling 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.

### 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 credential placeholders. For optional context, set `FORM_TEMPLATE_ID` to a real template UUID from [Suki Medical form templates](/form-filling-api-reference/info/suki-medical-form-templates). Put a 16 kHz mono LINEAR16 WAV (or raw PCM) at `audio.wav`. The numbered steps after this section explain each call in detail.

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

<CodeGroup>
  ```python Python expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  # form_filling_staging.py
  # Flow: login → create → optional context → stream → end → poll structured-data
  # 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
  WAV_HEADER_BYTES = 44

  PARTNER_ID = "your-partner-id"
  PARTNER_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
  PROVIDER_ID = "provider-123"  # Optional; required for Bearer / Single Auth Token partners
  SDP_PROVIDER_ID = ""  # Leave empty unless your partnership requires sdp_provider_id
  # Example template ID from Form filling context docs — replace with one your tenant can use
  FORM_TEMPLATE_ID = "019d4cdc-9319-7d81-ae2e-fd6de7f1b4f0"
  SEED_CONTEXT = True
  AUDIO_PATH = "audio.wav"


  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_form_filling_session(suki_token: str) -> str:
      url = f"{BASE_URL}/api/v1/form-filling/session/create"
      r = requests.post(url, headers=rest_headers(suki_token), json={}, timeout=60)
      expect_status(r, url, 201)
      sid = r.json().get("ambient_session_id")
      if not sid:
          raise RuntimeError("create response missing ambient_session_id")
      return sid


  def seed_context(suki_token: str, ambient_session_id: str, form_template_id: str) -> None:
      url = f"{BASE_URL}/api/v1/form-filling/session/{ambient_session_id}/context"
      payload = {
          "form_filling": {
              "values": [{"form_template_id": form_template_id}],
          },
      }
      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])}))

          ws.send(json.dumps({"type": "AUDIO", "data": "RU9G"}))
      finally:
          ws.close()


  def end_session(suki_token: str, ambient_session_id: str) -> None:
      url = f"{BASE_URL}/api/v1/form-filling/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/form-filling/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_structured_data(suki_token: str, ambient_session_id: str) -> Any:
      url = f"{BASE_URL}/api/v1/form-filling/session/{ambient_session_id}/structured-data"
      r = requests.get(url, headers=rest_headers(suki_token), timeout=60)
      expect_status(r, url, 200)
      return r.json()


  def wait_for_structured_data(
      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,
                  "structured_data": get_structured_data(suki_token, ambient_session_id),
              }
          if status in ("failed", "aborted"):
              return {"status": status, "structured_data": 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")

      ambient_session_id = create_form_filling_session(suki_token)
      print("ambient_session_id:", ambient_session_id)

      if SEED_CONTEXT:
          seed_context(suki_token, ambient_session_id, FORM_TEMPLATE_ID)
          print("Context seeded")

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

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

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

  ```typescript TypeScript expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
  // form_filling_staging.ts
  // Flow: login → create → optional context → stream → end → poll structured-data
  // Node 18+: npm install ws && npx tsx form_filling_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;
  const WAV_HEADER_BYTES = 44;

  const PARTNER_ID = "your-partner-id";
  const PARTNER_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...";
  const PROVIDER_ID = "provider-123";
  const SDP_PROVIDER_ID = "";
  const FORM_TEMPLATE_ID = "019d4cdc-9319-7d81-ae2e-fd6de7f1b4f0";
  const SEED_CONTEXT = true;
  const AUDIO_PATH = "audio.wav";

  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
    }
    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 createFormFillingSession(sukiToken: string): Promise<string> {
    const url = `${BASE_URL}/api/v1/form-filling/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 };
    if (!data.ambient_session_id) {
      throw new Error("create response missing ambient_session_id");
    }
    return data.ambient_session_id;
  }

  async function seedContext(
    sukiToken: string,
    ambientSessionId: string,
    formTemplateId: string,
  ): Promise<void> {
    const url = `${BASE_URL}/api/v1/form-filling/session/${ambientSessionId}/context`;
    const response = await fetch(url, {
      method: "POST",
      headers: restHeaders(sukiToken),
      body: JSON.stringify({
        form_filling: {
          values: [{ form_template_id: formTemplateId }],
        },
      }),
    });
    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"),
              }),
            );
          }
          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 endSession(sukiToken: string, ambientSessionId: string): Promise<void> {
    const url = `${BASE_URL}/api/v1/form-filling/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/form-filling/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 getStructuredData(
    sukiToken: string,
    ambientSessionId: string,
  ): Promise<unknown> {
    const url = `${BASE_URL}/api/v1/form-filling/session/${ambientSessionId}/structured-data`;
    const response = await fetch(url, { headers: restHeaders(sukiToken) });
    await expectStatus(response, url, 200);
    return response.json();
  }

  async function waitForStructuredData(
    sukiToken: string,
    ambientSessionId: string,
    pollMs = 2000,
  ): Promise<{ status: string; structured_data: unknown }> {
    for (;;) {
      const status = await getStatus(sukiToken, ambientSessionId);
      console.log("status:", status);
      if (status === "completed") {
        return {
          status,
          structured_data: await getStructuredData(sukiToken, ambientSessionId),
        };
      }
      if (status === "failed" || status === "aborted") {
        return { status, structured_data: 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 ambientSessionId = await createFormFillingSession(sukiToken);
    console.log("ambient_session_id:", ambientSessionId);

    if (SEED_CONTEXT) {
      await seedContext(sukiToken, ambientSessionId, FORM_TEMPLATE_ID);
      console.log("Context seeded");
    }

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

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

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

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

## Create your first Form filling session

<Steps>
  <Step title="Authenticate to Get a Suki Token">
    To begin, you must authenticate to get your access token. Send a **POST** request to the [Login API](/api-reference/authentication/login) endpoint with the following parameters in the request body:

    * **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.
    * **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.
    * **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 Bearer type partners only.

    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.

    <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 [Register API](/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.
    </Note>

    <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 not required
        }),
      });

      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");
      console.log("suki_token:", data.suki_token);
      ```

      ```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 not required
          },
          timeout=60,
      )
      if response.status_code != 200:
          raise RuntimeError(f"Login failed: {response.status_code} {response.text}")
      print("suki_token:", response.json()["suki_token"])
      ```

      ```bash cURL expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      curl --request POST \
        --url https://sdp.suki-stage.com/api/v1/auth/login \
        --header 'Content-Type: application/json' \
        --data '{
          "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="Create Form Filling Session">
    Create a seesion by calling the [Create Form filling session API](/form-filling-api-reference/form-filling-sessions/create) endpoint with the following parameters in the request body:

    * **ambient\_session\_id** (Optional): Supply your own Form filling session UUID. If omitted, Suki generates one. Do not pass an [Ambient API](/api-reference/ambient-sessions/create) session ID.
    * **correlation\_id** (Optional): Client-supplied identifier for tracing or correlating requests.

    <Note>
      Both the Form filling session ID and the [Suki Ambient API session ID](/api-reference/ambient-sessions/create) are named **`ambient_session_id`**. Those identifiers refer to **different** sessions. Use only the **`ambient_session_id`** returned from **Form filling** `/session/create` for Form filling REST calls and for **`/ws/stream`**.
    </Note>

    The request body itself is optional (you can send an empty JSON object). The response always includes the **`ambient_session_id`** for this Form filling session. Use that value for context, streaming, **end**, **status**, and **structured-data** calls.

    <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 required

      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/form-filling/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)}`);
      }
      console.log("ambient_session_id:", created.ambient_session_id);
      ```

      ```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 required

      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/form-filling/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}"
          )
      print("ambient_session_id:", create_response.json()["ambient_session_id"])
      ```

      ```bash cURL expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      curl --request POST \
        --url https://sdp.suki-stage.com/api/v1/form-filling/session/create \
        --header 'sdp_suki_token: <sdp_suki_token>' \
        --header 'Content-Type: application/json' \
        --data '{}'
      ```
    </CodeGroup>
  </Step>

  <Step title="Seed Context (Optional)">
    After creating the session, you can send a **POST** request to the [Seed Context API](/form-filling-api-reference/form-filling-sessions/context) endpoint to provide form template metadata. Skip this step for the minimum path and add it after your first end-to-end run works.

    <Note>
      The request body is **optional**. If you omit it, skip this step and continue to audio capture. If you include **`form_filling`**, you must send valid **`values`**: an array of objects, each with a required **`form_template_id`** (UUID for the template). Providing **context** improves the quality of structured form output for the templates you select.
    </Note>

    Include the following in the request body when you supply context:

    * **form\_filling** (Optional): An object with **`values`**, an array of **`form_template_id`** entries that identify which Medical form templates apply to this session.

    Refer to the [Context API reference](/form-filling-api-reference/form-filling-sessions/context) for the full request structure. To know which templates are available, use [Suki Medical form templates API](/form-filling-api-reference/info/suki-medical-form-templates).

    <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 ambientSessionId = "<form filling ambient_session_id from create>";
      const sdpProviderId = "";
      const formTemplateId = "019d4cdc-9319-7d81-ae2e-fd6de7f1b4f0"; // Replace with your template UUID

      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/form-filling/session/${ambientSessionId}/context`,
        {
          method: "POST",
          headers,
          body: JSON.stringify({
            form_filling: {
              values: [{ form_template_id: formTemplateId }],
            },
          }),
        },
      );
      if (!contextResponse.ok) {
        throw new Error(`Context failed: ${contextResponse.status} ${await contextResponse.text()}`);
      }
      console.log("Context seeded");
      ```

      ```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>"
      ambient_session_id = "<form filling ambient_session_id from create>"
      sdp_provider_id = ""
      form_template_id = "019d4cdc-9319-7d81-ae2e-fd6de7f1b4f0"  # Replace with your template UUID

      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/form-filling/session/{ambient_session_id}/context",
          headers=headers,
          json={"form_filling": {"values": [{"form_template_id": form_template_id}]}},
          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 expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      curl --request POST \
        --url https://sdp.suki-stage.com/api/v1/form-filling/session/<ambient_session_id>/context \
        --header 'sdp_suki_token: <sdp_suki_token>' \
        --header 'Content-Type: application/json' \
        --data '{
          "form_filling": {
            "values": [{"form_template_id": "019d4cdc-9319-7d81-ae2e-fd6de7f1b4f0"}]
          }
        }'
      ```
    </CodeGroup>
  </Step>

  <Step title="Stream Audio">
    Stream visit audio on the Partner WebSocket **`GET /ws/stream`** on the same host as REST (for example **`wss://sdp.suki-stage.com/ws/stream`** in staging). Authenticate with your Form filling **`ambient_session_id`** and **`sdp_suki_token`**.

    The WebSocket message format matches ambient:

    * **`START_TIME`**: Base64 of an RFC 3339 timestamp.
    * **`AUDIO`**: Base64 LINEAR16 PCM chunks (16 kHz, mono; strip a typical 44-byte WAV header).
    * End marker **`AUDIO`** with `"data": "RU9G"`.

    <Tip>
      Use the [complete staging script](#complete-staging-script) for a full streaming client. When capture finishes, close the WebSocket, then call **end session**.
    </Tip>
  </Step>

  <Step title="End Session">
    To complete the session and begin processing, send a **POST** request to the [End Session API](/form-filling-api-reference/form-filling-sessions/end) endpoint. This signals that you will not send more audio for this session.

    <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 ambientSessionId = "<form filling ambient_session_id from create>";
      const sdpProviderId = "";

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

      const endResponse = await fetch(
        `${BASE_URL}/api/v1/form-filling/session/${ambientSessionId}/end`,
        { method: "POST", headers },
      );
      if (!endResponse.ok) {
        throw new Error(`End failed: ${endResponse.status} ${await endResponse.text()}`);
      }
      console.log("Session ended");
      ```

      ```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>"
      ambient_session_id = "<form filling ambient_session_id from create>"
      sdp_provider_id = ""

      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/form-filling/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 expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      curl --request POST \
        --url https://sdp.suki-stage.com/api/v1/form-filling/session/<ambient_session_id>/end \
        --header 'sdp_suki_token: <sdp_suki_token>'
      ```
    </CodeGroup>
  </Step>

  <Step title="Poll Status and Retrieve Structured Data">
    For the minimum path, poll [Check Status API](/form-filling-api-reference/form-filling-sessions/status) until a terminal state such as `completed`, then retrieve output with [Retrieve Structured Data API](/form-filling-api-reference/form-filling-sessions/structured-data).

    A completed form is one **`generated_values`** item with **`MEDICAL_FORM_STATUS_COMPLETED`** and a populated **`data`** object. Templates without output appear under **`non_generated_values`**.

    After that works, you can also use a [webhook](/form-filling-api-reference/asynchronous/webhook) so your partner callback receives completion events with **`_links`** to follow for results.

    <Note>
      Terminal statuses include **`completed`**, **`failed`**, and **`aborted`**.
    </Note>

    <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 ambientSessionId = "<form filling ambient_session_id from create>";
      const sdpProviderId = "";

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

      const structuredResponse = await fetch(
        `${BASE_URL}/api/v1/form-filling/session/${ambientSessionId}/structured-data`,
        { headers },
      );
      const structured = await structuredResponse.json();
      if (!structuredResponse.ok) {
        throw new Error(
          `Structured data failed: ${structuredResponse.status} ${JSON.stringify(structured)}`,
        );
      }
      console.log(JSON.stringify(structured, 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>"
      ambient_session_id = "<form filling ambient_session_id from create>"
      sdp_provider_id = ""

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

      structured_response = requests.get(
          f"{BASE_URL}/api/v1/form-filling/session/{ambient_session_id}/structured-data",
          headers=headers,
          timeout=60,
      )
      if structured_response.status_code != 200:
          raise RuntimeError(
              f"Structured data failed: {structured_response.status_code} {structured_response.text}"
          )
      print(structured_response.json())
      ```

      ```bash cURL expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
      curl -X GET "https://sdp.suki-stage.com/api/v1/form-filling/session/YOUR_SESSION_ID/status" \
        -H "sdp_suki_token: YOUR_SUKI_TOKEN"

      curl -X GET "https://sdp.suki-stage.com/api/v1/form-filling/session/YOUR_SESSION_ID/structured-data" \
        -H "sdp_suki_token: YOUR_SUKI_TOKEN"
      ```
    </CodeGroup>
  </Step>
</Steps>

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

### Verify your first Form filling API session

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

* Authenticate successfully and use the returned `sdp_suki_token` in follow-up requests.
* Create a Form filling session and store the returned **`ambient_session_id`**.
* Stream visit audio on **`/ws/stream`** with that Form filling session ID.
* End the session and confirm that Suki starts processing.
* Poll session status until a terminal state, then retrieve structured data.

After this path works end to end on staging, add form template context, webhooks, recordings, and feedback.

## Available tutorials

<div className="hp-io-method-grid tut-hub-card-grid">
  <a className="hp-io-method-card tut-hub-method-card" href="/documentation/tutorials/form-filling-websocket-code-example">
    <div className="tut-hub-card-media" aria-hidden="true" />

    <div className="hp-io-method-card-body">
      <span className="hp-wn-badge hp-wn-badge-new">Form filling</span>
      <h3 className="hp-io-method-card-title">Build a Form filling Session Client</h3>

      <p className="hp-io-method-card-desc">
        Create a Form filling session, send template context, stream audio, and retrieve structured form data.
      </p>

      <div className="hp-io-method-card-meta tut-hub-card-foot" aria-label="20 min, Intermediate">
        <div className="tut-hub-card-foot-meta">
          <span className="hp-io-method-card-meta-time">20 min</span>
          <span className="tut-hub-level">Intermediate</span>
        </div>
      </div>
    </div>
  </a>
</div>

## Next steps

After completing your first session, add these capabilities when you need them:

<CardGroup cols={2}>
  <Card title="Seed Form Filling Context" icon="file-lines" href="/form-filling-api-reference/form-filling-sessions/context" arrow={true}>
    Bind a medical form template when you need structured form output for a specific template.
  </Card>

  <Card title="Configure Webhooks" icon="bell" href="/documentation/webhook/configuration" arrow={true}>
    Register your callback URL for async Form filling completion.
  </Card>

  <Card title="Form Filling Error Messages" icon="triangle-exclamation" href="/form-filling-api-reference/error-messages-form-filling" arrow={true}>
    Map Form filling API error codes for create, stream, and retrieve calls.
  </Card>
</CardGroup>
