MarqSpec.Client.ProjectX 3.0.0

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

ProjectX API Client

A .NET client library for the ProjectX REST API, providing easy access to market data and trading operations.

Features

✅ User Story 1: Authentication

  • API key and secret authentication via environment variables or configuration files
  • Automatic JWT token management with refresh
  • Secure credential handling (never logged or exposed)
  • Clear error messages for authentication failures

✅ User Story 2: Market Data Queries

  • Get current prices for symbols
  • Retrieve order book depth
  • Query recent trades
  • Proper error handling with meaningful messages
  • All responses deserialized into strongly-typed C# models

✅ User Story 3: Order Management

  • Place, modify, and cancel orders
  • Query open orders and order history
  • Manage positions (close, partial close)
  • Query trade executions
  • Bracket orders with stop-loss and take-profit

✅ User Story 4: Real-Time Streaming Data

  • WebSocket streaming via two SignalR hubs (Market Hub + User Hub)
  • Subscribe to real-time price updates, order book depth, and trade executions
  • Subscribe to real-time order status updates per account
  • Automatic reconnection with exponential backoff (1s initial, 5s max), restoring subscriptions before reporting Connected
  • Connection status monitoring via events
  • Thread-safe, high-throughput (1000+ events/second)

Installation

Add the package reference to your project:

dotnet add package MarqSpec.Client.ProjectX

Configuration

Set the following environment variables:

PROJECTX_API_KEY=your-api-key
PROJECTX_API_SECRET=your-api-secret

Option 2: appsettings.json

{
  "ProjectX": {
    "ApiKey": "your-api-key",
    "ApiSecret": "your-api-secret",
    "BaseUrl": "https://api.topstepx.com",
    "RetryOptions": {
      "MaxRetries": 3,
      "InitialDelay": "00:00:01",
      "MaxDelay": "00:00:30"
    }
  }
}

Note: Environment variables take precedence over appsettings.json

Quick Start

1. Register Services

In your Program.cs or Startup.cs:

using MarqSpec.Client.ProjectX.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Add ProjectX API client
builder.Services.AddProjectXApiClient(builder.Configuration);

var app = builder.Build();

DI lifetimes: IProjectXApiClient is registered as Scoped and IProjectXWebSocketClient as Singleton. Inject IProjectXWebSocketClient into long-lived services only; avoid resolving it from a scoped context directly.

2. Use the Client

using MarqSpec.Client.ProjectX;
using MarqSpec.Client.ProjectX.Api.Models;

public class TradingService
{
    private readonly IProjectXApiClient _apiClient;
    private readonly ILogger<TradingService> _logger;

    public TradingService(IProjectXApiClient apiClient, ILogger<TradingService> logger)
    {
        _apiClient = apiClient;
        _logger = logger;
    }

    public async Task RunAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            // Get trading accounts
            var accounts = await _apiClient.GetAccountsAsync(onlyActiveAccounts: true, cancellationToken);
            var account = accounts.First();
            _logger.LogInformation("Account {Id}: {Name} (Balance: {Balance})",
                account.Id, account.Name, account.Balance);

            // Search for live contracts
            var contracts = await _apiClient.SearchContractsAsync("NQ", live: true, cancellationToken);
            var contract = contracts.First();
            _logger.LogInformation("Contract {Id}: {Name} (Tick: {Size}/{Value})",
                contract.Id, contract.Name, contract.TickSize, contract.TickValue);

            // Place a limit order
            var orderResponse = await _apiClient.PlaceOrderAsync(new PlaceOrderRequest
            {
                AccountId = account.Id,
                ContractId = contract.Id,
                Type = OrderType.Limit,
                Side = OrderSide.Bid,
                Size = 1,
                LimitPrice = 18000m
            }, cancellationToken);
            _logger.LogInformation("Order placed: {OrderId}", orderResponse.OrderId);

            // Query open orders
            var openOrders = await _apiClient.GetOpenOrdersAsync(account.Id, cancellationToken);
            _logger.LogInformation("Open orders: {Count}", openOrders.Count());

            // Query open positions
            var positions = await _apiClient.GetOpenPositionsAsync(account.Id, cancellationToken);
            _logger.LogInformation("Open positions: {Count}", positions.Count());
        }
        catch (ProjectXApiException ex)
        {
            _logger.LogError(ex, "API error: {Message}", ex.Message);
        }
        catch (AuthenticationException ex)
        {
            _logger.LogError(ex, "Authentication failed: {Message}", ex.Message);
        }
    }
}

Real-Time Streaming

The client provides real-time market data and order updates via two SignalR WebSocket hubs:

  • Market Hub — price quotes, order book depth, and trade executions
  • User Hub — order status updates for your accounts

Updates are delivered through C# events using the Observer pattern.

Streaming Market Data

using MarqSpec.Client.ProjectX;
using MarqSpec.Client.ProjectX.WebSocket;

public class MarketDataService : IAsyncDisposable
{
    private readonly IProjectXWebSocketClient _wsClient;
    private readonly ILogger<MarketDataService> _logger;

    public MarketDataService(IProjectXWebSocketClient wsClient, ILogger<MarketDataService> logger)
    {
        _wsClient = wsClient;
        _logger = logger;
    }

    public async Task StartStreamingAsync(string contractId, CancellationToken cancellationToken = default)
    {
        // Monitor connection status changes
        _wsClient.ConnectionStatusChanged += (sender, change) =>
        {
            _logger.LogInformation("Market Hub: {Previous} → {Current}",
                change.PreviousState, change.CurrentState);

            if (change.ErrorMessage is not null)
                _logger.LogWarning("Connection error: {Error}", change.ErrorMessage);
        };

        // Register event handlers
        _wsClient.PriceUpdateReceived += (sender, update) =>
        {
            _logger.LogInformation("{Symbol} Last={Last} Bid={Bid} Ask={Ask} Chg={Chg}% Vol={Vol}",
                update.Symbol, update.LastPrice,
                update.BestBid, update.BestAsk,
                update.ChangePercent, update.Volume);
        };

        _wsClient.OrderBookUpdateReceived += (sender, update) =>
        {
            _logger.LogInformation("{Contract} Depth: {Type} {Price} x {Volume}",
                update.ContractId, update.Type, update.Price, update.Volume);
        };

        _wsClient.TradeUpdateReceived += (sender, update) =>
        {
            _logger.LogInformation("{Contract} Trade: {Price} x {Volume} ({Type})",
                update.ContractId, update.Price, update.Volume, update.Type);
        };

        // Connect and subscribe
        await _wsClient.ConnectMarketHubAsync(cancellationToken);
        await _wsClient.SubscribeToPriceUpdatesAsync(contractId, cancellationToken);
        await _wsClient.SubscribeToOrderBookUpdatesAsync(contractId, cancellationToken);
        await _wsClient.SubscribeToTradeUpdatesAsync(contractId, cancellationToken);
    }

    public async Task StopStreamingAsync(string contractId, CancellationToken cancellationToken = default)
    {
        await _wsClient.UnsubscribeFromPriceUpdatesAsync(contractId, cancellationToken);
        await _wsClient.UnsubscribeFromOrderBookUpdatesAsync(contractId, cancellationToken);
        await _wsClient.UnsubscribeFromTradeUpdatesAsync(contractId, cancellationToken);
        await _wsClient.DisconnectMarketHubAsync(cancellationToken);
    }

    public async ValueTask DisposeAsync()
    {
        await _wsClient.DisposeAsync();
    }
}

Streaming Order Updates

// Connect to the User Hub for order status updates
await wsClient.ConnectUserHubAsync(cancellationToken);

wsClient.OrderUpdateReceived += (sender, update) =>
{
    logger.LogInformation("Order {Id} on account {Acct}: {Status} — {Side} {Size} @ {Price}",
        update.OrderId, update.AccountId, update.Status,
        update.Side, update.Size, update.AverageFillPrice);

    if (update.RejectionReason is not null)
        logger.LogWarning("Rejected: {Reason}", update.RejectionReason);
};

await wsClient.SubscribeToOrderUpdatesAsync(accountId, cancellationToken);

// Later: clean up
await wsClient.UnsubscribeFromOrderUpdatesAsync(accountId, cancellationToken);
await wsClient.DisconnectUserHubAsync(cancellationToken);

Connection States

The ConnectionState enum tracks the lifecycle of each hub connection:

State Description
Disconnected Not connected
Connecting Connection attempt in progress
Connected Active and receiving data
Reconnecting Automatically reconnecting after a disconnection
Failed Connection failed (check ErrorMessage on the event)

Monitor state transitions via the ConnectionStatusChanged event, which provides a ConnectionStatusChange object containing PreviousState, CurrentState, Timestamp, ErrorMessage, and Exception.

Auto-Reconnection

Automatic reconnection is enabled by default. When a connection drops, the client uses exponential backoff starting at 1 second up to a maximum of 5 seconds. During reconnection, the ConnectionStatusChanged event fires with ConnectionState.Reconnecting. SignalR automatic reconnect is a new connection id, so server-side subscriptions are gone: the client re-invokes every recorded SubscribeTo* against the new connection before reporting Connected. A failed restore raises MessageSendFailed and reports Failed, not Connected. Connect*HubAsync after Failed disposes the previous hub and does the same restore-before-Connected step; a restore failure there is thrown to the caller. MarketSubscriptions and UserSubscriptions are recorded subscribe intent — what the next connect or reconnect will try to restore — not proof the server is currently delivering. Trust ConnectionState.Connected after a successful restore, not the snapshot alone. Configure reconnection behavior in appsettings.json:

{
  "ProjectX": {
    "WebSocket": {
      "AutoReconnect": true,
      "InitialReconnectDelaySeconds": 1,
      "MaxReconnectDelaySeconds": 5
    }
  }
}

API Reference

IProjectXApiClient

Accounts
Method Parameters Returns Description
GetAccountsAsync bool onlyActiveAccounts = true IEnumerable<TradingAccount> Get trading accounts
Contracts
Method Parameters Returns Description
SearchContractsAsync string? searchText, bool live = true IEnumerable<Contract> Search contracts by text
GetContractAsync string contractId, bool live Contract? Deprecated — live is ignored; the by-ID route has no such field. Use GetContractByIdAsync
GetContractByIdAsync string contractId Contract? Direct contract lookup by ID
GetAvailableContractsAsync bool live = true IEnumerable<Contract> List all available contracts
Historical Data
Method Parameters Returns Description
GetHistoricalBarsAsync string contractId, DateTime startTime, DateTime endTime, AggregateBarUnit unit, int unitNumber = 1, int limit = 1000, bool live = true, bool includePartialBar = false IEnumerable<AggregateBar> Retrieve historical OHLCV bars
Orders
Method Parameters Returns Description
PlaceOrderAsync PlaceOrderRequest request PlaceOrderResponse Place a new order
ModifyOrderAsync ModifyOrderRequest request ModifyOrderResponse Modify an existing order
CancelOrderAsync int accountId, long orderId CancelOrderResponse Cancel an existing order
GetOrderAsync int accountId, long orderId, DateTime startTime, DateTime? endTime Order? Get a specific order within a window
GetOrdersAsync int accountId, DateTime? startTime, DateTime? endTime IEnumerable<Order> Get orders in a time range
GetOpenOrdersAsync int accountId IEnumerable<Order> Get all open/working orders

The order search requires a window. The gateway's /api/Order/search schema marks startTimestamp required. Omitting it lets the gateway apply a window of its own, and an order outside that window comes back absent — which a caller cannot tell apart from "no such order". In a reconciliation path that reads as a live order was never placed.

So GetOrdersAsync throws ArgumentException when startTime is null, and the two-argument GetOrderAsync(accountId, orderId) is [Obsolete(error: true)] — replaced by the overload above. Neither substitutes a default window, because a window the client invents silently reproduces exactly the failure it is meant to prevent. Choose one that certainly contains the order; when reconciling a placement, that means starting before the placement was attempted.

GetOpenOrdersAsync needs no window — /api/Order/searchOpen takes only an account.

Note also that SearchOrderRequest.ContractId and .Status are not in the gateway's schema. They are serialized and ignored, so they filter nothing; filter the returned collection instead.

Positions
Method Parameters Returns Description
GetOpenPositionsAsync int accountId IEnumerable<Position> Get all open positions
ClosePositionAsync int accountId, string contractId ClosePositionResponse Close a full position
PartialClosePositionAsync int accountId, string contractId, int size PartialClosePositionResponse Partially close a position
Trades
Method Parameters Returns Description
GetTradesAsync int accountId, DateTime? startTime, DateTime? endTime IEnumerable<HalfTrade> Get trade executions
Utility
Method Returns Description
PingAsync bool Check if the API is responsive

Note: All methods accept an optional CancellationToken parameter (omitted from tables for brevity).

IProjectXWebSocketClient

Connection Management
Method Description
ConnectMarketHubAsync(CancellationToken) Connect to the market data hub
DisconnectMarketHubAsync(CancellationToken) Disconnect from the market data hub
ConnectUserHubAsync(CancellationToken) Connect to the user data hub
DisconnectUserHubAsync(CancellationToken) Disconnect from the user data hub
Properties
Property Type Description
MarketHubState ConnectionState Current connection state of the market hub
UserHubState ConnectionState Current connection state of the user hub
MarketSubscriptions MarketHubSubscriptions Recorded market-hub subscribe intent the client will try to restore; not a liveness guarantee
UserSubscriptions UserHubSubscriptions Recorded user-hub subscribe intent the client will try to restore; not a liveness guarantee
Market Data Subscriptions
Method Parameters Description
SubscribeToPriceUpdatesAsync string contractId, CancellationToken Subscribe to real-time price quotes
UnsubscribeFromPriceUpdatesAsync string contractId, CancellationToken Unsubscribe from price quotes
SubscribeToOrderBookUpdatesAsync string contractId, CancellationToken Subscribe to order book depth updates
UnsubscribeFromOrderBookUpdatesAsync string contractId, CancellationToken Unsubscribe from order book updates
SubscribeToTradeUpdatesAsync string contractId, CancellationToken Subscribe to trade execution updates
UnsubscribeFromTradeUpdatesAsync string contractId, CancellationToken Unsubscribe from trade updates
User Data Subscriptions
Method Parameters Description
SubscribeToOrderUpdatesAsync int accountId, CancellationToken Subscribe to order status updates
UnsubscribeFromOrderUpdatesAsync int accountId, CancellationToken Unsubscribe from order updates
Events
Event Payload Description
ConnectionStatusChanged ConnectionStatusChange Fires on any connection state transition
PriceUpdateReceived PriceUpdate Fires on each price quote
OrderBookUpdateReceived OrderBookUpdate Fires on each order book snapshot
TradeUpdateReceived TradeUpdate Fires on each trade execution
OrderUpdateReceived OrderUpdate Fires on each order status change
PriceUpdate
Property Type Description
ContractId string Contract the market hub bound this event to (e.g. "CON.F.US.EP.Z26"). Stamped from the hub argument, not the JSON payload
Symbol string Product-root symbol ID (e.g. "F.US.EP")
SymbolName string? Friendly symbol name
LastPrice decimal Last traded price
BestBid decimal Best bid price
BestAsk decimal Best ask price
Change decimal Price change since previous close
ChangePercent decimal Percent change since previous close
Open decimal Session opening price
High decimal Session high price
Low decimal Session low price
Volume decimal Total volume traded this session
LastUpdated DateTime Last updated timestamp
Timestamp DateTime Quote timestamp
OrderBookUpdate

A single DOM (depth-of-market) row from GatewayDepth, not a full book snapshot.

Property Type Description
ContractId string Contract the market hub bound this event to. Stamped from the hub argument; the payload has no symbol
Timestamp DateTime Update timestamp
Type DomType DOM entry type (Ask, Bid, BestAsk, …)
Price decimal Price level
Volume decimal Total volume at this price level
CurrentVolume int Current (delta) volume at this price level
TradeUpdate
Property Type Description
ContractId string Contract the market hub bound this event to. Stamped from the hub argument; SymbolId is the product root
SymbolId string Product-root symbol ID (e.g. "F.US.EP")
Price decimal Trade price
Timestamp DateTime Trade timestamp
Type TradeLogType? Buy (wire 0) or Sell (wire 1). null when the venue omitted or sent an unrecognised type
Volume decimal Trade volume
OrderUpdate
Property Type Description
OrderId long Order identifier
AccountId int Account identifier
ContractId string Contract identifier
Status OrderStatus Current order status
Type OrderType Order type (Market, Limit, Stop, etc.)
Side OrderSide? Bid (wire 0) or Ask (wire 1). null when the venue omitted side
Size int Total order size
FilledQuantity int Quantity filled so far
RemainingQuantity int Quantity remaining
LimitPrice decimal? Limit price (if applicable)
StopPrice decimal? Stop price (if applicable)
AverageFillPrice decimal? Average fill price
Timestamp DateTime Update timestamp
Message string? Update reason or message
RejectionReason string? Rejection reason (if rejected)
ConnectionStatusChange
Property Type Description
PreviousState ConnectionState Connection state before the change
CurrentState ConnectionState Connection state after the change
Timestamp DateTime When the state change occurred
ErrorMessage string? Error description (if error-related)
Exception Exception? Exception that caused the change (if any)

Enums

ConnectionState
Value Description
Disconnected Not connected
Connecting Connection attempt in progress
Connected Active and receiving data
Reconnecting Automatically reconnecting after a disconnection
Failed Connection failed
MarketHubSubscriptions / UserHubSubscriptions

Recorded subscribe intent, not live server state. After Failed the sets still list what the next Connect* or automatic reconnect will try to restore. MarketHubSubscriptions holds the contract-id sets for quotes, depth and trades. UserHubSubscriptions holds the account-updates flag and the account-id sets for orders, positions and trade notifications.

OrderStatus
Value Description
Unknown (0) Unknown status
Accepted (1) Order accepted by the system
Pending (2) Order pending execution
Triggered (3) Stop order has been triggered
PartiallyFilled (4) Order partially filled
Filled (5) Order completely filled
Cancelled (6) Order cancelled
Rejected (7) Order rejected
Expired (8) Order expired
OrderType
Value Description
Unknown (0) Unknown order type
Limit (1) Executes at a specific price or better
Market (2) Executes immediately at the best available price
StopLimit (3) Becomes a limit order when the stop price is reached
Stop (4) Becomes a market order when the stop price is reached
TrailingStop (5) Stop price trails the market by a specified amount
JoinBid (6) Automatically adjusts to join the best bid
JoinAsk (7) Automatically adjusts to join the best ask
OrderSide
Value Description
Bid (0) Buy order. This is the live wire value — do not treat 0 as "unset"
Ask (1) Sell order

Order.Side, HalfTrade.Side, OrderUpdate.Side and TradeNotification.Side are OrderSide?. null means the venue omitted side. PlaceOrderRequest.Side stays required.

TradeLogType
Value Description
Buy (0) Buy-side print. This is the live wire value — do not treat 0 as "unknown"
Sell (1) Sell-side print

TradeUpdate.Type is TradeLogType?. null means the venue omitted type or sent a value that is neither 0 nor 1.

PositionType
Value Description
Undefined (0) Position direction is undefined
Long (1) Long (buy) position
Short (2) Short (sell) position
AggregateBarUnit
Value Description
Unspecified (0) Unspecified unit
Second (1) Second-based aggregation
Minute (2) Minute-based aggregation
Hour (3) Hour-based aggregation
Day (4) Day-based aggregation
Week (5) Week-based aggregation
Month (6) Month-based aggregation

REST API Models

TradingAccount
Property Type Description
Id int Unique account identifier
Name string Account name
Balance decimal Current account balance
CanTrade bool Whether this account is allowed to trade
IsVisible bool Whether this account is visible
Simulated bool Whether this is a simulated account
Contract
Property Type Description
Id string Unique contract identifier
Name string Contract name
Description string Contract description
TickSize decimal Minimum price increment
TickValue decimal Monetary value of one tick
ActiveContract bool Whether this is an active contract
SymbolId string Symbol identifier
Order
Property Type Description
Id long Unique order identifier
AccountId int Account that owns this order
ContractId string Contract identifier
SymbolId string Symbol identifier
CreationTimestamp DateTime When the order was created
UpdateTimestamp DateTime? When the order was last updated
Status OrderStatus Current order status
Type OrderType Order type
Side OrderSide? Bid (wire 0) or Ask (wire 1). null when the venue omitted side
Size int Order quantity
LimitPrice decimal? Limit price (for limit orders)
StopPrice decimal? Stop price (for stop orders)
FillVolume int Number of contracts filled
FilledPrice decimal? Average fill price
CustomTag string? Custom tag for the order
Position
Property Type Description
Id int Unique position identifier
AccountId int Account that owns this position
ContractId string Contract identifier
ContractDisplayName string? Human-readable contract name
CreationTimestamp DateTime When this position was created
Type PositionType Position direction (Long/Short)
Size int Number of contracts
AveragePrice decimal Volume-weighted average entry price
HalfTrade
Property Type Description
Id long Unique trade identifier
AccountId int Account that executed this trade
ContractId string Contract identifier
CreationTimestamp DateTime When the trade was executed
Price decimal Execution price
ProfitAndLoss decimal? Realized P&L for this trade leg
Fees decimal Fees charged
Side OrderSide? Bid (buy) or Ask (sell). null when the venue omitted side
Size int Number of contracts traded
Voided bool Whether this trade has been voided
OrderId long ID of the order that generated this trade
AggregateBar
Property Type Description
Timestamp DateTime Bar timestamp
Open decimal Opening price
High decimal Highest price during the period
Low decimal Lowest price during the period
Close decimal Closing price
Volume long Trading volume
PlaceOrderRequest
Property Type Required Description
AccountId int Yes Account identifier
ContractId string Yes Contract to trade
Type OrderType Yes Order type
Side OrderSide Yes Order side
Size int Yes Number of contracts
LimitPrice decimal? No Limit price (required for Limit/StopLimit orders)
StopPrice decimal? No Stop price (required for Stop/StopLimit orders)
TrailPrice decimal? No Trail amount (for TrailingStop orders)
CustomTag string? No Custom tag for order tracking
StopLossBracket PlaceOrderBracket? No Stop-loss bracket configuration
TakeProfitBracket PlaceOrderBracket? No Take-profit bracket configuration
PlaceOrderBracket
Property Type Description
Ticks int Number of ticks from the entry price
Type OrderType Bracket order type

Error Handling

The client provides two exception types:

AuthenticationException

Thrown when authentication fails. Common causes:

  • Invalid API key or secret
  • Network connectivity issues
  • API service unavailable

ProjectXApiException

Thrown when API requests fail. Includes:

  • HTTP status code (if available)
  • Detailed error message
  • Original exception as inner exception

Features

Automatic Token Management

  • Tokens are cached and automatically refreshed before expiration
  • Thread-safe token acquisition
  • 1-minute buffer before token expiration

Retry Policy with Polly

  • Automatic retry for transient failures: HttpRequestException, HTTP 429 (Too Many Requests), and HTTP 500+ server errors
  • Exponential backoff strategy (configurable initial and max delay)
  • Retry-After header is honored on 429 responses (both delta-seconds and HTTP-date formats)
  • Configurable retry attempts and delays

Logging

  • Uses ILogger<T> from Microsoft.Extensions.Logging
  • Compatible with any logging provider (Serilog, NLog, etc.)
  • Structured logging with proper log levels
  • Credentials are never logged

Thread Safety

  • All methods are thread-safe
  • Supports concurrent API calls
  • Safe for use in multi-threaded applications

Configuration Options

Every option below is read by a code path. Two are listed as inert on purpose and say why; nothing else here is decorative.

All keys sit under the ProjectX section.

Option Type Default Description
ApiKey string required Your ProjectX API key
ApiSecret string required Your ProjectX API secret
BaseUrl string https://api.topstepx.com Base URL for the REST API
RetryOptions.MaxRetries int 3 Retry attempts on a transient fault. Never applies to POST /api/Order/place
RetryOptions.InitialDelay TimeSpan 00:00:01 First backoff delay
RetryOptions.MaxDelay TimeSpan 00:00:30 Backoff ceiling
WebSocket.UserHubUrl string https://rtc.topstepx.com/hubs/user User hub URL
WebSocket.MarketHubUrl string https://rtc.topstepx.com/hubs/market Market hub URL
WebSocket.AutoReconnect bool true false stops reconnection entirely
WebSocket.InitialReconnectDelaySeconds int 1 First reconnect backoff
WebSocket.MaxReconnectDelaySeconds int 5 Reconnect backoff ceiling
WebSocket.HandshakeTimeoutSeconds int 15 SignalR handshake timeout
WebSocket.KeepAliveIntervalSeconds int 15 Keep-alive ping interval
WebSocket.ServerTimeoutSeconds int 30 Server timeout before the connection is considered dead
WebSocket.MaxBufferSize long 1048576 Transport and application buffer ceiling, in bytes

Pointing at a different venue

The hub URLs have two accepted spellings, and both work:

{
  "ProjectX": {
    "WebSocket": {
      "UserHubUrl":   "https://rtc.example.com/hubs/user",     // preferred
      "MarketHubUrl": "https://rtc.example.com/hubs/market"
    },
    "WebSocketUserHubUrl":   "https://rtc.example.com/hubs/user",   // also honoured
    "WebSocketMarketHubUrl": "https://rtc.example.com/hubs/market"
  }
}

The nested WebSocket: form wins when both are set. Through 2.0.0 only the nested form was read, while this table documented only the outer one — so a consumer following these docs to reach a simulation venue edited a key that did nothing and stayed connected to production. Both resolve now.

Options that intentionally do nothing

Option Why
ValidateSslCertificates TLS validation is always on and cannot be disabled. Honouring false would add a supported way to skip certificate validation against a live trading venue. Trust a self-signed endpoint at the host instead.
WebSocket.UseMessagePack The hubs always speak JSON. MessagePack needs a protocol package, and this library's dependency surface is part of its public contract.

Both are marked [Obsolete] and produce a compiler warning; neither has been removed, so nothing breaks.

Requirements

  • .NET 8.0 or .NET 10.0 — the package multi-targets both, and both are first-class
  • Valid ProjectX API credentials

License

MIT

Support

For issues and questions, please contact the development team.

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 was computed.  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.

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
3.0.0 2,717 8/29/2026
2.1.0 2,655 8/22/2026
2.0.0 104 8/22/2026
1.0.4 187 4/2/2026
1.0.3 389 3/29/2026
1.0.2 120 3/29/2026
1.0.1 189 3/28/2026
1.0.0 287 3/28/2026