Client
The MarketDataClient is the entry point for the SDK. It groups the API into five resources — Stocks, Options, Funds, Markets, and Utilities — and owns the cross-cutting behavior: configuration loading, authentication, retries, timeouts, and rate-limit tracking.
Get Started Quickly
using MarketDataApp;
// Reads MARKETDATA_* from the environment, validates the token, seeds the rate limits.
using var client = await MarketDataClient.CreateAsync();
var quote = (await client.Stocks.GetQuoteAsync("AAPL")).Values[0];
Console.WriteLine(quote); // AAPL mid=311.94 last=311.92
MarketDataClient
Two ways to construct
Every entry point exists in two flavors: an async factory (CreateAsync) and a constructor. Both perform the same startup token validation when a token is configured; the difference is only whether that validation blocks the calling thread.
| Entry point | Startup validation | Use when |
|---|---|---|
await MarketDataClient.CreateAsync(...) | Asynchronous | Anywhere you can await — the recommended default. |
new MarketDataClient(...) | Blocking | Synchronous hosts and dependency-injection factories that cannot await. |
Both accept an optional MarketDataClientOptions. When omitted, options are loaded from the configuration cascade (user secrets, .env, environment variables).
Two ways to own the HttpClient
The SDK never disposes an HttpClient it did not create, and never reconfigures one you hand it.
- SDK-owned (default). The overloads without an
HttpClientparameter create one backed byMarketDataClient.CreateDefaultHttpHandler()(2-second connection timeout, pooled-connection rotation) with theHttpClient-level timeout disabled, because the SDK enforces its own fixed 99-second request timeout.Dispose()also disposes the owned client. - Application-owned. Supply your own
HttpClient— for example one fromIHttpClientFactory— and the application controls its lifetime. Its handler andTimeoutare respected as-is.
// SDK-owned transport (simplest).
using var client = await MarketDataClient.CreateAsync(options);
// Application-owned transport. CreateDefaultHttpHandler() supplies the 2-second
// connect timeout; a plain new HttpClient() works too.
using var httpClient = new HttpClient(MarketDataClient.CreateDefaultHttpHandler());
var client2 = await MarketDataClient.CreateAsync(httpClient, options);
Dependency injection (ASP.NET Core)
Register the client with one line. AddMarketDataClient lives in the Microsoft.Extensions.DependencyInjection namespace, so it is discoverable from Program.cs without an extra using:
// Program.cs
builder.Services.AddMarketDataClient(builder.Configuration);
Then take MarketDataClient as a constructor-injected dependency or a minimal-API parameter:
app.MapGet("/quote/{symbol}", async (string symbol, MarketDataClient client, CancellationToken ct) =>
Results.Ok((await client.Stocks.GetQuoteAsync(symbol, cancellationToken: ct)).Values));
AddMarketDataClient registers MarketDataClient as a singleton over an IHttpClientFactory-managed HttpClient backed by the SDK's default handler. Three overloads resolve the options for you:
| Overload | Options source |
|---|---|
AddMarketDataClient() | MarketDataClientOptions.FromEnvironment() — user secrets, .env, environment variables |
AddMarketDataClient(IConfiguration configuration) | MarketDataClientOptions.FromConfiguration(configuration) — the MARKETDATA_* keys |
AddMarketDataClient(MarketDataClientOptions options) | the supplied instance |
Registrations use TryAdd*, so you can override either the options or the client by registering your own first. When your app has logging configured, the container's ILogger<MarketDataClient> is auto-wired into the SDK. Because the DI path uses the constructor, a configured token is validated with a blocking GET /user/ the first time the singleton is resolved — typically during startup — so an invalid token fails the application at startup rather than on the first request. Register options with ValidateTokenOnStartup = false to defer that.
Async and Cancellation
Every endpoint method is asynchronous, returns a Task<TResponse>, and accepts an optional CancellationToken as its last parameter. There are no synchronous endpoint methods.
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var quotes = await client.Stocks.GetQuotesAsync(
["AAPL", "MSFT", "GOOG"],
cancellationToken: timeout.Token);
Caller cancellation surfaces as OperationCanceledException, which is deliberately not part of the SDK's exception hierarchy — it means you stopped the request. An SDK timeout surfaces as NetworkException.
Composing async calls
Endpoint calls are ordinary tasks, so they compose with Task.WhenAll. All requests share the client's retry, rate-limit, and concurrency behavior; MaxConcurrentRequests (default 50) bounds how many are in flight at once.
var aaplTask = client.Stocks.GetQuoteAsync("AAPL");
var msftTask = client.Stocks.GetQuoteAsync("MSFT");
var statusTask = client.Markets.GetStatusAsync("US");
await Task.WhenAll(aaplTask, msftTask, statusTask);
var aapl = aaplTask.Result.Values[0]; // safe: the tasks are already complete
var msft = msftTask.Result.Values[0];
var status = statusTask.Result.Values[0];
Scalar Parameters and Request Objects
Every endpoint has two overloads. The scalar overload takes the common parameters directly and is the fastest way to make a call:
var candles = await client.Stocks.GetCandlesAsync(StockResolution.Daily, "AAPL", countback: 30);
The request-object overload takes an immutable request record. Required values are constructor arguments; optional values are init properties. Use it when several optional filters should be grouped or reused:
var request = new StockCandlesRequest(StockResolution.Daily, "AAPL")
{
Countback = 30,
AdjustDividends = true
};
var candles = await client.Stocks.GetCandlesAsync(request);
Both overloads also accept an optional MarketDataRequestOptions for the universal parameters (date format, mode, columns, limit, offset).
The Response Object
Every endpoint returns a typed response record derived from MarketDataResponse<T>, wrapping the decoded data plus request metadata, with a uniform surface regardless of endpoint or format.
public abstract record MarketDataResponse<T>
{
public T Values { get; } // the typed payload (e.g. IReadOnlyList<StockQuote>)
public int StatusCode { get; } // 200, 203, or 404
public bool IsNoData { get; } // true on a 404 "no_data" response
public string? RequestId { get; } // server request id, for support tickets
public Uri RequestUrl { get; } // the absolute request URL
public RateLimitSnapshot? RateLimit { get; } // this request's rate limit
public IReadOnlyList<MarketDataResponsePart> Parts { get; } // one per HTTP request
public bool IsComposite { get; } // assembled from several HTTP responses
public string RawBody { get; } // the raw response body, as sent
public bool IsJson { get; }
public bool IsCsv { get; }
public bool IsHtml { get; }
public string SaveToFile(string path); // write the body; returns the path
public Task<string> SaveToFileAsync(string path, CancellationToken cancellationToken = default);
}
var response = await client.Stocks.GetQuoteAsync("AAPL");
response.Values; // IReadOnlyList<StockQuote> — the part you usually want
response.StatusCode; // 200
response.RequestId; // e.g. for a support ticket
response.RateLimit; // this request's rate-limit snapshot (may be null)
await response.SaveToFileAsync("aapl.json"); // cache the raw body
SaveToFile chooses the representation from the file extension (.json, .csv, .html) when the response can provide it, and falls back to the raw body otherwise.
Error Handling
Everything the SDK throws is a MarketDataException. The seven subtypes below are the complete set. Each exception carries support context — StatusCode, RequestId, RequestUrl, Timestamp, and ExceptionType — plus a preformatted SupportInfo block ready to paste into a support ticket.
| Subtype | When it's thrown |
|---|---|
AuthenticationException | Missing or invalid token (HTTP 401 / 403) |
BadRequestException | Invalid parameters (HTTP 400) |
NotFoundException | The resource doesn't exist (HTTP 404 that is not a "no data" response) |
RateLimitException | Quota exceeded (HTTP 429), or the client-side rate-limit guard refused to send; see RetryAfter |
ServerException | API-side failure (HTTP 5xx); see RetryAfter |
NetworkException | Connection failure or timeout |
ParseException | The response could not be decoded |
Request-shape mistakes caught before any HTTP call — an invalid date window, an unsupported option on a typed method — throw ArgumentException, the standard .NET signal for a bad argument.
using MarketDataApp.Exceptions;
try
{
var quote = await client.Stocks.GetQuoteAsync("AAPL");
}
catch (AuthenticationException)
{
Console.WriteLine("Check your token");
}
catch (RateLimitException e)
{
var wait = e.RetryAfter is { } delay ? $"{delay.TotalSeconds:F0}s" : "a moment";
Console.WriteLine($"Rate limited — retry after {wait}");
}
catch (MarketDataException e)
{
// Any other case. Attach SupportInfo to a bug report.
Console.WriteLine(e.SupportInfo);
}
Retries and Timeouts
Transient failures — NetworkException and HTTP 501–599 — are retried automatically up to MaxRetries times (default 3) with a fixed exponential backoff (1s, 2s, 4s, capped at 30s). A server-supplied Retry-After header is honored. Authentication, validation, parsing, and rate-limit failures are never retried; before retrying a server error the SDK also consults the cached /status/ reading and skips the retry if the API reports itself offline.
Every HTTP attempt has a fixed 99-second request timeout; SDK-owned and DI-registered transports add a 2-second connection timeout through the default handler. Neither is configurable — MaxRetries is the only retry knob.
Rate Limits
The API reports your quota in x-api-ratelimit-* response headers. The SDK captures them two ways:
- Per response:
response.RateLimit— the snapshot returned with that request. - Client-wide:
client.LatestRateLimit— the most recent complete snapshot the client has seen, seeded by the startupGET /user/when a token is configured.
if (client.LatestRateLimit is { } limit)
{
Console.WriteLine($"{limit.Remaining}/{limit.Limit} remaining, resets {limit.Reset:HH:mm}");
}
RateLimitSnapshot exposes Limit, Remaining, Reset (a DateTimeOffset), and Consumed. When the latest snapshot shows the quota exhausted and the reset time has not passed, the client refuses to send further requests and throws RateLimitException immediately, so you never burn calls that would fail anyway.
Disposing the Client
MarketDataClient implements IDisposable. Disposing it releases its internal resources and — only when the SDK created the transport — the owned HttpClient. An HttpClient you supplied is never disposed. A single client instance is safe to use concurrently from multiple threads or requests, so the typical pattern is one long-lived client per application:
using var client = await MarketDataClient.CreateAsync();
// ... use for the lifetime of the app