Build VoIP servers and softphones in C#.
One SDK for SIP registration, call control, RTP media, NAT traversal and IVR prompts. Start with a twenty-line client, grow into a registrar with carrier trunks, or ship your own branded softphone.
using Microsoft.Extensions.DependencyInjection;
using VoipSdk;
using VoipSdk.Audio.Windows;
using VoipSdk.Sip.Sipsorcery;
var sipPassword = Environment.GetEnvironmentVariable("SIP_PASSWORD")!;
var services = new ServiceCollection();
services.AddVoipSdk(new VoipClientOptions
{
Accounts = [new SipAccountOptions
{
Id = "work", Domain = "pbx.example.net",
Username = "1001", Password = sipPassword
}]
}).AddSipsorcery().AddWindowsAudio();
await using var provider = services.BuildServiceProvider();
var client = provider.GetRequiredService<IVoipClient>();
client.IncomingCall += async (_, e) => await e.Call.AcceptAsync();
await client.StartAsync(); // register
var call = await client.DialAsync("1002"); // send INVITEWhatever you are building, the hard parts are done.
SIP state machines, RTP timing, codec negotiation and NAT are where VoIP projects stall. VoipSDK gives you tested building blocks so you can focus on your product.
VoIP servers and IVRs
Host a SIP registrar, route internal calls, connect carrier trunks and build IVR flows with shared, cached audio prompts.
- Digest-authenticated registrar and call routing
- Registered provider trunks, in and out
- Prompt playback with DTMF barge-in
- HTTP and live-event control API
Softphones and desktop apps
A headless phone engine, ready-made WinUI 3 controls and a reference app you can rebrand and ship as your own.
- Call, hold, transfer, three-way conference
- Recording and mid-call device switching
- Runs in the tray, rings when closed
- White-label with one branding file
A complete calling stack, not a demo.
Everything a production phone or PBX needs, behind one clear, asynchronous API.
Call control
Answer, decline, busy, hold, mute, DTMF (RFC 4733 and SIP INFO), blind and attended REFER transfers.
Media and codecs
RTP/RTCP with Opus, G.722, G.711 A-law and ยต-law, G.729, GSM and L16, offered in your priority order.
NAT traversal
Opt-in STUN mapping, or ICE with STUN and TURN candidates, including relay-only for locked-down networks.
Secure calling
TLS signalling and SDES-SRTP media encryption, optional or required per deployment.
IVR building blocks
An audio cache bank decodes each prompt once and shares it across every call, with looping and barge-in.
Call quality
Live codec, jitter, packet loss and round-trip time per call, plus a simple quality rating.
Clean C#, from your first call to your first server.
An async API with typed events and per-call commands that fits naturally into dependency injection. Swap adapters for audio and history without touching your call logic.
- Dependency injection first:
AddVoipSdk().AddSipsorcery().AddWindowsAudio() - Pluggable ports: bring your own audio device or call-history store.
- Documented public API: XML docs throughout, guarded by an API compatibility test.
- Testable: an embeddable SIP server for real end-to-end calls in your test suite.
using Microsoft.Extensions.DependencyInjection;
using VoipSdk;
using VoipSdk.Audio.Windows;
using VoipSdk.Sip.Sipsorcery;
var sipPassword = Environment.GetEnvironmentVariable("SIP_PASSWORD")!;
var services = new ServiceCollection();
services.AddVoipSdk(new VoipClientOptions
{
Accounts = [new SipAccountOptions
{
Id = "work", Domain = "pbx.example.net",
Username = "1001", Password = sipPassword
}]
}).AddSipsorcery().AddWindowsAudio();
await using var provider = services.BuildServiceProvider();
var client = provider.GetRequiredService<IVoipClient>();
client.IncomingCall += async (_, e) => await e.Call.AcceptAsync();
await client.StartAsync(); // register
var call = await client.DialAsync("1002"); // send INVITE// Decoded once, shared by every call, encoded once per codec.
await client.AudioCache.PreloadAsync(
Directory.EnumerateFiles("prompts", "*.wav"));
client.IncomingCall += async (_, e) =>
{
var call = e.Call;
await call.AcceptAsync();
var result = await call.PlayAsync("prompts/welcome.wav",
new PlaybackOptions { StopOnDtmf = true }); // barge-in
if (result.Outcome == PlaybackOutcome.Dtmf && result.Digit == '1')
await call.PlayAsync("prompts/hold-music.wav",
new PlaybackOptions { Loop = true });
};using VoipSdk.Server;
await using var server = new VoipSipServer(new SipServerOptions
{
BindAddress = "0.0.0.0", Port = 5060,
Domain = "pbx.example.net",
Accounts =
[
new() { Username = "101", Password = Secret("EXT_101") },
new() { Username = "102", Password = Secret("EXT_102") }
],
Trunks =
[
new SipTrunkOptions
{
Id = "carrier", Name = "Carrier",
ServerAddress = "sip.carrier.example",
Username = "trunk-user", Password = Secret("TRUNK"),
IncomingExtension = "101", OutboundPrefix = "9"
}
]
});
await server.StartAsync();
Console.WriteLine($"Listening on {server.BoundEndpoint}");
static string Secret(string name) =>
Environment.GetEnvironmentVariable(name)!;using VoipSdk.Softphone;
using VoipSdk.Softphone.ViewModels;
Action<Action> ui = action =>
dispatcherQueue.TryEnqueue(() => action());
var engine = new SoftphoneEngine(new SoftphoneEngineOptions
{
Dispatch = ui, // state changes arrive on your UI thread
Branding = SoftphoneBranding.LoadFrom(AppContext.BaseDirectory)
});
var phone = new PhoneViewModel(engine, ui);
await phone.InitializeAsync();
Dialer.ViewModel = phone; // the WinUI dialler control
await phone.OpenLinkAsync("tel:+27315550100");Your code never touches a SIP library.
Your application talks to one public API. SIP, audio devices and storage sit behind adapters, so each can change without breaking you.
The core is UI-free and works in services, desktop apps and tests. Windows audio (WASAPI) and SQLite call history are included; implement the audio or history port to run elsewhere.
Pay for what you run: IP addresses and concurrent calls.
A VoipSDK licence covers the servers your calls run on and the number of calls you carry at once. Tell us your footprint and we will send you a quote.