Client
The MarketDataClient is the entry point to the SDK. It handles API requests, response parsing, rate-limit tracking, retries with exponential backoff, and logging. A single client gives you access to all five resources: stocks, options, funds, markets, and utilities.
The client is immutable, thread-safe, and AutoCloseable. Create one per application (it holds a shared HTTP client and a 50-request concurrency pool) and close it when you're done.
Get Started Quickly
- Review the authentication documentation to learn how to set your API token.
- Create a
MarketDataClientand use it to make requests. - Reach a resource through
client.stocks(),client.options(), etc. - Track your rate limit to see how many API credits remain.
- Configure Settings to customize output format, date format, and other universal parameters.
MarketDataClient
public final class MarketDataClient implements AutoCloseable {
// Production: resolves everything from the configuration cascade and
// validates the token on startup.
public MarketDataClient();
// Full control: any null falls back to the cascade / default for that slot.
public MarketDataClient(
@Nullable String apiKey,
@Nullable String baseUrl, // default: https://api.marketdata.app
@Nullable String apiVersion, // default: v1
boolean validateOnStartup);
// Resources
public StocksResource stocks();
public OptionsResource options();
public FundsResource funds();
public MarketsResource markets();
public UtilitiesResource utilities();
// Latest client-wide rate-limit snapshot (or null before the first request)
public @Nullable RateLimitSnapshot getRateLimits();
// Releases the shared HTTP client
@Override public void close();
}
Constructors
The no-arg constructor is what you'll use in production. It resolves the token and settings from the configuration cascade and fires a single GET /user/ request to validate the token before returning.
The four-arg constructor (apiKey, baseUrl, apiVersion, validateOnStartup) lets you set everything explicitly — handy for tests, short-lived runtimes, and serverless platforms where you want to skip the startup round-trip (validateOnStartup = false).
- Java
- Kotlin
import com.marketdata.sdk.MarketDataClient;
// Production: token from the cascade, validated on startup.
try (MarketDataClient client = new MarketDataClient()) {
// ...
}
// Explicit, no startup validation (e.g. on a Lambda cold start):
try (MarketDataClient client =
new MarketDataClient("your_token", null, null, false)) {
// ...
}
import com.marketdata.sdk.MarketDataClient
MarketDataClient().use { client ->
// ...
}
MarketDataClient("your_token", null, null, false).use { client ->
// ...
}
A misconfigured base URL or API version is rejected with an IllegalArgumentException at construction, not later on the first request. Tokens are never printed in full — the client's toString() redacts them (long tokens show only the last four characters; short ones are hidden entirely).
Sync and Async
Every endpoint comes in two variants: a blocking call (e.g. status()) and an async one (e.g. statusAsync()) returning a CompletableFuture. They share the same validation, retry, and rate-limit logic — pick whichever fits your code. Internally the SDK is async-first; the sync method is a thin wrapper that joins the future and unwraps the underlying exception.
Async pays off when you fan out several requests: total time is about the slowest single call, not the sum.
- Java
- Kotlin
import com.marketdata.sdk.MarketDataClient;
import com.marketdata.sdk.UtilitiesStatusResponse;
import java.util.concurrent.CompletableFuture;
try (MarketDataClient client = new MarketDataClient()) {
// Sync — blocks and returns the typed response.
var sync = client.utilities().status();
System.out.println(sync.values().size() + " services");
// Async — returns immediately with a future. Attach a callback, or join().
CompletableFuture<UtilitiesStatusResponse> future = client.utilities().statusAsync();
future.thenAccept(resp -> System.out.println(resp.values().size() + " services"));
// Fan out several calls in parallel and wait for all of them.
var a = client.utilities().statusAsync();
var b = client.utilities().statusAsync();
CompletableFuture.allOf(a, b).join();
}
import com.marketdata.sdk.MarketDataClient
import java.util.concurrent.CompletableFuture
MarketDataClient().use { client ->
// Sync
val sync = client.utilities().status()
println("${sync.values().size} services")
// Async — returns a CompletableFuture (coroutine users can await() it
// via kotlinx-coroutines-jdk8).
client.utilities().statusAsync()
.thenAccept { resp -> println("${resp.values().size} services") }
.join()
}
Composing async calls
The point of the async variants is to stay non-blocking — chain off the future instead of calling join() in the middle. Use thenApply to transform a result, thenCompose to chain a dependent call, and fire independent calls together when they don't depend on each other.
- Java
- Kotlin
import com.marketdata.sdk.options.OptionsLookupRequest;
import com.marketdata.sdk.options.OptionsQuoteRequest;
import com.marketdata.sdk.stocks.StockQuoteRequest;
import java.util.concurrent.CompletableFuture;
// thenApply — transform the result without blocking.
CompletableFuture<Double> lastPrice = client.stocks()
.quoteAsync(StockQuoteRequest.of("AAPL"))
.thenApply(resp -> resp.values().get(0).last());
// thenCompose — chain a dependent async call (resolve a symbol, then quote it).
var optionQuote = client.options()
.lookupAsync(OptionsLookupRequest.of("AAPL 1/16/2026 $200 Call"))
.thenCompose(sym -> client.options().quoteAsync(OptionsQuoteRequest.of(sym.values())));
// Independent calls in parallel, then use every result.
var aapl = client.stocks().quoteAsync(StockQuoteRequest.of("AAPL"));
var msft = client.stocks().quoteAsync(StockQuoteRequest.of("MSFT"));
CompletableFuture.allOf(aapl, msft).join(); // wait for both
double spread = aapl.join().values().get(0).last() - msft.join().values().get(0).last();
import com.marketdata.sdk.options.OptionsLookupRequest
import com.marketdata.sdk.options.OptionsQuoteRequest
import com.marketdata.sdk.stocks.StockQuoteRequest
import java.util.concurrent.CompletableFuture
// thenApply — transform the result without blocking.
val lastPrice = client.stocks()
.quoteAsync(StockQuoteRequest.of("AAPL"))
.thenApply { resp -> resp.values()[0].last() }
// thenCompose — chain a dependent async call.
val optionQuote = client.options()
.lookupAsync(OptionsLookupRequest.of("AAPL 1/16/2026 \$200 Call"))
.thenCompose { sym -> client.options().quoteAsync(OptionsQuoteRequest.of(sym.values())) }
// Independent calls in parallel, then use every result.
val aapl = client.stocks().quoteAsync(StockQuoteRequest.of("AAPL"))
val msft = client.stocks().quoteAsync(StockQuoteRequest.of("MSFT"))
CompletableFuture.allOf(aapl, msft).join()
val spread = aapl.join().values()[0].last()!! - msft.join().values()[0].last()!!
Handling errors in async
When a request fails, the future completes exceptionally. Inside exceptionally(), handle(), or whenComplete(), the throwable you receive is a java.util.concurrent.CompletionException — call getCause() to reach the underlying MarketDataException. (The blocking variants do this unwrapping for you, which is why try/catch around a sync call catches the MarketDataException directly.)
- Java
- Kotlin
import com.marketdata.sdk.exception.RateLimitError;
import com.marketdata.sdk.stocks.StockQuoteRequest;
import java.util.concurrent.CompletionException;
client.stocks().quoteAsync(StockQuoteRequest.of("AAPL"))
.thenApply(resp -> resp.values().get(0).last())
.exceptionally(ex -> {
// `ex` is a CompletionException; the real cause is the MarketDataException.
Throwable cause = (ex instanceof CompletionException && ex.getCause() != null)
? ex.getCause() : ex;
if (cause instanceof RateLimitError) {
System.out.println("Rate limited — backing off");
}
return null; // fallback value the rest of the chain will see
})
.join();
// handle((value, throwable) -> ...) sees both outcomes in one place; use it when
// you want to map success and failure to the same result type.
import com.marketdata.sdk.exception.RateLimitError
import com.marketdata.sdk.stocks.StockQuoteRequest
import java.util.concurrent.CompletionException
client.stocks().quoteAsync(StockQuoteRequest.of("AAPL"))
.thenApply { resp -> resp.values()[0].last() }
.exceptionally { ex ->
val cause = if (ex is CompletionException && ex.cause != null) ex.cause else ex
if (cause is RateLimitError) println("Rate limited — backing off")
null
}
.join()
join() vs get()To block on a future, prefer join() — it throws an unchecked CompletionException. get() does the same thing but throws the checked ExecutionException (plus InterruptedException), forcing a try/catch. Both wrap the underlying cause identically, so join() is usually the friendlier choice.
The Response Object
Every endpoint returns a typed response that wraps the decoded data plus request metadata, with a uniform surface regardless of endpoint or format.
public interface MarketDataResponse<T> {
T values(); // the typed payload (e.g. List<StockQuote>)
int statusCode(); // 200, 203, or 404
boolean isNoData(); // true on a 404 "no_data" response
@Nullable String requestId(); // server request id, for support tickets
java.net.URI requestUrl(); // the absolute request URL
@Nullable RateLimitSnapshot rateLimit(); // this request's rate limit
String json(); // the raw response body, as sent
boolean isJson();
boolean isCsv();
boolean isHtml();
void saveToFile(java.nio.file.Path path); // write the raw body verbatim
}
var response = client.stocks().quote(StockQuoteRequest.of("AAPL"));
response.values(); // List<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)
response.saveToFile(Path.of("aapl.json")); // cache the raw body
Error Handling
Everything the SDK throws is a MarketDataException. It is a sealed hierarchy — the seven subtypes below are the complete set, so you can branch on them exhaustively and the compiler will tell you if a future major version adds one. Each exception carries support context: getStatusCode(), getRequestId(), getRequestUrl(), and getSupportInfo().
| Subtype | When it's thrown |
|---|---|
AuthenticationError | Missing or invalid token (HTTP 401) |
BadRequestError | Invalid parameters (HTTP 4xx) |
NotFoundError | The resource doesn't exist (HTTP 404) |
RateLimitError | Quota exceeded (HTTP 429); see getRetryAfter() |
ServerError | API-side failure (HTTP 5xx) |
NetworkError | Connection failure or timeout |
ParseError | The response could not be decoded |
Async calls surface the same exceptions through the CompletableFuture (wrapped in a CompletionException); the sync wrappers unwrap them so you catch the cause directly.
- Java (JDK 17)
- Java (JDK 21+)
- Kotlin
import com.marketdata.sdk.exception.*;
try {
var quote = client.stocks().quote(StockQuoteRequest.of("AAPL"));
} catch (AuthenticationError e) {
System.out.println("Check your token");
} catch (RateLimitError e) {
System.out.println("Rate limited — retry after "
+ e.getRetryAfter().map(d -> d.toSeconds() + "s").orElse("a moment"));
} catch (MarketDataException e) {
// Any other case. Attach getSupportInfo() to a bug report.
System.out.println(e.getSupportInfo());
}
import com.marketdata.sdk.exception.*;
// On JDK 21+ the sealed hierarchy enables an exhaustive switch — the compiler
// rejects the code if any subtype is left unhandled.
try {
var quote = client.stocks().quote(StockQuoteRequest.of("AAPL"));
} catch (MarketDataException e) {
String message = switch (e) {
case AuthenticationError a -> "Authentication failed — check your token";
case BadRequestError b -> "Bad request — check your parameters";
case NotFoundError n -> "Not found";
case RateLimitError r -> "Rate limited";
case ServerError s -> "Server error (HTTP " + s.getStatusCode() + ")";
case NetworkError n -> "Network problem — is the API reachable?";
case ParseError p -> "Could not parse the response";
};
System.out.println(message);
}
import com.marketdata.sdk.exception.*
try {
val quote = client.stocks().quote(StockQuoteRequest.of("AAPL"))
} catch (e: AuthenticationError) {
println("Check your token")
} catch (e: RateLimitError) {
println("Rate limited")
} catch (e: MarketDataException) {
println(e.supportInfo)
}
Rate Limits
The SDK tracks your rate limit from the x-api-ratelimit-* headers on every response.
- Per-request:
response.rateLimit()reflects the headers of that specific response. - Client-wide:
client.getRateLimits()returns the latest snapshot seen across all requests.
public record RateLimitSnapshot(int limit, int remaining, Instant reset, int consumed) {}
var response = client.stocks().quote(StockQuoteRequest.of("AAPL"));
var rl = response.rateLimit();
if (rl != null) {
System.out.println(rl.remaining() + " / " + rl.limit() + " requests left");
}
The SDK also fails fast: if a prior response reported remaining = 0 and the reset window has not yet elapsed, the next request raises a RateLimitError before hitting the network.
Closing the Client
MarketDataClient is AutoCloseable. Prefer try-with-resources (Java) or use {} (Kotlin) so the underlying HTTP client is released. For a long-lived, application-scoped client, call close() on shutdown.