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

# C# .NET SDK - WebSocket Guide

> Connect to Stream and Transcribe APIs, handle real-time events, and manage session lifecycle

The SDK wraps both WebSocket APIs — [Stream](/api-reference/streams) and [Transcribe](/api-reference/transcribe) — with a consistent async/event-driven interface: factory methods to create connections, `ConnectAsync()` to open and configure, and typed `Event<T>` fields to subscribe to messages.

## Connecting

Both WebSocket APIs require a handshake before audio can flow. After the connection opens, the SDK sends the configuration automatically and waits for the server to respond with `CONFIG_ACCEPTED`. Only then does `ConnectAsync()` return. If configuration is rejected, `ConnectAsync()` throws.

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

    stream.StreamTranscriptMessage.Subscribe(message =>
    {
        Console.WriteLine($"Transcript: {message.Data.Text}");
    });

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

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

    await stream.Send(audioBytes);
    ```
  </Tab>

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

    transcribe.TranscribeTranscriptMessage.Subscribe(message =>
    {
        Console.WriteLine($"Transcript: {message.Data.Text}");
    });

    await transcribe.ConnectAsync(new TranscribeConfig
    {
        PrimaryLanguage = "en",
        AutomaticPunctuation = true,
    });

    // Send audio data (e.g. from a microphone stream)
    await transcribe.Send(audioBytes);
    ```
  </Tab>
</Tabs>

<Warning>
  Subscribe to events **before** calling `ConnectAsync()`. The SDK emits `Connected` and potentially early message events as soon as the WebSocket opens — subscribing after `ConnectAsync()` returns means those events are already gone.
</Warning>

### Connecting without configuration

If you want to manage the handshake manually and only use the SDK for **types** and **reconnection**, omit `configuration` from `ConnectAsync()`. The socket opens without waiting for `CONFIG_ACCEPTED` — you are responsible for sending the config message yourself and waiting for `StreamConfigStatusMessage` before sending audio.

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

stream.StreamConfigStatusMessage.Subscribe(msg =>
{
    if (msg.Status == StreamConfigStatusMessageStatus.ConfigAccepted)
        Console.WriteLine("Configuration accepted — ready to send audio");
    else
        Console.WriteLine($"Configuration denied: {msg.Status}");
});

// Connect without configuration — does not wait for CONFIG_ACCEPTED
await stream.ConnectAsync();

// Send configuration manually
await stream.Send(new StreamConfigMessage
{
    Configuration = new StreamConfig
    {
        Transcription = new StreamConfigTranscription
        {
            PrimaryLanguage = "en",
            Participants = new[]
            {
                new StreamConfigParticipant { Channel = 0, Role = StreamConfigParticipantRole.Doctor },
            },
        },
        Mode = new StreamConfigMode { Type = StreamConfigModeType.Facts, OutputLocale = "en" },
    },
});
```

***

## Sending audio

Both APIs accept raw audio bytes:

```csharp theme={null}
await stream.Send(audioBytes); // byte[]
```

<Warning>
  The SDK does not chunk audio for you. Send chunks at your own cadence — 100–250 ms per chunk is typical.
</Warning>

***

## Full example

<Tabs>
  <Tab title="Stream">
    ```csharp title="C# .NET" expandable theme={null}
    using Corti;

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

    var client = new CortiClient(
        auth: CortiClientAuth.Bearer(accessToken: ACCESS_TOKEN)
    );

    // Interaction must be created via REST before opening a stream
    const string interactionId = INTERACTION_ID;

    var stream = await client.CreateStreamApiAsync(interactionId);

    // Register handlers before connecting
    stream.StreamTranscriptMessage.Subscribe(msg =>
    {
        foreach (var seg in msg.Data)
            Console.WriteLine($"🗣  [{seg.Time.Start}s → {seg.Time.End}s] {seg.Transcript}");
    });

    stream.StreamFactsMessage.Subscribe(msg =>
    {
        foreach (var fact in msg.Fact)
            Console.WriteLine($"💡 Fact [{fact.Group}]: {fact.Text}");
    });

    stream.StreamFlushedMessage.Subscribe(_ =>
        Console.WriteLine("🔄 Buffer flushed"));

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

    stream.StreamEndedMessage.Subscribe(_ =>
        // Server closes the connection after sending "ENDED" — no need to close manually
        Console.WriteLine("🏁 Session ended — server closing socket"));

    stream.StreamErrorMessage.Subscribe(msg =>
        Console.Error.WriteLine($"❌ Server error: {msg.Error.Title}"));

    stream.ExceptionOccurred.Subscribe(ex =>
        Console.Error.WriteLine($"🚨 Connection error: {ex.Message}"));

    stream.Closed.Subscribe(info =>
        Console.WriteLine($"🔌 Connection closed [{info.Code}]: {info.Reason}"));

    try
    {
        // Step 1: Connect and send config — ConnectAsync waits for CONFIG_ACCEPTED before returning
        await stream.ConnectAsync(new StreamConfig
        {
            Transcription = new StreamConfigTranscription
            {
                PrimaryLanguage = "en",
                IsDiarization = false,
                IsMultichannel = false,
                Participants = new[]
                {
                    new StreamConfigParticipant { Channel = 0, Role = StreamConfigParticipantRole.Multiple },
                },
            },
            Mode = new StreamConfigMode
            {
                Type = StreamConfigModeType.Facts, // or StreamConfigModeType.Transcription
                OutputLocale = "en",
            },
        });

        Console.WriteLine("✅ Connected — session ready");

        // Step 2: Start sending audio now that config is accepted
        const string audioFile = "./sample.webm"; // swap with your audio file path
        const int chunkSize = 8192; // ~250–500ms chunks recommended

        if (!File.Exists(audioFile))
        {
            Console.WriteLine("⚠️  No audio file found — sending silence simulation");
            await Task.Delay(2000);
            await stream.Send(new StreamEndMessage());
        }
        else
        {
            var audioBytes = await File.ReadAllBytesAsync(audioFile);

            Console.WriteLine($"🎙  Streaming {audioBytes.Length} bytes of audio...");

            for (int i = 0; i < audioBytes.Length; i += chunkSize)
            {
                var chunk = audioBytes.AsMemory(i, Math.Min(chunkSize, audioBytes.Length - i));
                await stream.Send(chunk.ToArray());
                await Task.Delay(300); // send a chunk every 300ms
            }

            Console.WriteLine("✅ All audio sent");

            // Signal end of audio stream
            await stream.Send(new StreamEndMessage());
            Console.WriteLine("📤 Sent end — waiting for ENDED...");
        }
    }
    catch (Exception ex)
    {
        // CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
        Console.Error.WriteLine($"❌ Failed to connect: {ex.Message}");
        throw;
    }
    ```
  </Tab>

  <Tab title="Transcribe">
    ```csharp title="C# .NET" expandable 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)
    );

    var transcribe = await client.CreateTranscribeApiAsync();

    // Register handlers before connecting
    transcribe.TranscribeTranscriptMessage.Subscribe(msg =>
    {
        if (msg.Data.IsFinal)
            Console.WriteLine($"🗣  [{msg.Data.Start}s → {msg.Data.End}s] {msg.Data.Text}");
        else
            Console.WriteLine($"💬 Interim: {msg.Data.Text}");
    });

    transcribe.TranscribeCommandMessage.Subscribe(msg =>
        Console.WriteLine($"🎙  Command detected [{msg.Data.Id}]: {msg.Data.Variables}"));

    transcribe.TranscribeFlushedMessage.Subscribe(_ =>
        Console.WriteLine("🔄 Buffer flushed"));

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

    transcribe.TranscribeEndedMessage.Subscribe(_ =>
        // Server closes the connection after sending "ended" — no need to close manually
        Console.WriteLine("🏁 Session ended — server closing socket"));

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

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

    transcribe.Closed.Subscribe(info =>
        Console.WriteLine($"🔌 Connection closed [{info.Code}]: {info.Reason}"));

    try
    {
        // Step 1: Connect and send config — ConnectAsync waits for CONFIG_ACCEPTED before returning
        await transcribe.ConnectAsync(new TranscribeConfig
        {
            PrimaryLanguage = "en",
            AutomaticPunctuation = true,
            Formatting = new TranscribeFormatting
            {
                Numbers = TranscribeFormattingNumbers.NumeralsAboveNine,
                Measurements = TranscribeFormattingMeasurements.Abbreviated,
            },
        });

        Console.WriteLine("✅ Connected — session ready");

        // Step 2: Start sending audio now that config is accepted
        const string audioFile = "./sample.webm"; // swap with your audio file path
        const int chunkSize = 8192; // ~250–500ms per chunk

        if (!File.Exists(audioFile))
        {
            Console.WriteLine("⚠️  No audio file found — sending silence simulation");
            await transcribe.Send(new byte[chunkSize]);
            await transcribe.Send(new TranscribeEndMessage());
        }
        else
        {
            var audioBytes = await File.ReadAllBytesAsync(audioFile);

            for (int i = 0; i < audioBytes.Length; i += chunkSize)
            {
                var chunk = audioBytes.AsMemory(i, Math.Min(chunkSize, audioBytes.Length - i));
                await transcribe.Send(chunk.ToArray());
            }

            // Signal end of audio stream
            await transcribe.Send(new TranscribeEndMessage());
            Console.WriteLine("📤 Audio sent — end signal dispatched");
        }
    }
    catch (Exception ex)
    {
        // CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
        Console.Error.WriteLine($"❌ Failed to connect: {ex.Message}");
        throw;
    }
    ```
  </Tab>
</Tabs>

***

## Next steps (sending messages, lifecycle)

After the socket is **OPEN** and configuration has been **accepted**, use the SDK's methods to send audio, flush, end, and subscribe to typed message events.

* **SDK method reference**: See socket event types and method signatures in the C# .NET SDK reference.
  * [Stream `ConnectAsync()` + event types](/sdk/dotnet/reference#stream)
  * [Transcribe `ConnectAsync()` + event types](/sdk/dotnet/reference#transcribe)
* **Protocol behavior & message semantics**: For the underlying wire format and what each message means, refer to the WebSocket API docs.
  * [Stream WSS API reference](/api-reference/streams)
  * [Transcribe WSS API reference](/api-reference/transcribe)

***

## Resources

* **[Stream API reference](/api-reference/streams)** — WebSocket protocol details and message schemas
* **[Transcribe API reference](/api-reference/transcribe)** — Transcribe protocol and configuration options
* **[Authentication Guide](/sdk/dotnet/authentication)** — All auth flows including scoped tokens for WebSocket

***

<Note>For support or questions, reach out through [help.corti.app](https://help.corti.app)</Note>
