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

# Build Full-Stack APP To Capture Ambient Notes

> Learn how to build a full-stack app to capture Ambient notes with Suki. This tutorial covers the Ambient Partner APIs, Go backend, and frontend UI.

<Callout icon="info-circle" color="#FFE148">
  **What you will build**

  <p className="doc-tutorial-meta" aria-label="35 min, Intermediate">
    <Badge size="sm" color="gray" icon="clock">35 min</Badge> |
    <Badge size="sm" color="gray" icon="user-graduate">Intermediate</Badge>
  </p>

  * A Go backend that keeps Partner Token and Suki Token on the server.
  * An Ambient visit flow you can run yourself: create session, attach context, stream PCM, end, and retrieve the note.
  * Working Go examples for every Ambient REST and WebSocket step you need.
</Callout>

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

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

  <Prompt description="Fetch the Ambient and audio streaming skills and connect the documentation MCP." icon="gear" iconType="regular" actions={["copy", "cursor"]}>
    Implement Ambient Partner APIs in Go like a clinic visit backend.
    Fetch the Ambient skill:
    [https://developer.suki.ai/.well-known/agent-skills/suki-ambient/SKILL.md](https://developer.suki.ai/.well-known/agent-skills/suki-ambient/SKILL.md)
    Fetch the audio streaming skill:
    [https://developer.suki.ai/.well-known/agent-skills/suki-audio-streaming/SKILL.md](https://developer.suki.ai/.well-known/agent-skills/suki-audio-streaming/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>

## Agenda

In this tutorial, you learn how to implement **Suki Ambient APIs** in Go and run one ambient session from start to finish.

You will learn how to:

1. Authenticate with Partner ID and Partner Token, and keep tokens on the server.
2. Create an ambient session and attach provider, patient, visit, and note section context.
3. Open `wss://…/ws/stream`, send `START_TIME`, stream LINEAR16 PCM as JSON audio frames, and end with `RU9G`.
4. End the session over REST, poll status, then retrieve content, transcript, and structured data.
5. Optionally set note style with user preferences and submit note feedback.

Go through each step, copy the code examples into your product, and follow the same API sequence in your product.

<Note>
  The sample is **not a public GitHub repo**. Use the file explorer below to browse the code, or click **Run locally** to download a zip. Credentials are not included. Copy the patterns you need into your own backend.
</Note>

By the end, you can map which Ambient APIs to call, in which order, with working Go code for each step.

<span id="what-you-build" />

## What you build

You build a small clinic UI inside a Go application. The browser never calls Suki directly. Your Go server holds credentials, opens Ambient sessions, and forwards microphone PCM to Suki.

```mermaid actions={false} theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#FFE148','primaryTextColor':'#111827','primaryBorderColor':'#D4A017','lineColor':'#6F5410','secondaryColor':'#FFF394','tertiaryColor':'#FFFADE','tertiaryTextColor':'#111827','tertiaryBorderColor':'#D4A017','arrowheadColor':'#6F5410','fontSize':'14px','edgeLabelBackground':'#FFFADE'}}}%%
flowchart TB
  A[Browser mic and chart UI]
  B[Local Go server tokens stay here]
  C[Suki staging REST and WSS]
  A -->|ws://127.0.0.1:8080/ws/pcm| B
  B -->|REST https://sdp.suki-stage.com| C
  B -->|WSS wss://sdp.suki-stage.com| C
```

This tutorial covers only the **Ambient Partner API** path in the steps below. Clinic schedule UI, Dictation, and a fake EHR send are also in the sample. Browse the full partner sample in the explorer.

### Browse the sample project

Use the sidebar in the explorer to open files from the **ambient-api-starter** sample. Partner credentials are not included. Use `.env.example` as the template. **Run locally** downloads a zip you unzip and run with Go on your machine (it cannot run inside the browser).

<div data-tut-code-explorer data-manifest="/documentation/assets/tutorials/ambient-api-go-starter/sample/manifest.json" data-manifest-fallback="/documentation/assets/tutorials/ambient-api-go-starter/sample/manifest.txt" data-zip="/documentation/assets/tutorials/ambient-api-go-starter/suki-partner-samples.zip" data-default-open="ambient-api-starter/internal/suki/client.go" />

<span id="apis-used" />

## Ambient APIs used for this tutorial

| Step | Method and path |
| :- | :- |
| Login | `POST /api/v1/auth/login` |
| Register provider (once) | `POST /api/v1/auth/register` |
| Create ambient session | `POST /api/v1/ambient/session/create` |
| Attach context | `POST /api/v1/ambient/session/{id}/context` |
| Stream audio | `WSS /ws/stream` (`AUDIO` frames, end marker `RU9G`) |
| End session | `POST /api/v1/ambient/session/{id}/end` |
| Poll status | `GET /api/v1/ambient/session/{id}/status` |
| Get note content | `GET /api/v1/ambient/session/{id}/content` |
| Get transcript | `GET /api/v1/ambient/session/{id}/transcript` |
| Get structured data | `GET /api/v1/ambient/session/{id}/structured-data` and `GET /api/v1/ambient/note/{id}/structured-data` |
| Note section catalog | `GET /api/v1/info/loincs` |
| Note style | `PATCH /api/v1/user/preferences` |
| Note feedback | `POST /api/v1/ambient/session/{id}/content/feedback` |

Staging hosts you use while you learn:

| Protocol | Host |
| :- | :- |
| REST | `https://sdp.suki-stage.com` |
| WebSocket | `wss://sdp.suki-stage.com` |

When you move to production, switch to `https://sdp.suki.ai` and `wss://sdp.suki.ai`. See [Ambient API overview](/api-reference/overview).

<span id="architecture" />

## Architecture

Follow this Ambient visit order in your Go backend:

```mermaid actions={false} theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#FFE148','primaryTextColor':'#111827','primaryBorderColor':'#D4A017','lineColor':'#6F5410','secondaryColor':'#FFF394','tertiaryColor':'#FFFADE','tertiaryTextColor':'#111827','tertiaryBorderColor':'#D4A017','arrowheadColor':'#6F5410','fontSize':'14px','edgeLabelBackground':'#FFFADE'}}}%%
flowchart LR
  A[Login or register] --> B[Create ambient session]
  B --> C[Attach session context]
  C --> D[Open /ws/stream]
  D --> E[Stream PCM audio]
  E --> F[Send RU9G and end session]
  F --> G[Poll status]
  G --> H[Get content and structured data]
```

<span id="best-practices-in-this-tutorial" />

## Best practices you follow in this tutorial

Use these practices as you build your backend.

| Practice | What you do | Reference |
| :- | :- | :- |
| Keep tokens on the server | Store Partner Token and Suki Token in Go only. Never put them in browser code. | [Partner authentication](/documentation/how-to/partner-authentication) |
| Capture LINEAR16 PCM at 16 kHz | Send mono 16-bit little-endian PCM in small chunks as base64 JSON frames. | [Audio capture best practices](/documentation/how-to/audio-streaming/audio-capture-best-practices) |
| Follow Ambient stream order | Send `START_TIME`, stream `AUDIO`, end with `RU9G`, then call end session. | [Ambient audio streaming](/documentation/how-to/audio-streaming/ambient-audio-streaming) |
| Use the Ambient wire format | Use JSON text frames on `/ws/stream`. Do not send binary frames or Dictation markers. | [Ambient WebSocket wire format](/documentation/how-to/audio-streaming/websocket-streaming-wire-format-ambient) |
| Attach chart context before audio | Seed patient, visit, and LOINC sections so Suki can write the note. | [Session context](/api-reference/ambient-sessions/context) |
| Poll status before you fetch the note | Wait for `completed`, then retrieve content, transcript, or structured data. | [Retrieve ambient content](/documentation/how-to/ambient-clinical-notes/retrieve-ambient-content) |
| Pause with `KEEP_ALIVE` | While Ambient is paused, send `KEEP_ALIVE` so the stream stays open. | [Complete an ambient visit](/documentation/how-to/ambient-clinical-notes/end-ambient-session) |

<span id="prerequisites" />

<span id="dependencies-to-run" />

## Prerequisites and dependencies

<Tabs>
  <Tab title="Prerequisites">
    Before you begin, make sure you have:

    * [Go 1.22+](https://go.dev/dl/) installed.
    * Completed [Partner onboarding](/documentation/get-started/partner-onboarding) and received staging **Partner ID**.
    * Chrome or Edge with microphone permission for a live visit test.

    Set these environment variables when you run locally:

    | Variable | Purpose |
    | :- | :- |
    | `PARTNER_ID` | Your staging Partner ID |
    | `PARTNER_TOKEN` | Your staging Partner Token (server only) |
    | `AUTH_MODE` | `standard`, `bearer`, or `single` as assigned by Suki |
    | `PROVIDER_ID` | Required when `AUTH_MODE` is `bearer` or `single` |
    | `SUKI_BASE_URL` | Defaults to `https://sdp.suki-stage.com` |
    | `SUKI_WS_URL` | Defaults to `wss://sdp.suki-stage.com` |

    <Tip>
      Store **Partner Token** and **Suki Token** only on your backend. Never put them in browser JavaScript.
    </Tip>
  </Tab>

  <Tab title="Dependencies">
    Install and configure these before you run the Ambient path:

    | Dependency | Why you need it |
    | :- | :- |
    | Go 1.22+ | Language for the Partner API client |
    | `github.com/gorilla/websocket` | Ambient `/ws/stream` client |
    | Staging Partner ID and Partner Token | Login and session APIs |
    | 16 kHz LINEAR16 mono PCM | Ambient audio format |
    | HTTPS and WSS access to `sdp.suki-stage.com` | Staging REST and streaming |

    Add the WebSocket client:

    ```shell theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    go get github.com/gorilla/websocket@v1.5.3
    ```

    Standard library packages used in the snippets: `net/http`, `encoding/json`, `encoding/base64`, `context`, `fmt`, `io`, `sync`, `time`, `strings`.
  </Tab>
</Tabs>

<span id="project-setup" />

## Project setup

1. Create a Go module for your backend.
2. Add `github.com/gorilla/websocket`.
3. Put Partner credentials in a local `.env` or secret store. Do not commit them.
4. Plan to send **LINEAR16** PCM at **16 kHz**, mono, 16-bit little-endian. If your UI captures browser audio, resample to that format before you forward bytes to Suki.

Your backend should have this shape:

```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
package suki

import (
	"net/http"
	"strings"
	"sync"
	"time"
)

type Client struct {
	baseURL      string
	partnerID    string
	partnerToken string
	authMode     string
	providerID   string
	http         *http.Client

	mu        sync.Mutex
	sukiToken string
	tokenAt   time.Time
}

func NewClient(baseURL, partnerID, partnerToken, authMode, providerID string) *Client {
	return &Client{
		baseURL:      strings.TrimRight(baseURL, "/"),
		partnerID:    partnerID,
		partnerToken: partnerToken,
		authMode:     authMode,
		providerID:   providerID,
		http:         &http.Client{Timeout: 60 * time.Second},
	}
}
```

<span id="tutorial-steps" />

## Tutorial steps

Go through each step in order. Read the explanation, copy the Go example, then continue. Every code block is written in Go and follows the Ambient API path.

<Steps>
  <Step title="Log In and Keep the Suki Token">
    Call [Login](/api-reference/authentication/login) with Partner ID and Partner Token. Send the returned `suki_token` later as the `sdp_suki_token` header. If the provider is new, call [Register](/api-reference/authentication/register) once, then log in again.

    In the demo UI, clinic sign in stays in the browser. Partner Token stays in `.env` on the Go server.

    **Code example**

    ```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    func (c *Client) login(ctx context.Context) (string, error) {
    var out struct {
    	SukiToken string `json:"suki_token"`
    }
    body := map[string]string{
    	"partner_id":    c.partnerID,
    	"partner_token": c.partnerToken,
    }
    if c.providerID != "" {
    	body["provider_id"] = c.providerID
    }
    if err := c.doJSON(ctx, http.MethodPost, c.baseURL+"/api/v1/auth/login", "", body, http.StatusOK, &out); err != nil {
    	return "", err
    }
    if out.SukiToken == "" {
    	return "", fmt.Errorf("login response missing suki_token")
    }
    return out.SukiToken, nil
    }

    func (c *Client) RegisterThenLogin(ctx context.Context, providerName, providerOrgID string) error {
    body := map[string]string{
    	"partner_id":      c.partnerID,
    	"partner_token":   c.partnerToken,
    	"provider_name":   strings.TrimSpace(providerName),
    	"provider_org_id": strings.TrimSpace(providerOrgID),
    }
    if c.providerID != "" {
    	body["provider_id"] = c.providerID
    }
    // 409 Conflict means the provider is already registered.
    _ = c.doJSON(ctx, http.MethodPost, c.baseURL+"/api/v1/auth/register", "", body, http.StatusCreated, nil)

    token, err := c.login(ctx)
    if err != nil {
    	return err
    }
    c.mu.Lock()
    c.sukiToken = token
    c.tokenAt = time.Now()
    c.mu.Unlock()
    return nil
    }
    ```

    For every subsequent REST call, set the following headers:

    ```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("sdp_suki_token", sukiToken)
    if providerID != "" {
    req.Header.Set("sdp_provider_id", providerID) // single auth mode
    }
    ```
  </Step>

  <Step title="Open a Visit from the Clinic UI">
    Before you create an ambient session, run the sample locally so you have a clinic UI and a patient chart ready.

    1. In the explorer above, click **Run locally** to download `suki-partner-samples.zip`.
    2. Unzip the archive, then `cd ambient-api-starter`.
    3. Copy `.env.example` to `.env` and set your staging **Partner ID** and **Partner Token**.
    4. Run `go run ./cmd/server`, then open `http://127.0.0.1:8080`.
    5. Register or sign in to the demo clinic, open **Appointments**, and add a walk-in or open a scheduled visit so a patient chart is ready.

    The clinic schedule is demo UI only. Suki Ambient does not require a booked calendar slot. Your Go backend still runs the same create session and stream path for a walk-in or a scheduled visit.

    In the explorer, open `ambient-api-starter/internal/suki/client.go` to see how login and session helpers are wired before the next steps.
  </Step>

  <Step title="Create an Ambient Session">
    Call [Create ambient session](/api-reference/ambient-sessions/create). The response includes `ambient_session_id` (required for streaming) and often `composition_id` (useful for note-scoped structured data).

    ```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    type CreateSessionResult struct {
    AmbientSessionID string `json:"ambient_session_id"`
    CompositionID    string `json:"composition_id"`
    }

    func (c *Client) CreateAmbientSession(ctx context.Context) (CreateSessionResult, error) {
    token, err := c.Token(ctx)
    if err != nil {
    	return CreateSessionResult{}, err
    }
    var out CreateSessionResult
    err = c.doJSON(ctx, http.MethodPost, c.baseURL+"/api/v1/ambient/session/create", token, map[string]any{}, http.StatusCreated, &out)
    if err != nil {
    	return CreateSessionResult{}, err
    }
    if out.AmbientSessionID == "" {
    	return CreateSessionResult{}, fmt.Errorf("create response missing ambient_session_id")
    }
    return out, nil
    }
    ```
  </Step>

  <Step title="Attach Session Context">
    Send provider specialty, patient demographics, visit details, and LOINC note sections with [Session context](/api-reference/ambient-sessions/context). Suki uses this chart context when writing the note.

    ```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    type NoteSection struct {
    Loinc string `json:"loinc"`
    Title string `json:"title,omitempty"`
    }

    var DefaultSections = []NoteSection{
    {Loinc: "10154-3", Title: "Chief Complaint"},
    {Loinc: "10164-2", Title: "History of Present Illness"},
    {Loinc: "29545-1", Title: "Physical Exam"},
    {Loinc: "51847-2", Title: "Assessment and Plan"},
    }

    func DemoContext(sections []NoteSection) map[string]any {
    if len(sections) == 0 {
    	sections = DefaultSections
    }
    return map[string]any{
    	"provider": map[string]string{"specialty": "CARDIOLOGY", "provider_role": "ATTENDING"},
    	"patient": map[string]any{
    		"patient_id": "demo-alex-001",
    		"name":       map[string]any{"given": []string{"Alex"}, "family": "Demo"},
    		"dob":        "1985-06-15",
    		"sex":        "male",
    	},
    	"visit": map[string]string{
    		"chief_complaint":  "Headache",
    		"encounter_type":   "AMBULATORY",
    		"reason_for_visit": "Follow-up for migraines",
    		"visit_type":       "ESTABLISHED_PATIENT",
    	},
    	"sections": sections,
    }
    }

    func (c *Client) SeedContext(ctx context.Context, ambientSessionID string, payload map[string]any) error {
    token, err := c.Token(ctx)
    if err != nil {
    	return err
    }
    url := fmt.Sprintf("%s/api/v1/ambient/session/%s/context", c.baseURL, ambientSessionID)
    return c.doJSON(ctx, http.MethodPost, url, token, payload, http.StatusOK, nil)
    }
    ```

    <Note>
      Do not put verbosity or section format in session context. Set those with `PATCH /api/v1/user/preferences` before recording starts.
    </Note>
  </Step>

  <Step title="Open the Ambient WebSocket and Stream PCM">
    Dial [Ambient audio streaming](/documentation/how-to/audio-streaming/ambient-audio-streaming) at `/ws/stream`. Authenticate with HTTP headers from Go (`sdp_suki_token`, `ambient_session_id`, and `sdp_provider_id` when required).

    On the patient chart in the local demo, start Ambient from the microphone control. The browser sends PCM to your Go server at `ws://127.0.0.1:8080/ws/pcm`, and Go forwards it to Suki on `/ws/stream`.

    While you implement this step:

    1. Keep `go run ./cmd/server` running from Step 2.
    2. In the explorer, open `ambient-api-starter/internal/suki/stream.go` (and related session helpers) to match the dial, `START_TIME`, `AUDIO`, and `RU9G` order.
    3. On the chart, start Ambient, speak a short fake visit, then stop. Confirm your server logs show frames forwarding without Partner Token errors.

    Send `START_TIME` first, then base64 PCM chunks in `AUDIO` frames. Chunk about **3200 bytes** (about 100 ms of 16 kHz mono 16-bit). Keep sending audio at least every **25 seconds**, or send `KEEP_ALIVE` while paused.

    ```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    const chunkBytes = 3200 // ~100 ms of 16 kHz mono 16-bit PCM

    type Stream struct {
    mu   sync.Mutex
    conn *websocket.Conn
    }

    func (c *Client) DialStream(sukiToken, ambientSessionID, wsHost string) (*Stream, error) {
    hdr := http.Header{}
    hdr.Set("sdp_suki_token", sukiToken)
    hdr.Set("ambient_session_id", ambientSessionID)
    if pid := c.ProviderID(); pid != "" {
    	hdr.Set("sdp_provider_id", pid)
    }
    conn, resp, err := websocket.DefaultDialer.Dial(wsHost+"/ws/stream", hdr)
    if err != nil {
    	if resp != nil {
    		b, _ := io.ReadAll(resp.Body)
    		resp.Body.Close()
    		return nil, fmt.Errorf("WebSocket connect failed: %w; body=%s", err, string(b))
    	}
    	return nil, err
    }
    rfc3339 := time.Now().UTC().Format("2006-01-02T15:04:05Z")
    if err := conn.WriteJSON(map[string]any{
    	"type": "START_TIME",
    	"data": base64.StdEncoding.EncodeToString([]byte(rfc3339)),
    }); err != nil {
    	conn.Close()
    	return nil, fmt.Errorf("send START_TIME: %w", err)
    }
    return &Stream{conn: conn}, nil
    }

    func (s *Stream) SendPCM(pcm []byte) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    for i := 0; i < len(pcm); i += chunkBytes {
    	end := i + chunkBytes
    	if end > len(pcm) {
    		end = len(pcm)
    	}
    	if err := s.conn.WriteJSON(map[string]any{
    		"type": "AUDIO",
    		"data": base64.StdEncoding.EncodeToString(pcm[i:end]),
    	}); err != nil {
    		return err
    	}
    }
    return nil
    }

    func (s *Stream) SendEvent(event string) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    return s.conn.WriteJSON(map[string]any{"type": "EVENT", "event": event})
    }

    func (s *Stream) EndAudio() error {
    s.mu.Lock()
    defer s.mu.Unlock()
    return s.conn.WriteJSON(map[string]any{"type": "AUDIO", "data": "RU9G"})
    }
    ```

    Pause and resume use stream events (same session):

    ```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    _ = stream.SendEvent("PAUSE")
    _ = stream.SendEvent("KEEP_ALIVE") // every about 4 to 5 seconds while paused
    _ = stream.SendEvent("RESUME")
    ```
  </Step>

  <Step title="End the Session and Poll for the Note">
    When the visit stops, send the ambient end marker `RU9G`, close the WebSocket, then call [End ambient session](/api-reference/ambient-sessions/end). Poll [Session status](/api-reference/ambient-content/status) until `completed`, then fetch [content](/documentation/how-to/ambient-clinical-notes/retrieve-ambient-content).

    ```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    func (c *Client) EndSession(ctx context.Context, ambientSessionID string) error {
    token, err := c.Token(ctx)
    if err != nil {
    	return err
    }
    url := fmt.Sprintf("%s/api/v1/ambient/session/%s/end", c.baseURL, ambientSessionID)
    return c.doJSON(ctx, http.MethodPost, url, token, nil, http.StatusOK, nil)
    }

    func (c *Client) Status(ctx context.Context, ambientSessionID string) (string, error) {
    token, err := c.Token(ctx)
    if err != nil {
    	return "", err
    }
    var out struct {
    	Status string `json:"status"`
    }
    url := fmt.Sprintf("%s/api/v1/ambient/session/%s/status", c.baseURL, ambientSessionID)
    if err := c.doJSON(ctx, http.MethodGet, url, token, nil, http.StatusOK, &out); err != nil {
    	return "", err
    }
    return out.Status, nil
    }

    func (c *Client) Content(ctx context.Context, ambientSessionID string) (any, error) {
    return c.getOK(ctx, fmt.Sprintf("%s/api/v1/ambient/session/%s/content?cumulative=false", c.baseURL, ambientSessionID))
    }

    func (c *Client) Transcript(ctx context.Context, ambientSessionID string) (any, error) {
    return c.getOK(ctx, fmt.Sprintf("%s/api/v1/ambient/session/%s/transcript", c.baseURL, ambientSessionID))
    }

    func (c *Client) SessionStructuredData(ctx context.Context, ambientSessionID string) (any, error) {
    return c.getOK(ctx, fmt.Sprintf("%s/api/v1/ambient/session/%s/structured-data", c.baseURL, ambientSessionID))
    }
    ```

    End the stream and poll for results like this:

    ```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    _ = stream.EndAudio() // RU9G
    stream.Close()
    if err := client.EndSession(ctx, ambientSessionID); err != nil {
    return err
    }
    for {
    status, err := client.Status(ctx, ambientSessionID)
    if err != nil {
    	return err
    }
    switch status {
    case "completed":
    	note, _ := client.Content(ctx, ambientSessionID)
    	transcript, _ := client.Transcript(ctx, ambientSessionID)
    	structured, _ := client.SessionStructuredData(ctx, ambientSessionID)
    	_, _, _ = note, transcript, structured
    	return nil
    case "failed", "skipped", "aborted":
    	return fmt.Errorf("ambient session ended with status %s", status)
    }
    time.Sleep(2 * time.Second)
    }
    ```

    <Warning>
      Very short recordings are often skipped. When you test Ambient note generation, aim for about one minute of clinical speech.
    </Warning>
  </Step>

  <Step title="Optional: Note Style and Feedback">
    Set verbosity and section format with [User preferences](/api-reference/user-preferences/preferences) before you start recording. After the note is ready, submit a 1 to 5 rating with [content feedback](/api-reference/feedback/feedback).

    ```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    func (c *Client) UpdatePreferences(ctx context.Context, verbosity, sectionStyle string) error {
    payload := map[string]any{
    	"personalization_preference": map[string]any{
    		"verbosity": verbosity, // CONCISE | BALANCED | DETAILED
    		"section_format": []map[string]string{
    			{"loinc": "10164-2", "style": sectionStyle}, // NARRATIVE | BULLETED
    			{"loinc": "51847-2", "style": sectionStyle},
    		},
    	},
    }
    token, err := c.Token(ctx)
    if err != nil {
    	return err
    }
    return c.doJSON(ctx, http.MethodPatch, c.baseURL+"/api/v1/user/preferences", token, payload, http.StatusOK, nil)
    }

    func (c *Client) SubmitContentFeedback(ctx context.Context, ambientSessionID string, rating int, comments string) error {
    token, err := c.Token(ctx)
    if err != nil {
    	return err
    }
    body := map[string]any{
    	"ratingFeedback": map[string]any{
    		"min_rating": 1,
    		"max_rating": 5,
    		"rating":     rating,
    	},
    }
    if comments != "" {
    	body["qualitative_comments"] = comments
    }
    url := fmt.Sprintf("%s/api/v1/ambient/session/%s/content/feedback", c.baseURL, ambientSessionID)
    return c.doJSON(ctx, http.MethodPost, url, token, body, http.StatusCreated, nil)
    }
    ```
  </Step>
</Steps>

<span id="full-ambient-start-flow" />

## Full Ambient start flow

Use one function to run the start path: login, create the session, attach context, then open the WebSocket stream.

```go theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
func StartAmbientVisit(ctx context.Context, client *Client, wsHost string, chart map[string]any) (ambientSessionID string, stream *Stream, err error) {
	if err := client.Login(ctx); err != nil {
		return "", nil, err
	}
	created, err := client.CreateAmbientSession(ctx)
	if err != nil {
		return "", nil, err
	}
	payload := DemoContext(DefaultSections)
	// Overlay patient and visit fields from your EHR chart when present.
	if chart != nil {
		if visit, ok := chart["visit"].(map[string]any); ok {
			payload["visit"] = visit
		}
		if patient, ok := chart["patient"].(map[string]any); ok {
			payload["patient"] = patient
		}
	}
	if err := client.SeedContext(ctx, created.AmbientSessionID, payload); err != nil {
		_ = client.EndSession(ctx, created.AmbientSessionID)
		return "", nil, err
	}
	token, err := client.Token(ctx)
	if err != nil {
		_ = client.EndSession(ctx, created.AmbientSessionID)
		return "", nil, err
	}
	stream, err = client.DialStream(token, created.AmbientSessionID, wsHost)
	if err != nil {
		_ = client.EndSession(ctx, created.AmbientSessionID)
		return "", nil, err
	}
	return created.AmbientSessionID, stream, nil
}
```

In a UI-backed demo, the browser can send mic PCM to your local Go process over `ws://127.0.0.1:8080/ws/pcm`. Your Go process then calls `stream.SendPCM(pcm)` toward Suki. Capture audio any way your product needs, as long as the bytes you send to `/ws/stream` are LINEAR16 PCM.

<span id="how-this-tutorial-was-tested" />

## How this tutorial was tested

This tutorial is tested on the following:

* Go 1.22+ on macOS
* Staging endpoints `https://sdp.suki-stage.com` and `wss://sdp.suki-stage.com`
* Local server start with `go run ./cmd/server` on `http://127.0.0.1:8080`
* Partner ID and Partner Token from a staging partner account in local `.env`
* Demo clinic register or sign in in Chrome
* Ambient visit on the patient chart: start recording, stream microphone PCM through the Go server, stop, poll status, and retrieve note content
* Fake clinical speech only (do not record real patients)

<span id="common-mistakes" />

## Common mistakes

| Mistake | What to do instead |
| :- | :- |
| Putting Partner Token in the browser | Keep Partner Token and Suki Token on the Go server only |
| Mixing Ambient and Dictation frames | Ambient uses `/ws/stream` and `RU9G`. Dictation uses `/ws/transcribe` and `AUDIO_END` |
| Sending binary WebSocket frames | Send JSON text frames with base64 PCM in `data` |
| Skipping context | Attach patient and visit context before you stream |
| Ending without `RU9G` | Send `AUDIO` with `data: "RU9G"` before you close the socket and call end |
| Expecting a note from a few seconds of audio | Record closer to one minute for Ambient note generation |

<span id="next-steps" />

## Next steps

<Icon icon="file-lines" iconType="solid" /> **[Build an ambient streaming client](/documentation/tutorials/ambient-websocket-code-example)** - End-to-end login, stream, and retrieve in another tutorial.

<Icon icon="file-lines" iconType="solid" /> **[Ambient audio streaming](/documentation/how-to/audio-streaming/ambient-audio-streaming)** - `/ws/stream` order, auth headers, and chunking.

<Icon icon="file-lines" iconType="solid" /> **[Retrieve ambient content](/documentation/how-to/ambient-clinical-notes/retrieve-ambient-content)** - Load note sections, transcript, and structured data after `completed`.

<Icon icon="file-lines" iconType="solid" /> **[Partner authentication](/documentation/how-to/partner-authentication)** - Keep Partner Token and Suki Token on the server.

<Icon icon="file-lines" iconType="solid" /> **[Create ambient session](/api-reference/ambient-sessions/create)** - Request and response fields for session create.
