TimDinh.IgMarkets.Client 1.0.0

dotnet add package TimDinh.IgMarkets.Client --version 1.0.0
                    
NuGet\Install-Package TimDinh.IgMarkets.Client -Version 1.0.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="TimDinh.IgMarkets.Client" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TimDinh.IgMarkets.Client" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="TimDinh.IgMarkets.Client" />
                    
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 TimDinh.IgMarkets.Client --version 1.0.0
                    
#r "nuget: TimDinh.IgMarkets.Client, 1.0.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 TimDinh.IgMarkets.Client@1.0.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=TimDinh.IgMarkets.Client&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=TimDinh.IgMarkets.Client&version=1.0.0
                    
Install as a Cake Tool

TimDinh.IgMarkets.Client

A reusable, Native AOT compatible .NET client for the IG Markets REST Trading API.

Targets net8.0 and net10.0. No third-party dependencies.


Why this exists

The IG REST API is versioned per endpoint, tunnels its deletes through a header, and returns a deal reference rather than a deal outcome. This library hides those quirks behind a typed surface while staying trim-safe and reflection-free, so it can be linked into a Native AOT binary.

Every payload contract is generated by the System.Text.Json source generator. Nothing is built by reflection at run time, so nothing the trimmer removes can be needed later.


Quick start

using TimDinh.IgMarkets;
using TimDinh.IgMarkets.Models.Common;
using TimDinh.IgMarkets.Models.Dealing;

using var client = new IgClient(new IgClientOptions
{
    ApiKey = "your-api-key",
    Environment = IgEnvironment.Demo,   // Demo is the default, so a misconfigured client cannot deal for real
});

await client.Session.LoginAsync("your-username", "your-password");

// Find something to deal on.
var search = await client.Markets.SearchAsync("EUR/USD");
var epic = search.Markets[0].Epic;

// Check what the market allows before sending a deal.
var market = await client.Markets.GetMarketAsync(epic);
Console.WriteLine($"{market.Instrument!.Name}: {market.Snapshot!.Bid} / {market.Snapshot.Offer}");

// Place a deal, then confirm what actually happened to it.
var deal = await client.Dealing.CreatePositionAsync(new CreatePositionRequest
{
    Epic = epic,
    Expiry = market.Instrument.Expiry!,
    Direction = Direction.Buy,
    CurrencyCode = "GBP",
    Size = 1,
    ForceOpen = true,
    GuaranteedStop = false,
    OrderType = OrderType.Market,
    TimeInForce = PositionTimeInForce.FillOrKill,
});

var confirmation = await client.Dealing.GetConfirmationAsync(deal.DealReference);

if (confirmation.DealStatus is DealStatus.Rejected)
{
    Console.WriteLine($"Rejected: {confirmation.Reason}");
}

await client.Session.LogoutAsync();

A deal reference is not a filled deal. Every dealing call returns only a dealReference, which means IG accepted the instruction. You must call GetConfirmationAsync to learn whether the deal was filled or rejected, and why.


Authentication

IG offers three login schemes. The client stores whichever credentials it receives and attaches them to every later request automatically.

Version 2 — session token headers (most common)

await client.Session.LoginAsync("username", "password");

Returns the CST and X-SECURITY-TOKEN header pair. These do not expire on a fixed timer, but IG invalidates them after a period of inactivity.

Version 2 with an encrypted password

await client.Session.LoginEncryptedAsync("username", "password");

Fetches the RSA public key from GET /session/encryptionKey and encrypts the password with it, so the plain text password never leaves the process. The scheme is base64(RSA-PKCS1(base64("password|timestamp"))), implemented in IgPasswordEncryptor.

Version 3 — OAuth

await client.Session.LoginOAuthAsync("username", "password");

Returns a short-lived bearer token plus a refresh token, sent as Authorization: Bearer and IG-ACCOUNT-ID. The access token expires after about 60 seconds. By default the client detects a 401, refreshes the token, and retries the request once. Turn that off with AutoRefreshOAuthToken = false if you would rather handle it yourself.

Inspect the current state at any time:

if (client.Authentication.IsAuthenticated) { /* ... */ }
client.Authentication.AccessTokenExpiresAtUtc;   // null unless a v3 session is active

Endpoint coverage

The client is organised into groups on IgClient. Each call targets the latest documented version of its endpoint; the Version header is set for you.

client.Session

Method Endpoint Ver
LoginAsync POST /session 2
LoginEncryptedAsync GET /session/encryptionKey then POST /session 1, 2
LoginOAuthAsync POST /session 3
GetEncryptionKeyAsync GET /session/encryptionKey 1
GetAsync GET /session 1
SwitchAccountAsync PUT /session 1
RefreshTokenAsync POST /session/refresh-token 1
LogoutAsync DELETE /session 1

client.Dealing

Method Endpoint Ver
GetPositionsAsync GET /positions 2
GetPositionAsync GET /positions/{dealId} 2
CreatePositionAsync POST /positions/otc 2
ClosePositionAsync DELETE /positions/otc 1
UpdatePositionAsync PUT /positions/otc/{dealId} 2
GetWorkingOrdersAsync GET /workingorders 2
CreateWorkingOrderAsync POST /workingorders/otc 2
UpdateWorkingOrderAsync PUT /workingorders/otc/{dealId} 2
DeleteWorkingOrderAsync DELETE /workingorders/otc/{dealId} 2
GetConfirmationAsync GET /confirms/{dealReference} 1

client.Markets

Method Endpoint Ver
GetMarketAsync GET /markets/{epic} 3
GetMarketsAsync GET /markets?epics= 2
SearchAsync GET /markets?searchTerm= 1
GetPricesAsync GET /prices/{epic} 3
GetCategoriesAsync GET /categories 1
GetCategoryInstrumentsAsync GET /categories/{categoryId}/instruments 1

client.Accounts

Method Endpoint Ver
GetAccountsAsync GET /accounts 1
GetPreferencesAsync GET /accounts/preferences 1
UpdatePreferencesAsync PUT /accounts/preferences 1
GetActivityAsync GET /history/activity 3
GetTransactionsAsync GET /history/transactions 2

client.Watchlists

Method Endpoint Ver
GetWatchlistsAsync GET /watchlists 1
GetWatchlistAsync GET /watchlists/{id} 1
CreateWatchlistAsync POST /watchlists 1
DeleteWatchlistAsync DELETE /watchlists/{id} 1
AddMarketAsync PUT /watchlists/{id} 1
RemoveMarketAsync DELETE /watchlists/{id}/{epic} 1

client.ClientSentiment

Method Endpoint Ver
GetAsync(marketIds) GET /clientsentiment?marketIds= 1
GetAsync(marketId) GET /clientsentiment/{marketId} 1
GetRelatedAsync GET /clientsentiment/related/{marketId} 1

client.Operations

Method Endpoint Ver
GetApplicationsAsync GET /operations/application 1
UpdateApplicationAsync PUT /operations/application 1
DisableCurrentApplicationAsync PUT /operations/application/disable 1
GetRepeatDealingWindowAsync GET /repeat-dealing-window 1

client.IndicativeCosts

Method Endpoint Ver
GetOpeningCostsAsync POST /indicativecostsandcharges/open 1
GetClosingCostsAsync POST /indicativecostsandcharges/close 1
GetEditCostsAsync POST /indicativecostsandcharges/edit 1
GetHistoryAsync GET /indicativecostsandcharges/history/from/{from}/to/{to} 1
DownloadDurableMediumAsync GET /indicativecostsandcharges/durablemedium/{ref} 1

Error handling

Any non-success status raises IgApiException:

try
{
    await client.Session.LoginAsync("username", "wrong-password");
}
catch (IgApiException ex)
{
    ex.StatusCode;     // HttpStatusCode.BadRequest
    ex.ErrorCode;      // "error.security.invalid-details"
    ex.ResponseBody;   // the raw body, kept for diagnostics
}

ErrorCode is null when IG returns something that is not a JSON error payload — an HTML gateway page, for example. The raw body is always preserved.


Dependency injection

Pass your own HttpClient to participate in an IHttpClientFactory pipeline:

services.AddHttpClient<IgClient>()
        .AddStandardResilienceHandler();

services.AddSingleton(sp => new IgClient(
    igOptions,
    sp.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(IgClient))));

When you supply the HttpClient, you keep ownership of it; IgClient.Dispose only clears the stored credentials. When the client creates its own, disposing it disposes both.

Threading: an IgClient instance is one IG session. Concurrent reads and deals on an established session are fine. Do not log in concurrently on one instance — a login replaces the stored credentials.


Native AOT

The library sets IsAotCompatible and IsTrimmable, and publishes with zero trim or AOT analyzer warnings.

samples/IgMarkets.AotCheck exists to prove it. It runs the client against canned responses and asserts on the results, so a contract that fell back to reflection would fail there even though it passes on CoreCLR:

dotnet publish samples/IgMarkets.AotCheck -c Release -r osx-arm64
./samples/IgMarkets.AotCheck/bin/Release/net10.0/osx-arm64/publish/IgMarkets.AotCheck

Produces a ~5 MB self-contained native binary and exits non-zero if any check fails.


Implementation notes

Things that are easy to get wrong against this API, and how they are handled here.

The documentation's URL slugs are not the wire paths. The reference pages are served at working-orders.html and client-sentiment.html, but the actual endpoints are /workingorders and /clientsentiment, unhyphenated. This was verified against two independent client libraries before it was written down.

Every delete is tunnelled. IG does not accept a real HTTP DELETE on these endpoints, so all deletes are sent as POST carrying the _method: DELETE header, with {} as the body when there is nothing else to send. This matches what the reference Python and Node clients do.

Request bodies are flat. The reference pages show wrapper names such as authenticationRequest and createWatchlistRequest. These are schema model names, not JSON envelopes; the real bodies have no wrapper.

Enums are strict. Enum members are declared in PascalCase and converted to IG's UPPER_SNAKE_CASE on the wire by a single generic converter. Every documented constant is present, including IG's own misspelling of PARTIALY_CLOSED_POSITION_NOT_DELETED. If IG adds a constant this library does not know, deserialization throws a JsonException naming the value, which is preferable to silently reporting a deal in the wrong direction. Open an issue and the value gets added.

Dates stay strings. IG returns at least four different date formats across endpoints, some without a timezone, and the format varies by endpoint and version. Rather than guess and risk a silent off-by-one-hour bug, date fields are surfaced exactly as IG sends them. Fields IG documents as snapshotTimeUTC or createdDateUTC are mapped explicitly, since the casing does not follow the camelCase convention.

Some money fields are strings. GET /history/transactions returns profitAndLoss, openLevel, closeLevel and size as preformatted strings, sometimes with a currency symbol. They are surfaced unchanged rather than being coerced into decimal.


Deliberately not implemented

  • Older versions of covered endpoints. IG keeps v1 of /positions, /workingorders, /history/activity and others alongside newer versions, along with dated-path variants such as GET /prices/{epic}/{resolution}/{numPoints} and GET /history/activity/{fromDate}/{toDate}. Each endpoint is implemented once, at the newest documented version, whose query-parameter form supersedes the dated paths.
  • GET /markets/{epic} version 4. The reference lists it but documents the version 3 schema, so version 3 is what is implemented rather than a guess at version 4.
  • /marketnavigation. A real endpoint for browsing IG's market hierarchy, but absent from the API reference this library was built against. /categories covers the documented equivalent.
  • The streaming API. IG's live prices run over Lightstreamer, which is a separate protocol and a separate library. SessionDetails.LightstreamerEndpoint gives you the endpoint if you want to wire one up.

Building and testing

dotnet build                                     # both target frameworks, warnings as errors
dotnet test                                      # 101 tests
dotnet publish samples/IgMarkets.AotCheck -c Release -r osx-arm64

The test suite substitutes the HTTP layer with NSubstitute and asserts on the exact bytes sent — paths, Version headers, the _method override, enum wire names and omitted optional fields — as well as on how responses are mapped back.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

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
1.0.0 69 9/6/2026