TwsApi 0.5.0
dotnet add package TwsApi --version 0.5.0
NuGet\Install-Package TwsApi -Version 0.5.0
<PackageReference Include="TwsApi" Version="0.5.0" />
<PackageVersion Include="TwsApi" Version="0.5.0" />
<PackageReference Include="TwsApi" />
paket add TwsApi --version 0.5.0
#r "nuget: TwsApi, 0.5.0"
#:package TwsApi@0.5.0
#addin nuget:?package=TwsApi&version=0.5.0
#tool nuget:?package=TwsApi&version=0.5.0
TwsApi - modern async/await .NET 10 wrapper for the IB TWS API
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-apisubmodule must be checked out before building. Clone withgit clone --recursive <repo>, or if already cloned rungit submodule update --init.
Design
ITwsClient- the public contractTwsClientimplements. 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 returnTask<T>; streaming subscriptions returnIAsyncEnumerable<T>(backed bySystem.Threading.Channels).Internal/TwsEventDispatcher- a singleDefaultEWrappersubclass that re-publishes the relevant callbacks as typed .NET events.Internal/ConnectionManager- encapsulates the socket +EReader+ processing thread and exposesConnectAsyncthat completes onnextValidId(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 differentClientId).- Depend on
ITwsClientFactory/ITwsClient, not the concrete types, so both creation and use are mockable in tests. The staticTwsClient.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.
ClientIdis namespaced per gateway. Reusing1across 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.
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 2048Choose a consumer key — a 9-character identifier you pick (A–Z; alpha characters are upper-cased). This is your
consumerKey.Upload the public material to the portal:
public_signature.pem,public_encryption.pem, anddhparam.pem. Keep the twoprivate_*.pemfiles secret — they never leave your machine.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.pem → signingKeyPemPath, private_encryption.pem → encryptionKeyPemPath,
dhparam.pem → dhParamPemPath.
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.0 → 1.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 | Versions 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. |
-
net10.0
- Google.Protobuf (>= 3.29.5)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Http (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.