> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corti.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Real-time conversational transcript generation and fact extraction (FactsR™)

> WebSocket Secure (WSS) API Documentation for /streams endpoint

## Overview

The WebSocket Secure (WSS) `/streams` API enables real-time, bidirectional communication with the Corti system for interaction streaming. Clients can send and receive structured data, including transcripts and fact updates. Learn more about [FactsR™ here](/textgen/factsr/).

This documentation provides a structured guide for integrating the Corti WSS API for real-time interaction streaming.

<Info>
  This `/streams` endpoint supports real-time ambient documentation interactions and clinical decision support workflows.

  * If you are looking for a stateless endpoint that is geared towards front-end dictation workflows you should use the [/transcribe WSS](/api-reference/transcribe)
  * If you are looking for asynchronous ambient documentation interactions, then please refer to the [/documents endpoint](/api-reference/documents/generate-document)
</Info>

***

## 1. Establishing a Connection

Clients must initiate a WebSocket connection using the `wss://` scheme and provide a valid interaction ID in the URL.

<Note>When creating an interaction, the 200 response provides a `websocketUrl` for that interaction including the `tenant-name` as url parameter.
The authentication for the WSS streams requires in addition to the `tenant-name` parameter a `token` parameter to pass in the Bearer access token.</Note>

### Path Parameters

<ParamField path="id" type="uuid" required>
  Unique interaction identifier
</ParamField>

### Query Parameters

<ParamField query="environment" type="enum" required>`eu` or `us`</ParamField>

<ParamField query="tenant-name" type="string" required>
  Specifies the tenant context
</ParamField>

<ParamField query="token" type="string" required>Bearer \$token</ParamField>

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  import { CortiClient } from "@corti/sdk";

  // Replace these with your values
  const ACCESS_TOKEN = "<your-access-token>";

  const client = new CortiClient({
    auth: {
      accessToken: ACCESS_TOKEN,
    },
  });
  ```

  {/* Create a Corti SDK client using a Bearer access token */}

  ```csharp title="C# .NET" theme={null}
  using Corti;

  // Replace these with your values
  const string ACCESS_TOKEN = "<your-access-token>";

  var client = new CortiClient(
      auth: CortiClientAuth.Bearer(accessToken: ACCESS_TOKEN)
  );
  ```
</CodeGroup>

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  const streamSocket = await client.stream.connect({
    id: "<your-interaction-id>"
  });
  ```

  {/* Connect to /streams without configuration */}

  ```csharp title="C# .NET" theme={null}
  var stream = await client.CreateStreamApiAsync("<your-interaction-id>");
  await stream.ConnectAsync();
  ```
</CodeGroup>

***

## 2. Handshake Responses

### 101 Switching Protocols

Indicates a successful WebSocket connection.

Upon successful connection, send a `config` message to define the configuration: Specify the input language and expected output preferences.

<Info>The config message must be sent within 10 seconds of the web socket being opened to prevent `CONFIG-TIMEOUT`, which will require establishing a new wss connection.</Info>

***

## 3. Sending Messages

### Configuration

Declare your `/streams` configuration using the message `"type": "config"` followed by defining the `"configuration": {<config details, per options below>}`.

Defining the type is required along with `transcription: primaryLanguage` and `mode: type and outputLocale` configuration parameters. The other parameters are optional for use, depending on your need and workflow.

<Info>
  Configuration notes:

  * Clients must send a streams configuration message and wait for a response of type `CONFIG_ACCEPTED` before transmitting other data.
  * If the configuration is not valid it will return `CONFIG_DENIED`.
  * The configuration must be committed within 10 seconds of opening the WebSocket, else it will time-out with `CONFIG_NOT_PROVIDED`.
</Info>

<ParamField body="type" type="string" default="config" required />

<ParamField body="configuration" type="object" required>
  <Expandable defaultOpen="true">
    <ParamField body="transcription" type="object" required>
      Define parameters for speech to text processing:

      <Expandable>
        <ParamField body="primaryLanguage" type="string [enum]" required>
          The primary spoken language for transcription. See supported languages codes and more information [here](/about/languages).
        </ParamField>

        <ParamField body="isDiarization" type="bool">
          Set to true to enable speaker diarization in mono channel audio.
        </ParamField>

        <ParamField body="isMultichannel" type="bool">
          Set to true to enable transcription of a multi-channel audio stream.
        </ParamField>

        <ParamField body="participants" type="array">
          List of participants with roles assigned to a channel

          <Expandable>
            <ParamField body="channel" type="integer" required>
              Audio channel number (e.g., 0 or 1)
            </ParamField>

            <ParamField body="role" type="string [enum]" required>
              Label for audio channel participant (e.g., Doctor, patient, or multiple)
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="mode" type="object" required>
      Define facts or transcript as desired output, depending on workflow need:

      <Expandable>
        <ParamField body="type" type="string [enum]" required>
          Set as `facts` to receive structured facts output along with transcripts, or `transcription` to only receive transcript output
        </ParamField>

        <ParamField body="outputLocale" type="string [enum]" required>
          Output language for extracted `facts` (required for `type: "facts"`). See supported languages codes and more information [here](/about/languages). (Note: This may be different than the `primaryLanguage` defined for transcript output; see details [here](/textgen/facts_realtime).)
        </ParamField>

        <ParamField body="factGenerationInterval" type="string [enum]">
          <Badge className="accent-badge" shape="rounded">Beta</Badge> Rate at which fact generation should process and return results (optional for `type: "facts"`). Possible values: (empty), `fixed`, `fast_init`. If no value is set, the default is `fixed` and will trigger fact generation at the standard interval of around 60s. With `fast_init`, fact generation will follow a logarithmic curve, currently with the initial generation at roughly around 10s, then 20s, then 26s and continuously increasing interval length until the default 60s interval is reached.

          Note: Increased fact processing rate can result in increased (near) duplicates, quantities and increased credit consumption.
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField path="retentionPolicy" type="enum<string>" default="retain">
      With the optional parameter `retentionPolicy:none` the API will generate and return the transcripts and facts as expected, but the generated output will not be saved to the database. If the configuration option is omitted, then the default retention policy will apply and the output will be stored in the database.

      Available options: `none`, `retain`
    </ParamField>

    <ParamField body="audioFormat" type="string">
      Define the audio format of the incoming audio stream - optional but recommended.

      * When omitted, the server auto-detects the format from the first audio chunk using ffprobe. Supported audio will be processed. Unsupported audio returns an error, but in some cases might error silently.
      * If provided (recommended), the provided MIME type must be supported and the audio must match the MIME type. An unsupported MIME type results in `CONFIG_REJECTED`. Audio that differs from the MIME type will return audio validation errors on the socket.

      See more about supported formats [here](/stt/audio).
    </ParamField>

    <ParamField body="audioEvents" type="object">
      When true, enables audio quality and speech activity events to be sent over the WebSocket. Disabled by default.

      <Expandable>
        <ParamField body="enabled" type="boolean" required>
          The following events are supported:

          * Speech quality issue detected / recovered
          * Long silence detected / recovered
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

#### Example

<CodeGroup>
  ```json title="Raw message" theme={null}
  {
    "type": "config",
    "configuration": {
      "transcription": {
        "primaryLanguage": "en",
        "isDiarization": false,
        "isMultichannel": false,
        "participants": [
          {
            "channel": 0,
            "role": "multiple"
          }
        ]
      },
      "mode": {
        "type": "facts",
        "outputLocale": "en"
      },
      "retentionPolicy": "retain",
      "audioFormat": "audio/ogg",
      "audioEvents":{
        "enabled": true
      }
    }
  }
  ```

  ```ts title="JavaScript" theme={null}
  const configuration = {
    transcription: {
      primaryLanguage: "en",
      isDiarization: false,
      isMultichannel: false,
      participants: [
        {
          channel: 0,
          role: "multiple"
        }
      ]
    },
    mode: {
      type: "facts",
      outputLocale: "en"
    },
    xCortiRetentionPolicy: "retain"
  };

  // When configuration is provided, connect() resolves after CONFIG_ACCEPTED (or throws on failure).
  const streamSocket = await client.stream.connect({
    id: "<your-interaction-id>",
    configuration
  });
  ```

  {/* Configure /streams via ConnectAsync — waits for CONFIG_ACCEPTED */}

  ```csharp title="C# .NET" theme={null}
  var stream = await client.CreateStreamApiAsync("<your-interaction-id>");

  // ConnectAsync(configuration) waits for CONFIG_ACCEPTED and throws on rejection.
  await stream.ConnectAsync(new StreamConfig
  {
      Transcription = new StreamConfigTranscription
      {
          PrimaryLanguage = "en",
          IsDiarization = false,
          IsMultichannel = false,
          Participants = new List<StreamConfigParticipant>
          {
              new() { Channel = 0, Role = StreamConfigParticipantRole.Multiple },
          },
      },
      Mode = new StreamConfigMode
      {
          Type = StreamConfigModeType.Facts,
          OutputLocale = "en",
      },
      XCortiRetentionPolicy = StreamConfigXCortiRetentionPolicy.Retain,
  });
  ```
</CodeGroup>

### Sending Audio

<Info>
  Ensure that your configuration was accepted before sending audio, and that the initial audio chunk is not too small as it needs to contain the headers to properly decode the audio.

  We recommend sending audio in chunks of 250-500ms. In terms of buffering, the limit is 64000 bytes per chunk.

  Audio data should be sent as raw binary without JSON wrapping.
</Info>

A variety of common audio formats are supported; audio will be passed through a transcoder before speech-to-text processing. Similarly, specification of sample rate, depth or other audio settings is not required at this time.

See more details on supported audio formats [here](/stt/audio).

#### Channels, participants, and speakers

In most workflows, especially **in-person settings**, mono-channel audio should be used. If the microphone is a stereo-microphone, then ensure to set `isMultichannel: false` and audio will be converted to mono-channel, preventing duplicate transcripts from being returned.

In a telehealth workflow, or other **virtual setting**, the virtual audio may be on one channel (e.g., from webRTC) with audio from the microphone of the local client on a separate channel. In this scenario, define `isMultichannel: true` and assign each channel the relevant participant role (e.g., if the doctor is on the local client, then set that to channel 0 with participant defined as `doctor` and the virtual audio for patient on channel `defined as participant`patient\`).

<Info>
  **Diarization** is independent of audio channels and participant roles as it enables speaker separation for mono audio.

  With configuration `isDiarization: true`, transcript segments will be assigned to automatically with first speaker identified being channel 0, second on channel 1, etc. If `isDiarization:false`, then transcript segments will all be assigned with `speakerId: -1`.

  Read more [here](/stt/diarization).
</Info>

<CodeGroup>
  ```ts title="JavaScript" theme={null}
  streamSocket.sendAudio(chunk); // method doesn't do the chunking
  ```

  {/* Send binary audio over a stream socket */}

  ```csharp title="C# .NET" theme={null}
  await stream.Send(audioChunkBytes); // method doesn't do the chunking
  ```
</CodeGroup>

### Flush the Audio Buffer

To flush the audio buffer, forcing transcript segments to be returned over the web socket (e.g., when turning off or muting the microphone for the patient to share something private, not to be recorded, during the conversation), send a message -

<CodeGroup>
  ```json title="Raw message" theme={null}
  {
    "type": "flush"
  }
  ```

  ```ts title="JavaScript" theme={null}
  streamSocket.sendFlush({ type: "flush" });
  ```

  {/* Send flush request to force processing of buffered audio */}

  ```csharp title="C# .NET" theme={null}
  await stream.Send(new StreamFlushMessage());
  ```
</CodeGroup>

The server will return text for audio sent before the `flush` message and then respond with messages -

```json theme={null}
{
  "type": "flushed"
}
```

```json theme={null}
{
  "type": "delta_usage",
  "credits": 0.00116
}
```

`Delta usage` represents incremental credit consumption between recording initiation and `flush` events. Delta usage is approximate and may differ slightly from final `usage` sent after `end` message is processed (see [below](/api-reference/streams#usage)). Final, end session usage will be reflected in API billing.

The web socket will remain open after `flush` processing so recording can continue.

<Badge className="accent-badge" shape="rounded">Beta</Badge> FactsR generation (i.e., when working in `configuration.mode: facts`) will be triggered upon `flush` event. Facts will only be returned if the sliding transcript window, including the forced transcript segment after `flush`, contains relevant new information for FactsR.

Note: Too frequent triggering of `flush` can negatively impact FactsR, e.g. partial transcripts, increased (near) duplicates or fact quantities, and credits usage can increase.

<Tip>
  Client side considerations:

  <sup>1</sup> If you rely on a `flush` event to separate data (e.g., for different sections in an EHR template), then be sure to receive the `flushed` event before moving on to the next data field.

  <sup>2</sup> When using a web browser `MediaRecorder` API, audio is buffered and only emitted at the configured timeslice interval. Therefore, *before* sending a `flush` message, call `MediaRecorder.requestData()` to force any remaining buffered audio on the client to be transmitted to the server. This ensures all audio reaches the server before the `flush` is processed.
</Tip>

### Ending the Session

To end the `/streams` session, send a message -

<CodeGroup>
  ```json title="Raw message" theme={null}
  {
    "type": "end"
  }
  ```

  ```ts title="JavaScript" theme={null}
  streamSocket.sendEnd({ type: "end" });
  ```

  {/* Send end message to signal no more audio */}

  ```csharp title="C# .NET" theme={null}
  await stream.Send(new StreamEndMessage());
  ```
</CodeGroup>

This will signal the server to send any remaining transcript segments and facts (depending on `mode` configuration). Then, the server will send two messages -

```json theme={null}
{
  "type": "usage",
  "credits":0.1
}
```

```json theme={null}
{
  "type": "ENDED"
}
```

Following the message type `ENDED`, the server will close the web socket.

<Info>You can at any time open the WebSocket again by sending the configuration.</Info>

***

## 4. Responses

### Configuration

<ResponseField name="type" type="string" required default="CONFIG_ACCEPTED">
  Returned when sending a valid configuration. Response body will include the full configuration object to confirm values applied in the configuration.
</ResponseField>

<ResponseField name="sessionId" type="uuid" required>
  Returned when sending a valid configuration.
</ResponseField>

<ResponseField name="configuration" type="object">
  The resolved configuration, including accepted client-defined values and server-applied defaults for parameters not defined in client configuration.
</ResponseField>

### Transcripts

<ResponseField name="type" type="string" required default="transcript" />

<ResponseField name="data" type="object" required>
  <Expandable>
    <ResponseField name="id" type="string(UUID)" required>
      Interaction ID that the transcript segments are associated with
    </ResponseField>

    <ResponseField name="transcript" type="string" required>
      Transcript text segments
    </ResponseField>

    <ResponseField name="time" type="object">
      <Expandable>
        <ResponseField name="start" type="float64">
          Start time of the transcript segment in seconds
        </ResponseField>

        <ResponseField name="end" type="float64">
          End time of the transcript segment in seconds
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="final" type="bool">
      Indicates whether the transcript text results are final or interim (Note: only final transcripts are supported in Streams workflows)
    </ResponseField>

    <ResponseField name="speakerId" type="integer">
      Speaker identification (Note: value of `-1` is returned when diarization is disabled)
    </ResponseField>

    <ResponseField name="participant" type="object">
      <Expandable>
        <ResponseField name="channel" type="integer">
          Audio channel number (e.g., 0 or 1)
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

```json Transcript response theme={null}
{
  "type": "transcript",
  "data": [
    {
      "id": "UUID",
      "transcript": "Patient presents with fever and cough.",
      "time": { "start": 1.71, "end": 11.296 },
      "final": true,
      "speakerId": -1,
      "participant": { "channel": 0 }
    }
  ]
}
```

<Note>
  Transcript output will automatically apply server-side default values documented [here](/stt/formatting). Configuration of output formatting is not supported on the `streams` endpoint at this time as it is with `transcribe`.
</Note>

### Facts

<ResponseField name="type" type="string" required default="facts" />

<ResponseField name="facts" type="object" required>
  <Expandable>
    <ResponseField name="id" type="string (UUID)" required>
      Unique identifier for the fact
    </ResponseField>

    <ResponseField name="text" type="string" required>
      Text description of the fact
    </ResponseField>

    <ResponseField name="group" type="string" required>
      Categorization of the fact (e.g., "medical-history")
    </ResponseField>

    <ResponseField name="groupId" type="string (UUID)" required deprecated="true">
      Deprecated. Response still includes groupID but will always be empty `""`
    </ResponseField>

    <ResponseField name="isDiscarded" type="bool">
      Default response is `false`. Use [PATCH /facts](/api-reference/facts/update-facts) to set to `true` if you want to keep track that a clinician discarded a fact.
    </ResponseField>

    <ResponseField name="source" type="string">
      Indicates the source of the fact (e.g., "core", "user"). Default response for LLM-generated is `core`. Use [PATCH /facts](/api-reference/facts/update-facts) if you want to keep track that a clinician edited or added a fact.
    </ResponseField>

    <ResponseField name="createdAt" type="string (date-time)">
      Timestamp when the fact was created
    </ResponseField>

    <ResponseField name="updatedAt" type="string (date-time) or null">
      Timestamp when the fact was last updated via [PATCH /facts](/api-reference/facts/update-facts). Default response equals `createdAt`.
    </ResponseField>
  </Expandable>
</ResponseField>

```json Fact response theme={null}
{
  "type": "facts",
  "fact": [
    {
      "id": "UUID",
      "text": "Patient has a history of hypertension.",
      "group": "medical-history",
      "groupId": "" // deprecated,
      "isDiscarded": false,
      "source": "core",
      "createdAt": "2024-02-28T12:34:56Z",
      "updatedAt": ""
    }
  ]
}
```

<Info>
  By default, incoming audio and returned data streams are persisted on the server, associated with the interactionId. You may query the interaction to retrieve the stored `recordings`, `transcripts`, and `facts` via the relevant REST endpoints. Audio recordings are saved as .webm format; transcripts and facts as json objects.

  Data persistence can be disabled by Corti upon request when needed to support compliance with your applicable regulations and data handling preferences.
</Info>

<CodeGroup>
  {/* Handle all incoming stream message types */}

  ```ts expandable theme={null}
  socket.on("message", (msg) => {
      switch (msg.type) {
          case "transcript":
              console.log("Transcript:", msg.data);
              break;
          case "facts":
              console.log("Facts:", msg.fact);
              break;
          case "flushed":
              console.log("Flush complete");
              break;
          case "ENDED":
              console.log("Stream ended");
              socket.close();
              break;
          case "usage":
              console.log("Credits used:", msg.credits);
              break;
          case "error":
              console.error("Server error:", msg.error);
              break;
      }
  });
  ```

  {/* Subscribe to all stream message types — register before ConnectAsync */}

  ```csharp title="C# .NET" expandable theme={null}
  stream.StreamTranscriptMessage.Subscribe(message =>
  {
      Console.WriteLine($"Transcript: {message.Data.Text}");
  });

  stream.StreamFactsMessage.Subscribe(message =>
  {
      Console.WriteLine("Facts received");
  });

  stream.StreamFlushedMessage.Subscribe(_ =>
  {
      Console.WriteLine("Flush complete");
  });

  stream.StreamEndedMessage.Subscribe(_ =>
  {
      Console.WriteLine("Stream ended");
  });

  stream.StreamUsageMessage.Subscribe(message =>
  {
      Console.WriteLine($"Credits used: {message.Credits}");
  });

  stream.StreamErrorMessage.Subscribe(message =>
  {
      Console.Error.WriteLine($"Server error: {message.Error.Title} ({message.Error.Status})");
  });
  ```
</CodeGroup>

### Audio Events

<ResponseField name="type" default="audioEvent" type="string">Server message indicating an audio quality or speech activity event</ResponseField>

<ResponseField name="data" type="object">
  <Expandable>
    <ResponseField name="event" type="string" required>
      The type of audio quality or speech activity event.

      Possible values: `speechQualityIssueDetected`, `speechQualityIssueRecovered`, `longSilenceDetected`, `longSilenceRecovered`
    </ResponseField>

    <ResponseField name="channel" type="integer" required>
      Audio channel identifier
    </ResponseField>

    <ResponseField name="startTimeMs" type="integer" required>
      Start time of the event in milliseconds
    </ResponseField>
  </Expandable>
</ResponseField>

```json Audio Event response theme={null}
{
  "type": "audioEvent",
  "data": {
    "event": "speechQualityIssueDetected",
    "channel": 0,
    "startTimeMs": 4200
  }
}
```

### Flushed

<ResponseField name="type" type="string" required default="flushed">
  Returned by server, after processing `flush` event from client, to return transcript segments
</ResponseField>

```json theme={null}
{
  "type":"flushed"
}
```

### Usage

<ResponseField name="type" type="string" default="delta-usage">
  Returned by server, after processing `flush` event from client, to convey amount of credits consumed since recording started. Delta usage is approximate and may differ slightly from final `usage` sent after `end` message is processed.
</ResponseField>

```json theme={null}
{
  "type": "delta_usage",
  "credits": 0.00116
}
```

<ResponseField name="type" type="string" default="usage">
  Returned by server, after processing `end` event from client, to convey amount of credits consumed
</ResponseField>

```json theme={null}
{
  "type":"usage",
  "credits":0.1
}
```

### Ended

<ResponseField name="type" type="string" required default="ENDED">
  Returned by server, after processing `end` event from client, before closing the web socket
</ResponseField>

```json theme={null}
{
  "type":"ENDED"
}
```

***

## 5. Error Handling

In case of an invalid or missing interaction ID, the server will return an error before opening the WebSocket.

In case of an invalid configuration, the server will return one of the following errors:

<ResponseField name="type" type="string" required>
  Returned when sending an invalid configuration.

  Possible errors: `CONFIG_DENIED`, `CONFIG_NOT_PROVIDED`, `CONFIG_ALREADY_RECEIVED`, `CONFIG_MISSING`
</ResponseField>

<ResponseField name="reason" type="string">
  The reason the configuration is invalid.
</ResponseField>

<ResponseField name="interactionId" type="uuid" required>
  The interaction ID.
</ResponseField>

Once configuration has been accepted and the session is running, you may encounter runtime or application-level errors. These are sent as JSON objects with the following structure:

```json theme={null}
{
  "type": "error",
  "error": {
    "id": "error id",
    "title": "error title",
    "status": 400,
    "details": "error details",
    "doc":"link to documentation"
  }
}
```

<CodeGroup>
  {/* Stream error handling: connect rejection and runtime errors */}

  ```ts expandable theme={null}
  try {
      const socket = await client.stream.connect({
          id: interactionId,
          configuration: {
              transcription: { primaryLanguage: "en", participants: [{ channel: 0, role: "doctor" }] },
              mode: { type: "facts", outputLocale: "en" },
          },
      });

      socket.on("error", (err) => {
          // Network errors and reconnect failures
          console.error("Socket error:", err.message);
      });

      socket.on("message", (msg) => {
          if (msg.type === "error") {
              // Server-sent runtime error (e.g. audio format issue)
              console.error("Server error:", msg.error);
          }
      });
  } catch (err) {
      // CONFIG_DENIED, CONFIG_TIMEOUT, CONFIG_MISSING, or connection failure
      console.error("Connect failed:", err.message);
  }
  ```

  {/* Stream error handling: subscribe before connecting, catch ConnectAsync rejection */}

  ```csharp title="C# .NET" expandable theme={null}
  var stream = await client.CreateStreamApiAsync(interactionId);

  stream.ExceptionOccurred.Subscribe(ex =>
  {
      // Network errors, reconnect failures
      Console.Error.WriteLine($"Socket error: {ex.Message}");
  });

  stream.StreamErrorMessage.Subscribe(message =>
  {
      // Server-sent runtime error (e.g. audio format issue)
      Console.Error.WriteLine($"Server error: {message.Error.Title} ({message.Error.Status})");
  });

  try
  {
      // ConnectAsync waits for CONFIG_ACCEPTED and throws if configuration is denied
      await stream.ConnectAsync(new StreamConfig
      {
          Transcription = new StreamConfigTranscription
          {
              PrimaryLanguage = "en",
              Participants = new[] { new StreamConfigParticipant { Channel = 0, Role = StreamConfigParticipantRole.Doctor } },
          },
          Mode = new StreamConfigMode { Type = StreamConfigModeType.Facts, OutputLocale = "en" },
      });
  }
  catch (InvalidOperationException ex)
  {
      // CONFIG_DENIED, CONFIG_TIMEOUT, CONFIG_MISSING, or connection failure
      Console.Error.WriteLine($"Connect failed: {ex.Message}");
      throw;
  }
  ```
</CodeGroup>
