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

# Handle Ambient Session Conflicts

> Clear a blocking remote ambient session with cancelRemote or endRemote, then retry create, and keep UI in sync when another Suki product ends the live session

<div className="quick-summary-wrapper">
  <div className="quick-summary-header">
    <span className="quick-summary-icon" aria-hidden="true" />

    <span className="quick-summary-title">Quick summary</span>
  </div>

  <div className="quick-summary-content">
    Clear a blocking remote ambient session with `cancelRemote` or `endRemote` when create fails with `sessionAlreadyExists`, then retry create. Keep your UI in sync with `onSessionTerminatedByPeer` when another Suki product cancels or ends the live session.
  </div>

  <div className="quick-summary-footer">
    <span className="quick-summary-footer-icon" aria-hidden="true" />

    <span className="quick-summary-footer-text">Last updated:</span>
    <span className="quick-summary-footer-date">September 2026</span>
  </div>
</div>

<Warning>
  * These flows apply only to **Ambient interoperability for Headless Web SDK**. Dictation and Form filling do not support interoperability.
  * Use **`@suki-sdk/platform`** and **`@suki-sdk/platform-react`** **v0.3.0** or later.
  * Pass **`emrEncounterId`** on create so the note is interoperable. Refer to [Ambient interoperability](/headless-web-sdk/guides/ambient-interoperability).
</Warning>

Only **one active ambient session** can exist for the same **provider and EMR encounter** across Suki products.

* If `session.create()` fails because another Suki product already has an active session for the same provider and EMR encounter, clear the existing session with `cancelRemote` or `endRemote`, then retry the create request.

* If another Suki product cancels or ends the session currently active in your Headless Web SDK app, the SDK clears its local session state and calls `onSessionTerminatedByPeer`.

## Resolve an active session conflict

When another Suki product already has an active ambient session for the same provider and EMR encounter, `session.create()` fails with:

* **HTTP status:** `409`
* **`reason`:** `sessionAlreadyExists`
* **`blockingSessionId`:** The ID of the active session blocking the create request, returned in `error.additionalProperties`

To resolve the conflict:

1. Catch the `session.create()` error and check whether `reason === "sessionAlreadyExists"`.
2. Read the `blockingSessionId` from `error.additionalProperties`.
3. Call **`cancelRemote`** or **`endRemote`** for the blocking session.
4. After the remote operation succeeds, retry `session.create()` with the same `emrEncounterId` and `encounterId`.

The Headless Web SDK does not require a delay between a successful `cancelRemote` or `endRemote` call and the retry of `session.create()`.

<Note>
  The `sessionAlreadyExists` conflict applies only when `session.create()` runs online. If the first ambient session is fully offline, another Suki product can start a session for the same EMR encounter without receiving a conflict error.
</Note>

### Conflict error fields

When create fails with a remote session conflict, the thrown `PlatformError` includes the following fields.

<ResponseField name="reason" type="&#x22;sessionAlreadyExists&#x22;">
  Identifies a remote session conflict. Check this value before calling `cancelRemote` or `endRemote`.
</ResponseField>

<ResponseField name="message" type="string">
  Human-readable conflict message from the platform. Use this for UI copy when present.
</ResponseField>

<ResponseField name="additionalProperties" type="object">
  Structured conflict details. Some JSDoc comments refer to `metadata`. The runtime field is `additionalProperties`.

  <Expandable title="properties" defaultOpen={true}>
    <ResponseField name="blockingSessionId" type="string" required>
      Ambient session id of the blocking remote session. Maps to Ambient API `ambient_session_id`. Pass this value to `cancelRemote` or `endRemote`.
    </ResponseField>

    <ResponseField name="blockingModality" type="string">
      **Optional**: Modality that owns the blocking session, for example `"WEB_SDK"` or `"MOBILE"`.
    </ResponseField>

    <ResponseField name="elapsed" type="string">
      **Optional**: How long the blocking session has been active, for example `"0s"` or `"5m"`.
    </ResponseField>
  </Expandable>
</ResponseField>

### Remote recovery actions

`cancelRemote` and `endRemote` are available on **`useAmbient`** and **`useAmbientSession`**. Pass the blocking session id from the conflict error. Do not pass your local `ambientSessionId` unless that id is the blocking session.

<ResponseField name="cancelRemote" type="(params: { ambientSessionId: string }) => Promise<void>">
  Discards the blocking remote session without generating a note from that session. Pass `{ ambientSessionId: blockingSessionId }`.
</ResponseField>

<ResponseField name="endRemote" type="(params: { ambientSessionId: string }) => Promise<void>">
  Ends the blocking remote session so any audio already captured can be processed. Pass `{ ambientSessionId: blockingSessionId }`.
</ResponseField>

If `cancelRemote` fails, the error reason is **`cancelRemoteSessionFailed`**. If `endRemote` fails, the error reason is **`endRemoteSessionFailed`**. Refer to [Error handling](/headless-web-sdk/guides/error-handling#interoperability-errors).

### Code example for clearing a remote session conflict

```tsx React expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
import { useAmbient } from "@suki-sdk/platform-react";

export function CreateOrResolveConflict() {
  const { cancelRemote, endRemote, session } = useAmbient();

  // Reuse the same encounterId for every re-ambient session on this note.
  const existingEncounterId = "visit-note-1";
  const emrEncounterId = "6ec3920f-b0b1-499d-a4e9-889bf788e5ab";

  const createOrResolveConflict = async () => {
    try {
      await session.create({
        encounterId: existingEncounterId,
        emrEncounterId,
      });
    } catch (error) {
      if (
        error &&
        typeof error === "object" &&
        "reason" in error &&
        error.reason === "sessionAlreadyExists"
      ) {
        const blockingSessionId =
          "additionalProperties" in error &&
          error.additionalProperties &&
          typeof error.additionalProperties === "object" &&
          "blockingSessionId" in error.additionalProperties
            ? String(error.additionalProperties.blockingSessionId)
            : undefined;

        if (!blockingSessionId) {
          throw error;
        }

        // Discard the blocking session:
        await cancelRemote({ ambientSessionId: blockingSessionId });

        // Or end the blocking session so captured audio can be processed:
        // await endRemote({ ambientSessionId: blockingSessionId });

        await session.create({
          encounterId: existingEncounterId,
          emrEncounterId,
        });
        return;
      }

      throw error;
    }
  };

  return (
    <button type="button" onClick={() => void createOrResolveConflict()}>
      Create or resolve conflict
    </button>
  );
}
```

<Warning>
  * Use **`cancelRemote`** when you want to discard the blocking session.
  * Use **`endRemote`** when you want the blocking session to end and process any audio already captured.
</Warning>

## What happens when another Suki product ends the live session

If another Suki product cancels or ends the ambient session that is currently active in your Headless Web SDK app, the SDK clears local session state before it notifies your app. The SDK:

* Stops the microphone.
* Deletes local audio and IndexedDB metadata for the session.
* Resets the stream.
* Sets local status to **`submitted`** when the termination reason is **`ENDED`**, or to **`cancelled`** otherwise.

Then it calls **`onSessionTerminatedByPeer`** with `{ sessionId, reason }`.

In that callback, update your recording UI to match the new session status. The Headless Web SDK already stopped the microphone and cleared local session state, so do not call `cancel()`, `submit()`, `cancelRemote()`, or `endRemote()`.

### Remote session end callback

Pass **`onSessionTerminatedByPeer`** to `useAmbientSession` when you need to keep recording UI in sync after another Suki product cancels or ends the live session.

<ResponseField name="onSessionTerminatedByPeer" type="function (payload: { sessionId: string; reason: string }) => void">
  **Optional**: Called after another Suki product cancels or ends the live session. Update your recording UI to match the new session status. Do not call `cancel()`, `submit()`, `cancelRemote()`, or `endRemote()`. The SDK already cleared local mic, stream, and session state.

  <Expandable title="payload" defaultOpen={true}>
    <ResponseField name="sessionId" type="string">
      The ambient session id that was cancelled or ended remotely.
    </ResponseField>

    <ResponseField name="reason" type="string">
      Termination reason from the other product, for example `"ENDED"` or `"CANCELLED"`. When the reason is `"ENDED"`, local status becomes `submitted`. Otherwise local status becomes `cancelled`.
    </ResponseField>
  </Expandable>
</ResponseField>

### Code example for handling a remote session end

```tsx React expandable theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
import { useAmbientSession } from "@suki-sdk/platform-react";

export function RemoteSessionEndListener({
  ambientSessionId,
}: {
  ambientSessionId: string;
}) {
  useAmbientSession({
    ambientSessionId,
    onSessionTerminatedByPeer: ({ sessionId, reason }) => {
      // reason: "ENDED" | "CANCELLED" | ...
      // Update recording UI. Do not call cancel, submit, cancelRemote, or endRemote.
      console.log("Remote session ended", sessionId, reason);
    },
  });

  return null;
}
```

## Available cookbooks

<div className="hp-io-method-grid tut-hub-card-grid" data-cookbook-related-grid>
  <a className="hp-io-method-card tut-hub-method-card" href="/documentation/cookbooks/create-interoperable-headless-session">
    <div className="tut-hub-card-media" aria-hidden="true" />

    <div className="hp-io-method-card-body">
      <div className="tut-hub-card-badges">
        <span className="hp-wn-badge hp-wn-badge-new">Ambient</span>
        <span className="hp-wn-badge cookbook-hub-badge-surface cookbook-hub-badge-surface--sdk">Headless Web SDK</span>
      </div>

      <h3 className="hp-io-method-card-title">Create an Interoperable Headless Session</h3>

      <p className="hp-io-method-card-desc cookbook-hub-card-desc">
        Pass emrEncounterId and clear conflicts.
      </p>

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

  <a className="hp-io-method-card tut-hub-method-card" href="/documentation/cookbooks/resolve-session-conflict">
    <div className="tut-hub-card-media tut-hub-card-media--blue" aria-hidden="true" />

    <div className="hp-io-method-card-body">
      <div className="tut-hub-card-badges">
        <span className="hp-wn-badge hp-wn-badge-new">Ambient</span>
        <span className="hp-wn-badge cookbook-hub-badge-surface cookbook-hub-badge-surface--api">API</span>
      </div>

      <h3 className="hp-io-method-card-title">Clear a Remote Session Conflict</h3>

      <p className="hp-io-method-card-desc cookbook-hub-card-desc">
        End remote session, then retry.
      </p>

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

## Next steps

<Icon icon="file-lines" iconType="solid" /> Refer to [Ambient interoperability](/headless-web-sdk/guides/ambient-interoperability) to enable create with `emrEncounterId` and `encounterId`.

<Icon icon="file-lines" iconType="solid" /> Refer to [Read shared ambient notes](/headless-web-sdk/guides/read-shared-ambient-notes) for `useGetEncounterInfo` and patient context for Web SDK handoff.

<Icon icon="file-lines" iconType="solid" /> Refer to [Create ambient session](/headless-web-sdk/guides/hooks/ambient-hook) for `useAmbient`, including `cancelRemote` and `endRemote` on create.

<Icon icon="file-lines" iconType="solid" /> Refer to [Manage ambient session](/headless-web-sdk/guides/hooks/ambient-session-hook) for `onSessionTerminatedByPeer`, `cancelRemote`, and `endRemote` on the live session.

<Icon icon="file-lines" iconType="solid" /> Refer to [Error handling](/headless-web-sdk/guides/error-handling) for `sessionAlreadyExists`, `cancelRemoteSessionFailed`, and `endRemoteSessionFailed`.
