> ## 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 stateless dictation

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

## Overview

The WebSocket Secure (WSS) `/transcribe` API enables real-time, bidirectional communication with the Corti system for stateless speech to text. Clients can send and receive structured data, including transcripts and detected commands.

This documentation provides a comprehensive guide for integrating these capabilities.

<Info>
  This `/transcribe` endpoint supports real-time stateless dictation.

  * If you are looking for real-time ambient documentation interactions, you should use the [/streams WSS](/api-reference/streams)
  * If you are looking for transcript generation based on a pre-recorded audio file, then please refer to the [/transcripts endpoint](/api-reference/transcripts/create-transcript)
</Info>

***

## 1. Establishing a Connection

Clients must initiate a WebSocket connection using the `wss://` scheme.

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

### 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} theme={null} 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,
    },
  });
  ```

  ```csharp title="C# .NET" theme={null} theme={null} 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>
  ```bash title="cURL" theme={null} theme={null} theme={null}
  # Replace these with your values
  ACCESS_TOKEN="<your-access-token>"
  ENVIRONMENT="<eu-or-us>"
  TENANT="<your-tenant-name>"

  curl --request GET \
    --url "wss://api.${ENVIRONMENT}.corti.app/audio-bridge/v2/transcribe?tenant-name=${TENANT}&token=Bearer%20${ACCESS_TOKEN}"
  ```

  ```ts title="JavaScript" theme={null} theme={null} theme={null}
  const transcribeSocket = await client.transcribe.connect();
  ```

  ```csharp title="C# .NET" theme={null} theme={null} theme={null}
  var transcribe = await client.CreateTranscribeApiAsync();
  await transcribe.ConnectAsync();
  ```
</CodeGroup>

***

## 2. Handshake Response

### 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 `/transcribe` configuration using the message `"type": "config"` followed by defining the `"configuration": {...}`.

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

<Note>
  Configuration Notes:

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

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

<ParamField body="configuration" type="object" required>
  <Expandable defaultOpen="true">
    <ParamField body="primaryLanguage" type="string" required>
      The locale of the primary spoken language.

      See supported languages codes and more information [here](/stt/languages).
    </ParamField>

    <ParamField body="interimResults" type="boolean">
      When true, interim (preview) transcript results (`"isFinal"=false`) will be returned with lower latency than final transcript results.
    </ParamField>

    <ParamField body="spokenPunctuation" type="boolean">
      When true, converts spoken punctuation such as `period` or `slash` into `.`or `/`.

      Read more about supported punctuation [here](/stt/punctuation).
    </ParamField>

    <ParamField body="automaticPunctuation" type="boolean">
      When true, automatically punctuates and capitalizes in the final transcript.

      <Note>
        Spoken and Automatic Punctuation are mutually exclusive - only one should be set to true in a given configuration request. If both are included and set to `true`, then `spokenPunctuation` will take precedence and override `automaticPunctuation`.
      </Note>
    </ParamField>

    <ParamField body="commands" type="array">
      Define dictation commands to be registered and detected during audio recording.

      Read more about commands, with examples, [here](/stt/commands).

      <Expandable>
        <ParamField body="id" type="string" required>
          Unique value to identify the command. This, along with the command phrase, will be returned by the API when the command is recognized during dictation.
        </ParamField>

        <ParamField body="phrases" type="string[]" required>
          One or more word sequence(s) that can be spoken to trigger the command. At least one phrase is required per command.
        </ParamField>

        <ParamField body="variables" type="array">
          Placeholders that can (optionally) be added in `phrases` to provide flexibility and extensibility for triggering commands.

          <Expandable>
            <ParamField body="key" type="string" required>
              Define the variable used in command phrase here.
            </ParamField>

            <ParamField body="type" type="enum" required>
              Possible values:

              * `enum` - use to define a list of values that can be recognized for a given command phrase
              * `wildcard` - use for any free text utterance to be recognized in a command phrase

              When using `wildcard` type, the phrase must include a literal trigger word before the variable, and multiple wildcard variables in one phrase must be separated by a non-empty literal string.
            </ParamField>

            <ParamField body="enum" type="string[]">
              Required when `type` is `"enum"`. List of values that should be recognized for the defined variable. Not used (and ignored) when `type` is `"wildcard"`.
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="formatting" type="object">
      Define preferences for output formatting using the `enum` options described below. Formatting configuration is optional, and one or more fields can be defined individually. When a property is not provided for a given field, then the values listed as `default` will be applied automatically.

      Read more about formatting options, including how localization is handled per language, and detailed examples, [here](/stt/formatting).

      <Expandable>
        <ParamField body="dates" type="enum">
          * Default option: `locale:long`
          * Alternative options: `locale:medium`, `locale:short`, `iso`, `as_dictated`
        </ParamField>

        <ParamField body="times" type="enum">
          * Default option: `locale`
          * Alternative options: `h24`, `h12`
        </ParamField>

        <ParamField body="numbers" type="enum">
          * Default option: `numerals_above_nine`
          * Alternative options: `numerals`, `as_dictated`

          <Info>Localization of numbers applied automatically for thousands and decimal separators</Info>
        </ParamField>

        <ParamField body="measurements" type="enum">
          * Default option: `abbreviated`
          * Alternative options: `as_dictated`

          <Info>[Click here](/stt/formatting#units-and-measurements) to see a full list of supported units and measurements</Info>
        </ParamField>

        <ParamField body="numericRanges" type="enum">
          * Default option: `numerals`
          * Alternative options: `as_dictated`
        </ParamField>

        <ParamField body="ordinals" type="enum">
          * Default option: `numerals_above_nine`
          * Alternative options: `numerals`, `as_dictated`
        </ParamField>
      </Expandable>
    </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 return an error but might in some cases 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">
      Enables audio quality and speech activity events to be sent over the WebSocket. The following events are supported:

      * Speech quality issue detected / recovered
      * Long silence detected / recovered

      <Expandable>
        <ParamField body="enabled" type="boolean" required>
          When true, enables audio quality and speech activity events to be sent over the WebSocket. Disabled by default.
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="replacements" type="array">
      Don't like how Corti STT outputs certain words, phrases, or acronyms? Define `replacements` to have terms (single words or multi-word phrases) replaced in final text output with your preferred style.

      <Expandable>
        <ParamField body="find" type="string" required>Speech-to-text output to be replaced (case insensitive) </ParamField>
        <ParamField body="replace" type="string" required>Replacement text to be used final transcript </ParamField>
        <Info>A limit of 1,000 items is enforced on configuration validation.</Info>
      </Expandable>
    </ParamField>

    <ParamField body="keyterms" type="object">
      Define words, terms, and phrases to be recognized by Corti speech-to-text. Especially useful for proper nouns (e.g., surnames), but also supportive of words not being recognized consistently.

      <Expandable>
        <ParamField body="terms" type="array">
          Ordered list of words to be recognized (case sensitive)

          <Expandable defaultOpen="true">
            <ParamField body="term" type="string" required>
              Word to be recognized, defined in it's expected "written form"
              <Info>A defined `term` is limited to a length of 50 characters</Info>
            </ParamField>
          </Expandable>

          <Info>A limit of 1,000 items is enforced on configuration validation</Info>
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

#### Example

Here is an example configuration for transcription of dictated audio in English with spoken punctuation enabled, two commands defined, and (default) formatting options defined:

<CodeGroup>
  ```json title="wss:/transcribe configuration example" theme={null} theme={null} theme={null}
  {
    "type": "config",
    "configuration":{
      "primaryLanguage": "en",
      "interimResults": true,
      "spokenPunctuation": true, 
      "commands": [
        {
          "id": "next_section",
          "phrases": ["next section", "go to next section"]
        },
        {
          "id": "insert_template",
          "phrases": ["insert my {template_name} template", "insert {template_name} template"],
          "variables": [
            {
              "key": "template_name",
              "type": "enum",
              "enum": ["soap", "radiology", "referral"]
            }
          ]
        },
        {
          "id": "select_text",
          "phrases": ["select {text}"],
          "variables": [
            {
              "key": "text",
              "type": "wildcard"
            }
          ]
        },
      ],
      "formatting": {
        "dates": "locale:long",
        "times": "locale",
        "numbers": "numerals_above_nine",
        "measurements": "abbreviated",
        "numericRanges": "numerals",
        "ordinals": "numerals_above_nine"
      },
      "audioFormat": "audio/ogg",
      "audioEvents":{
        "enabled": true
      },
      "replacements": [
        {
          "find": "BID",
          "replace": "twice daily"
        }
      ],
      "keyterms": {
        "terms": [
          {
            "term": "Corti"
          }
        ]
      }
    }
  }
  ```

  ```ts title="JavaScript" theme={null} theme={null} theme={null}
  const configuration = {
      primaryLanguage: "en",
      spokenPunctuation: true,
      commands: [
        {
          id: "next_section",
          phrases: ["next section", "go to next section"]
        },
        {
          id: "insert_template",
          phrases: ["insert my {template_name} template", "insert {template_name} template"],
          variables: [
            {
              key: "template_name",
              type: "enum",
              enum: ["soap", "radiology", "referral"]
            }
          ]
        },
        {
          id: "select_text",
          phrases: ["select {text}"],
          variables: [
            {
              key: "text",
              type: "wildcard"
            }
          ]
        },
      ],
      audioEvents: {
        enabled: true
      },
      replacements: [
        {
          find: "BID",
          replace: "twice daily"
        }
      ],
      keyterms: {
        terms: [{ term: "Corti" }]
      }
    };

  const transcribeSocket = await client.transcribe.connect(
    { configuration }
  );
  ```

  ```csharp title="C# .NET" theme={null} theme={null} theme={null}
  var transcribe = await client.CreateTranscribeApiAsync();

  await transcribe.ConnectAsync(new TranscribeConfig
  {
      PrimaryLanguage = "en",
      SpokenPunctuation = true,
      Commands = new List<TranscribeCommand>
      {
          new()
          {
              Id = "next_section",
              Phrases = new List<string> { "next section", "go to next section" },
          },
          new()
          {
              Id = "insert_template",
              Phrases = new List<string>
              {
                  "insert my {template_name} template",
                  "insert {template_name} template",
              },
              Variables = new List<TranscribeCommandVariable>
              {
                  new()
                  {
                      Key = "template_name",
                      Type = TranscribeCommandVariableType.Enum,
                      Enum = new List<string> { "soap", "radiology", "referral" },
                  },
              },
          },
          new()
          {
              Id = "select_text",
              Phrases = new List<string> { "select {text}" },
              Variables = new List<TranscribeCommandVariable>
              {
                  new()
                  {
                      Key = "text",
                      Type = TranscribeCommandVariableType.Wildcard,
                  },
              },
          },
      },
      AudioEvents = new TranscribeAudioEventsConfig { Enabled = true },
      Replacements = new List<TranscribeConfigReplacementsItem>
      {
          new() { Find = "BID", Replace = "twice daily" },
      },
      Keyterms = new TranscribeConfigKeyterms
      {
          Terms = new List<TranscribeConfigKeytermsTermsItem>
          {
              new() { Term = "Corti" },
          },
      },
  });
  ```
</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).

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

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

### Flush the Audio Buffer

To flush the audio buffer, forcing transcript segments and detected commands to be returned over the web socket (e.g., when turning off or muting the microphone in a "hold-to-talk" dictation workflow, or in applications that support mic "go to sleep"), send a message -

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

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

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

The server will return text/commands 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/transcribe#usage)). Final, end session usage will be reflected in API billing.

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

<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 `/transcribe` session, send a message -

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

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

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

This will signal the server to send any remaining transcript segments and/or detected commands. Then, the server will send two messages -

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

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

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

***

## 4. Responses

### Configuration

<ResponseField name="type" type="string" required default="CONFIG_ACCEPTED">
  Returned when sending a valid 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">Server message indicated recognized speech to text</ResponseField>

<ResponseField name="data" type="object" required>
  <Expandable>
    <ResponseField name="text" type="string" required>
      Transcript segment with punctuations applied and command phrases removed
    </ResponseField>

    <ResponseField name="rawTranscriptText" type="string">
      The raw transcript without spoken punctuation applied and without command phrases removed
    </ResponseField>

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

    <ResponseField name="isFinal" type="boolean" required>
      If false, then interim transcript result
    </ResponseField>
  </Expandable>
</ResponseField>

```json Transcript response  theme={null} theme={null} theme={null}
{
    "type": "transcript",
    "data": {
        "text": "patient reports mild chest pain.",
        "rawTranscriptText": "patient reports mild chest pain period",
        "start": 0.0,
        "end": 3.2,
        "isFinal": true
    }
}
```

<Callout icon="arrow-right">**[Click here](/stt/best-practices-transcribe)** for detailed guide on how to properly insert transcript segments with proper handling of whitespace, interim vs. final results, and `text` vs. `rawTranscriptText` fields. </Callout>

### Commands

<ResponseField name="type" type="string" required default="command">Server message indicating a recognized Command</ResponseField>

<ResponseField name="data" type="object" required>
  <Expandable>
    <ResponseField name="id" type="string" required>
      To identify the command when it gets detected and returned over the WebSocket
    </ResponseField>

    <ResponseField name="variables" type="string[]">
      The variables identified
    </ResponseField>

    <ResponseField name="rawTranscriptText" type="string">
      The raw transcript without spoken punctuation applied and without command phrases removed
    </ResponseField>

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

```json Command response theme={null} theme={null} theme={null}
{
  "type": "command",
  "data": {
    "id": "insert_template",
    "variables": {
      "template_name": "radiology"
    },
    "rawTranscriptText": "insert my radiology template",
    "start": 2.3,
    "end": 2.9
  }
}
```

### 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} theme={null} theme={null}
{
  "type": "audioEvent",
  "data": {
    "event": "speechQualityIssueDetected",
    "channel": 0,
    "startTimeMs": 4200
  }
}
```

### Flushed

<ResponseField name="type" type="string" default="flushed">
  Returned by server, after processing `flush` event from client, to return transcript segments/ detected commands
</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} theme={null} 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"
}
```

### Subscribe to messages in SDK

<CodeGroup>
  ```ts expandable theme={null} theme={null} theme={null}
  socket.on("message", (msg) => {
      switch (msg.type) {
          case "transcript":
              console.log("Transcript:", msg.data.text, "final:", msg.data.isFinal);
              break;
          case "command":
              console.log("Command:", msg.data.id, msg.data.variables);
              break;
          case "flushed":
              console.log("Flush complete");
              break;
          case "ended":
              console.log("Session ended");
              socket.close();
              break;
          case "usage":
              console.log("Credits used:", msg.credits);
              break;
          case "error":
              console.error("Server error:", msg.error);
              break;
      }
  });
  ```

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

  transcribe.TranscribeCommandMessage.Subscribe(message =>
  {
      Console.WriteLine($"Command: {message.Data.Id}");
  });

  transcribe.TranscribeFlushedMessage.Subscribe(_ =>
  {
      Console.WriteLine("Flush complete");
  });

  transcribe.TranscribeEndedMessage.Subscribe(_ =>
  {
      Console.WriteLine("Session ended");
  });

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

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

***

## 5. Error Handling

For the full catalog of error codes, real `reason` strings, and SDK / REST-specific behavior, see the [Errors reference](/api-reference/errors).

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

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

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

<ResponseField name="sessionId" type="uuid" required>
  The session 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": {
    "requestid": "<uuid>",
    "details": "error details"
  }
}
```

### Handle errors in SDK

With the recommended approach (passing configuration as part of `connect`), configuration errors are raised during `connect` (the call fails). Runtime errors are emitted via the <code>error</code> event; you can also inspect the original message in the <code>message</code> handler.

<CodeGroup>
  ```ts title="JavaScript" expandable theme={null} theme={null} theme={null}
  try {
      const socket = await client.transcribe.connect({
          configuration: {
              primaryLanguage: "en",
              automaticPunctuation: true,
          },
      });

      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
              console.error("Server error:", msg.error);
          }
      });
  } catch (err) {
      // CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
      console.error("Connect failed:", err.message);
  }
  ```

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

  transcribe.ExceptionOccurred.Subscribe(ex =>
  {
      Console.Error.WriteLine($"Socket error: {ex.Message}");
  });

  transcribe.TranscribeErrorMessage.Subscribe(message =>
  {
      Console.Error.WriteLine($"Server error: {message.Error.Title} ({message.Error.Status})");
  });

  try
  {
      await transcribe.ConnectAsync(new TranscribeConfig
      {
          PrimaryLanguage = "en",
          AutomaticPunctuation = true,
      });
  }
  catch (InvalidOperationException ex)
  {
      // CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
      Console.Error.WriteLine($"Connect failed: {ex.Message}");
      throw;
  }
  ```
</CodeGroup>
