TwsApi 0.5.0

dotnet add package TwsApi --version 0.5.0
                    
NuGet\Install-Package TwsApi -Version 0.5.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TwsApi" Version="0.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TwsApi" Version="0.5.0" />
                    
Directory.Packages.props
<PackageReference Include="TwsApi" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TwsApi --version 0.5.0
                    
#r "nuget: TwsApi, 0.5.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TwsApi@0.5.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TwsApi&version=0.5.0
                    
Install as a Cake Addin
#tool nuget:?package=TwsApi&version=0.5.0
                    
Install as a Cake Tool

TwsApi - modern async/await .NET 10 wrapper for the IB TWS API

CI

A modern .NET 10 facade over the Interactive Brokers TWS API C# client. It hides the legacy callback/threading/request-id model behind idiomatic async/await and IAsyncEnumerable<T>.

await using var tws = await TwsClient.ConnectAsync(new TwsConnectionOptions { Port = 4002 });

var details = await tws.ResolveContractAsync(Contracts.Stock("AAPL"));
var bars    = await tws.GetHistoricalBarsAsync(Contracts.Stock("AAPL"), duration: "5 D", barSize: "1 day");

tws.SetMarketDataType(3); // delayed data (works on paper without subscriptions)
await foreach (var tick in tws.SubscribeMarketDataAsync(Contracts.Stock("AAPL"), cancellationToken: ct))
    Console.WriteLine($"field {tick.Field}: {tick.Price ?? tick.Value}");

Layout

Path What
src/TwsApi The modern library (net10.0). Wraps the vendored CSharpAPI.csproj.
src/Tests xUnit tests. Integration tests boot IB Gateway via TestContainers.
vendor/tws-api Vendored IBKR TWS API, pinned as a git submodule (referenced, unchanged).
docker-compose.yml Standalone IB Gateway (paper) for manual use.

The vendor/tws-api submodule must be checked out before building. Clone with git clone --recursive <repo>, or if already cloned run git submodule update --init.

Design

  • ITwsClient - the public contract TwsClient implements. Depend on this interface in your own code so the API can be stubbed/mocked in tests.
  • TwsClient - the public entry point. One-shot calls return Task<T>; streaming subscriptions return IAsyncEnumerable<T> (backed by System.Threading.Channels).
  • Internal/TwsEventDispatcher - a single DefaultEWrapper subclass that re-publishes the relevant callbacks as typed .NET events.
  • Internal/ConnectionManager - encapsulates the socket + EReader + processing thread and exposes ConnectAsync that completes on nextValidId (no busy-wait).
  • Internal/RequestIdAllocator - hands out request/order ids so callers never manage them.

Service collection

The library ships a Microsoft.Extensions.DependencyInjection integration. Register the factory and default connection options once at startup with AddTwsApi:

services.AddTwsApi(o =>
{
    o.Host = "127.0.0.1";
    o.Port = 4002;
    o.ClientId = 1;
});

This registers ITwsClientFactory (singleton) and binds the configured TwsConnectionOptions. Inject the factory and create connections on demand:

public sealed class Quotes(ITwsClientFactory factory)
{
    public async Task<decimal> NetLiqAsync(CancellationToken ct)
    {
        await using var tws = await factory.ConnectAsync(ct);        // uses configured options
        var summary = await tws.GetAccountSummaryAsync("NetLiquidation", cancellationToken: ct);
        return decimal.Parse(summary[0].Value);
    }
}
  • factory.ConnectAsync(ct) - connect with the configured (default) options.
  • factory.ConnectAsync(options, ct) - override the options per call (e.g. a different ClientId).
  • Depend on ITwsClientFactory / ITwsClient, not the concrete types, so both creation and use are mockable in tests. The static TwsClient.ConnectAsync(options) remains for scripts and non-DI use.

Running multiple connections

Every ITwsClient is a fully independent connection - its own socket, background reader thread and request-id sequence - with no shared or static state. Several can stay live at the same time, each pointed at a different IB gateway (different port / login).

Register one named configuration per gateway and resolve it by name:

services.AddTwsApi("paperA", o => { o.Port = 4002; o.ClientId = 1; });
services.AddTwsApi("paperB", o => { o.Port = 4004; o.ClientId = 1; });

// inject ITwsClientFactory, then:
await using var a = await factory.ConnectAsync("paperA", ct);
await using var b = await factory.ConnectAsync("paperB", ct);

var (timeA, timeB) = (await a.GetServerTimeAsync(ct), await b.GetServerTimeAsync(ct));

Without DI, construct each client directly with its own options:

await using var a = await TwsClient.ConnectAsync(new() { Port = 4002, ClientId = 1 });
await using var b = await TwsClient.ConnectAsync(new() { Port = 4004, ClientId = 1 });

Two rules from the IB side:

  • Each gateway is a separate login/account. Connecting to several gateways means several sessions - one login can't be shared across gateways.
  • ClientId is namespaced per gateway. Reusing 1 across different gateways is fine; two connections to the same gateway must use distinct client ids.

Running the Gateway standalone

cp .env.example .env      # fill in TWS_USERID / TWS_PASSWORD (paper account)
docker compose up -d      # API on 127.0.0.1:4002, optional VNC on :5900

2FA must be disabled on the paper login for headless (IBC) auto-login to succeed. IBKR permits disabling 2FA for paper accounts.

REST client authentication

The REST/Web API client (RestApi.IRestClient, over the IBKR Client Portal Web API) supports two authentication flows. Both expose the exact same typed surface — you only change how RestClientOptions is configured. See API.md for the full endpoint map.

Gateway (session) OAuth 1.0a
Runs Client Portal Gateway (docker/cpgateway) Gateway-free, direct to IBKR
Base address https://localhost:5000 (default) https://api.ibkr.com
Login Browser SSO, once per session Signed requests, no browser
Credentials in code None Consumer key, access token/secret, RSA + DH keys
Best for Interactive / desktop use Headless services, automation

Gateway (session)

The default. The gateway holds the session established by a browser SSO login, so no credentials are sent from code. Before REST calls succeed you must (1) have the gateway running and (2) have logged in via browser at https://localhost:5000. Keep the session alive by calling Session.TickleAsync() roughly once a minute.

using RestApi;

using IRestClient rest = new RestClient(new RestClientOptions
{
    BaseAddress = new Uri("https://localhost:5000"),   // default; self-signed cert accepted by default
});

var status = await rest.Session.GetAuthStatusAsync();  // Authenticated == true once logged in

OAuth 1.0a (gateway-free)

Set RestClientOptions.OAuth to talk to IBKR's Web API host directly with no running gateway. On first use the client negotiates a live session token (Diffie-Hellman + RSA-SHA256), then signs every request with HMAC-SHA256 — all transparently. After construction, call Session.InitializeBrokerageSessionAsync() once to open the brokerage session, then tickle as usual.

Setting up OAuth on IBKR's website

OAuth is registered through IBKR's self-service portal. Log in with the account you want the API to act as at the OAuth login URL and append the OAuth action:

Self-service portal: https://ndcdyn.interactivebrokers.com/sso/Login?action=OAUTH&RL=1&ip2loc=US (if that doesn't resolve for your region, open your local IBKR login page and append &action=OAUTH). Reference: IBKR Campus — OAuth 1.0a. This flow mirrors the one documented by Voyz/ibind.

  1. Generate the key material locally with OpenSSL — a signature key pair, an encryption key pair, and a Diffie-Hellman prime:

    openssl genrsa -out private_signature.pem 2048
    openssl rsa -in private_signature.pem -outform PEM -pubout -out public_signature.pem
    openssl genrsa -out private_encryption.pem 2048
    openssl rsa -in private_encryption.pem -outform PEM -pubout -out public_encryption.pem
    openssl dhparam -out dhparam.pem 2048
    
  2. Choose a consumer key — a 9-character identifier you pick (A–Z; alpha characters are upper-cased). This is your consumerKey.

  3. Upload the public material to the portal: public_signature.pem, public_encryption.pem, and dhparam.pem. Keep the two private_*.pem files secret — they never leave your machine.

  4. Generate the access token — the portal produces your Access Token and Access Token Secret. Copy both immediately: the secret is shown only once and won't reappear on a later visit.

The three files you keep map directly onto OAuth1aOptions.FromPemFiles: private_signature.pemsigningKeyPemPath, private_encryption.pemencryptionKeyPemPath, dhparam.pemdhParamPemPath.

Activation delay: newly registered consumer keys aren't live immediately — IBKR activates them on a weekend server restart, so it can take up to a few days before the handshake succeeds.

Supply the material issued during IBKR OAuth self-registration (consumer key, access token/secret, the signing and encryption RSA keys, and the DH prime):

using RestApi;
using RestApi.Authentication;

using IRestClient rest = new RestClient(new RestClientOptions
{
    BaseAddress = new Uri("https://api.ibkr.com"),
    AcceptAnyServerCertificate = false,                // IBKR's cert is trusted
    OAuth = OAuth1aOptions.FromPemFiles(
        consumerKey: "MYCONSUMER",
        accessToken: "…",
        accessTokenSecret: "…",                        // base64, RSA-encrypted for you
        signingKeyPemPath: "private_signature.pem",
        encryptionKeyPemPath: "private_encryption.pem",
        dhParamPemPath: "dhparam.pem"),
});

await rest.Session.InitializeBrokerageSessionAsync();  // required before trading / market data
var status = await rest.Session.GetAuthStatusAsync();

Both flows also work through DI — set o.OAuth = … inside AddRestApi(o => …) / AddIbkrRestClient(o => …). Use test_realm for IBKR's TESTCONS test consumer; the default realm is limited_poa.

Tests

Unit tests always run. Gateway integration tests require paper credentials and Docker; without them they skip (they never fail CI).

# provide credentials (either works)
export TWS_USERID=... TWS_PASSWORD=...
#   or, locally:
dotnet user-secrets --project src/Tests set TWS_USERID ...
dotnet user-secrets --project src/Tests set TWS_PASSWORD ...

dotnet test

With credentials + Docker present, TestContainers pulls ghcr.io/gnzsnz/ib-gateway, waits for the API to become ready (auto-login can take 30-90s), runs the suite, and tears the container down automatically.

Publishing

The .github/workflows/publish-nuget.yml action packs src/TwsApi and pushes it to nuget.org as TwsApi whenever a GitHub Release is published. The package version comes from the release tag (v1.2.01.2.0). The package bundles the vendored CSharpAPI.dll and declares Google.Protobuf + Microsoft.Extensions.* as dependencies.

Auth uses NuGet trusted publishing (OIDC) - there is no long-lived API key secret. One-time setup on nuget.org: Account → Trusted Publishing → Add, with

  • Package owner: your nuget.org account
  • Repository owner: UtopleMan, Repository: tws-api-dotnet
  • Workflow file: publish-nuget.yml

Set the user: input in the workflow to your nuget.org username. Then create a release tagged e.g. v0.1.0 to publish.

Note: the package embeds Interactive Brokers' compiled CSharpAPI.dll. Redistributing it is subject to the IB API license terms (see THIRD-PARTY-NOTICES.md).

License

The wrapper code in this repository (the src/ tree) is licensed under the MIT License.

The IBKR TWS API is not covered by that license. It is included as a git submodule under vendor/tws-api and remains © Interactive Brokers LLC under the IB API Non-Commercial / Commercial License. This project does not redistribute it and grants you no rights to it - your use of the IBKR API is governed by Interactive Brokers' own terms (the default is non-commercial). See THIRD-PARTY-NOTICES.md for the full dependency breakdown (also Google.Protobuf, BSD-3-Clause; Microsoft.Extensions.*, MIT).

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.0 97 7/13/2026
0.4.1 97 7/6/2026
0.4.0 100 7/5/2026
0.3.0 100 7/5/2026
0.2.0 101 7/5/2026
0.1.0 99 7/4/2026