Skip to main content
The SDK wraps both WebSocket APIs — Stream and 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.
C# .NET
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);
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.

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.
C# .NET
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:
await stream.Send(audioBytes); // byte[]
The SDK does not chunk audio for you. Send chunks at your own cadence — 100–250 ms per chunk is typical.

Full example

C# .NET
using Corti;

var client = new CortiClient(
    new CortiClientAuth.Bearer("YOUR_ACCESS_TOKEN")
);

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

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

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.

Resources


For support or questions, reach out through help.corti.app