+ [](/sdk/java)
+ ### [Java SDK](/sdk/java)
+ [Typed Market Data integration for JVM applications, with first-class Kotlin interop.](/sdk/java)
+
+
Every SDK is crafted for ease of use, guaranteeing a straightforward setup process with minimal configuration required.
diff --git a/sdk/java/authentication.mdx b/sdk/java/authentication.mdx
new file mode 100644
index 0000000..b9caa7e
--- /dev/null
+++ b/sdk/java/authentication.mdx
@@ -0,0 +1,142 @@
+---
+title: Authentication
+sidebar_position: 2
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+The Market Data API uses a **Bearer Token** for authentication. The token is required for almost every request. Your token should have been e-mailed to you when you first signed up for an account. If you do not have a token or have lost your sign-up email, request a new token from the [Market Data Dashboard](https://www.marketdata.app/dashboard/).
+
+There are three ways to set your token when using the Java SDK:
+
+1. Set it from an environment variable _(recommended for production)_
+2. Load it from a `.env` file _(recommended for local development)_
+3. Pass it directly when creating the [client](/sdk/java/client)
+
+On startup, the SDK looks for the `MARKETDATA_TOKEN` environment variable. If found, it uses that token for all requests. The SDK also loads a `.env` file automatically from the working directory if one is present.
+
+:::tip
+When your code is running in a production environment, we recommend using an environment variable to ensure your token is not stored with your code. This is the most secure way to set your token.
+:::
+
+## How To Set Up The Environment Variable
+
+### Set The Environment Variable In The Console
+
+
+
+
+This command sets the environment variable for the current session only. If you open a new terminal or restart your computer, it will not persist.
+
+```bash
+export MARKETDATA_TOKEN="your_api_token"
+```
+
+#### Make The Variable Persistent
+
+Add the `export` line to your shell's profile script (`~/.zshrc`, `~/.bashrc`, `~/.bash_profile`, etc.), then restart your terminal or run `source ~/.zshrc` (adjusting for your shell).
+
+
+
+
+`setx` sets the variable permanently, but it is not available in the current Command Prompt session — open a new one after running it.
+
+```bash
+setx MARKETDATA_TOKEN "your_api_token"
+```
+
+
+
+
+### Using a .env File
+
+The SDK automatically loads a `.env` file from your working directory at startup. Create a file named `.env` in your project root:
+
+```env title=".env"
+MARKETDATA_TOKEN=your_api_token
+```
+
+:::warning
+Add `.env` to your `.gitignore` so the token is not committed to source control.
+:::
+
+### Make A Test Request
+
+Verify your authentication is working by making a test request against `SPY` (or any symbol that requires authentication). Do **not** use `AAPL` to test authentication — `AAPL` is a free test symbol and returns data even when you are not authenticated.
+
+
+
+
+```java
+import com.marketdata.sdk.MarketDataClient;
+import com.marketdata.sdk.stocks.StockQuoteRequest;
+import com.marketdata.sdk.exception.AuthenticationError;
+
+// No need to pass a token here — the SDK reads MARKETDATA_TOKEN automatically.
+try (MarketDataClient client = new MarketDataClient()) {
+ var quote = client.stocks().quote(StockQuoteRequest.of("SPY")).values().get(0);
+ System.out.println(quote.symbol() + " last=" + quote.last());
+} catch (AuthenticationError e) {
+ System.out.println("Authentication failed: " + e.getMessage());
+}
+```
+
+
+
+
+```kotlin
+import com.marketdata.sdk.MarketDataClient
+import com.marketdata.sdk.stocks.StockQuoteRequest
+import com.marketdata.sdk.exception.AuthenticationError
+
+MarketDataClient().use { client ->
+ try {
+ val quote = client.stocks().quote(StockQuoteRequest.of("SPY")).values()[0]
+ println("${quote.symbol()} last=${quote.last()}")
+ } catch (e: AuthenticationError) {
+ println("Authentication failed: ${e.message}")
+ }
+}
+```
+
+
+
+
+## Passing the Token Directly
+
+If you prefer to pass the token explicitly (not recommended for production code), use the four-argument constructor `(apiKey, baseUrl, apiVersion, validateOnStartup)`. Pass `null` for any slot you want resolved from the cascade or left at its default.
+
+
+
+
+```java
+import com.marketdata.sdk.MarketDataClient;
+
+try (MarketDataClient client =
+ new MarketDataClient("your_token_here", null, null, true)) {
+ // ... make requests
+}
+```
+
+
+
+
+```kotlin
+import com.marketdata.sdk.MarketDataClient
+
+MarketDataClient("your_token_here", null, null, true).use { client ->
+ // ... make requests
+}
+```
+
+
+
+
+:::info Demo mode
+If no token is found anywhere in the cascade, the SDK runs in **demo mode** — startup validation is skipped and you can call the free, public endpoints (such as `AAPL` quotes and `utilities().status()`).
+:::
+
+## Next Steps
+
+After successful authentication, read the overview of how the [client](/sdk/java/client) works, then configure [Settings](/sdk/java/settings) to customize output format, date format, and other universal parameters.
diff --git a/sdk/java/client.mdx b/sdk/java/client.mdx
new file mode 100644
index 0000000..f24caf6
--- /dev/null
+++ b/sdk/java/client.mdx
@@ -0,0 +1,399 @@
+---
+title: Client
+sidebar_position: 3
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+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
+
+1. Review the [authentication documentation](/sdk/java/authentication) to learn how to set your API token.
+2. Create a [`MarketDataClient`](#MarketDataClient) and use it to make requests.
+3. Reach a resource through `client.stocks()`, `client.options()`, etc.
+4. Track your [rate limit](#rate-limits) to see how many API credits remain.
+5. Configure [Settings](/sdk/java/settings) to customize output format, date format, and other universal parameters.
+
+
+## MarketDataClient
+
+```java
+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](/sdk/java/settings#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
+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)) {
+ // ...
+}
+```
+
+
+
+
+```kotlin
+import com.marketdata.sdk.MarketDataClient
+
+MarketDataClient().use { client ->
+ // ...
+}
+
+MarketDataClient("your_token", null, null, false).use { client ->
+ // ...
+}
+```
+
+
+
+
+:::info Fail-fast configuration
+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
+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 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();
+}
+```
+
+
+
+
+```kotlin
+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
+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 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();
+```
+
+
+
+
+```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.
+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
+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.
+```
+
+
+
+
+```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()[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()
+```
+
+
+
+
+:::note `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.
+
+```java
+public interface MarketDataResponse {
+ T values(); // the typed payload (e.g. List)
+ 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
+}
+```
+
+```java
+var response = client.stocks().quote(StockQuoteRequest.of("AAPL"));
+
+response.values(); // List — 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
+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());
+}
+```
+
+
+
+
+```java
+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);
+}
+```
+
+
+
+
+```kotlin
+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.
+
+```java
+public record RateLimitSnapshot(int limit, int remaining, Instant reset, int consumed) {}
+```
+
+```java
+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.
diff --git a/sdk/java/funds/candles.mdx b/sdk/java/funds/candles.mdx
new file mode 100644
index 0000000..82bc081
--- /dev/null
+++ b/sdk/java/funds/candles.mdx
@@ -0,0 +1,105 @@
+---
+title: Candles
+sidebar_position: 1
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+Retrieve a mutual fund's NAV (net asset value) OHLC series.
+
+## Making Requests
+
+Use the `candles()` method on the `funds` resource, built with `FundCandlesRequest`. Funds report NAV, not traded volume, so there is **no volume column**, and only daily-and-coarser resolutions are available (no intraday).
+
+```java
+FundCandlesResponse candles(FundCandlesRequest request)
+CompletableFuture candlesAsync(FundCandlesRequest request)
+```
+
+### FundCandlesRequest
+
+```java
+FundCandlesRequest.of(FundResolution resolution, String symbol)
+FundCandlesRequest.builder(FundResolution resolution, String symbol)
+ .date(LocalDate date) // a single day
+ .from(LocalDate from) // window start (inclusive)
+ .to(LocalDate to) // window end (exclusive)
+ .countback(int n) // N candles before `to`
+ .build()
+```
+
+### FundResolution
+
+A value type for the candle interval — daily and coarser only:
+
+```java
+FundResolution.DAILY // also WEEKLY, MONTHLY, YEARLY
+FundResolution.days(1)
+FundResolution.weeks(1)
+FundResolution.of("D") // any raw wire token
+```
+
+#### Returns
+
+`FundCandlesResponse` wrapping `List`:
+
+```java
+public record FundCandle(
+ @Nullable ZonedDateTime time, // bar opening moment (America/New_York)
+ @Nullable Double open, // NAV at open
+ @Nullable Double high,
+ @Nullable Double low,
+ @Nullable Double close) // NAV at close — note: no volume
+```
+
+## Examples
+
+
+
+
+```java
+import com.marketdata.sdk.MarketDataClient;
+import com.marketdata.sdk.funds.FundCandle;
+import com.marketdata.sdk.funds.FundCandlesRequest;
+import com.marketdata.sdk.funds.FundResolution;
+import java.time.LocalDate;
+
+try (MarketDataClient client = new MarketDataClient()) {
+ var candles = client.funds().candles(
+ FundCandlesRequest.builder(FundResolution.DAILY, "VFINX")
+ .from(LocalDate.now().minusWeeks(2))
+ .to(LocalDate.now())
+ .build());
+
+ for (FundCandle bar : candles.values()) {
+ System.out.printf("%s O=%.2f H=%.2f L=%.2f C=%.2f%n",
+ bar.time(), bar.open(), bar.high(), bar.low(), bar.close());
+ }
+}
+```
+
+
+
+
+```kotlin
+import com.marketdata.sdk.MarketDataClient
+import com.marketdata.sdk.funds.FundCandlesRequest
+import com.marketdata.sdk.funds.FundResolution
+import java.time.LocalDate
+
+MarketDataClient().use { client ->
+ val candles = client.funds().candles(
+ FundCandlesRequest.builder(FundResolution.DAILY, "VFINX")
+ .from(LocalDate.now().minusWeeks(2))
+ .to(LocalDate.now())
+ .build())
+
+ for (bar in candles.values()) {
+ println("${bar.time()} close=${bar.close()}")
+ }
+}
+```
+
+
+
diff --git a/sdk/java/funds/index.mdx b/sdk/java/funds/index.mdx
new file mode 100644
index 0000000..d4dc5b5
--- /dev/null
+++ b/sdk/java/funds/index.mdx
@@ -0,0 +1,16 @@
+---
+title: Funds
+slug: /java/funds
+sidebar_position: 30
+---
+
+The Java SDK from Market Data provides methods to streamline your use of the Funds endpoints. These methods provide a typed interface over the underlying HTTP requests and responses, with both sync and async variants.
+
+Reach the resource through `client.funds()`. For CSV output use `client.funds().asCsv()`.
+
+## Funds Endpoints
+
+import DocCardList from "@theme/DocCardList";
+import { useCurrentSidebarCategory } from "@docusaurus/theme-common";
+
+
diff --git a/sdk/java/index.mdx b/sdk/java/index.mdx
new file mode 100644
index 0000000..d3dbb12
--- /dev/null
+++ b/sdk/java/index.mdx
@@ -0,0 +1,77 @@
+---
+title: Java SDK
+sidebar_position: 6
+slug: /java
+sidebar_custom_props:
+ badge: n
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+Welcome to the Market Data Java SDK documentation. This SDK lets you integrate Market Data services into any JVM application. It ships typed, records-based responses, sync **and** async (`CompletableFuture`) variants for every endpoint, automatic retries and rate-limit tracking, and a sealed exception hierarchy you can branch on exhaustively.
+
+The SDK is written in Java (JDK 17+) with no Kotlin runtime dependency, but it is designed to be **idiomatic from Kotlin too**: nullability is annotated (no platform types), the client is `AutoCloseable` (so `use {}` works), and async methods return `CompletableFuture`, which Kotlin interops with directly.
+
+## Quick Start
+
+
+
+
+```java
+import com.marketdata.sdk.MarketDataClient;
+import com.marketdata.sdk.stocks.StockQuoteRequest;
+
+// The no-arg constructor reads MARKETDATA_TOKEN from the environment (or a .env file)
+// and validates it on startup. The client is AutoCloseable.
+try (MarketDataClient client = new MarketDataClient()) {
+
+ // A single quote — quote(...) returns a list; a single symbol is row 0.
+ var quote = client.stocks().quote(StockQuoteRequest.of("AAPL")).values().get(0);
+ System.out.println(quote.symbol() + " last=" + quote.last());
+}
+```
+
+
+
+
+```kotlin
+import com.marketdata.sdk.MarketDataClient
+import com.marketdata.sdk.stocks.StockQuoteRequest
+
+// `use {}` closes the client when the block ends — the Kotlin equivalent of try-with-resources.
+MarketDataClient().use { client ->
+ val quote = client.stocks().quote(StockQuoteRequest.of("AAPL")).values()[0]
+ println("${quote.symbol()} last=${quote.last()}")
+}
+```
+
+
+
+
+## Open Source
+
+The SDK is open source and available on GitHub. Feel free to contribute to the project, report bugs, or request new features.
+
+- [Java SDK on GitHub](https://github.com/MarketDataApp/sdk-java/)
+
+## Documentation
+
+The best source for documentation on the SDK is right here. This documentation is the most up-to-date and accurate source of information on the SDK.
+
+## Using the SDK
+
+This SDK is designed to help you get up and running with Market Data's APIs as quickly as possible, providing all the tools you need to access real-time stock and options prices, historical data, and much more.
+
+### Getting Started
+
+1. [Install the SDK](/sdk/java/installation) into a Gradle or Maven project.
+2. Set up your [authentication token](/sdk/java/authentication) to access the API.
+3. Learn about the [client](/sdk/java/client) and how to make your first API requests.
+4. Configure [Settings](/sdk/java/settings) to customize output format, date format, and other universal parameters.
+5. Explore the available endpoints for [stocks](/sdk/java/stocks), [options](/sdk/java/options), [funds](/sdk/java/funds), [markets](/sdk/java/markets), and [utilities](/sdk/java/utilities).
+
+### Support
+
+- If you have any questions or need further assistance, please don't hesitate to open a ticket at our [helpdesk](https://www.marketdata.app/dashboard/).
+- If you find a bug you may also [open an issue in our GitHub repository](https://github.com/MarketDataApp/sdk-java/issues). Please only open issues if you find a bug. Use our helpdesk for general questions or implementation assistance.
diff --git a/sdk/java/installation.mdx b/sdk/java/installation.mdx
new file mode 100644
index 0000000..25faa6a
--- /dev/null
+++ b/sdk/java/installation.mdx
@@ -0,0 +1,84 @@
+---
+title: Installation
+sidebar_position: 1
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+This guide will help you install the Market Data Java SDK and configure it for your project.
+
+## Prerequisites
+
+- **JDK 17 or newer.** The SDK is compiled for Java 17 and is tested on JDK 17, 21, and 25.
+- A build tool: [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/).
+
+## Basic Installation
+
+Add the SDK as a dependency. The published artifact is a single JAR; it does **not** pull in a Kotlin standard library or a third-party HTTP client.
+
+
+
+
+```kotlin
+// build.gradle.kts
+dependencies {
+ implementation("app.marketdata:marketdata-sdk-java:0.1.0")
+}
+```
+
+
+
+
+```groovy
+// build.gradle
+dependencies {
+ implementation 'app.marketdata:marketdata-sdk-java:0.1.0'
+}
+```
+
+
+
+
+```xml
+
+ app.marketdata
+ marketdata-sdk-java
+ 0.1.0
+
+```
+
+
+
+
+:::tip
+Always check the [latest release](https://github.com/MarketDataApp/sdk-java/releases) for the current version number.
+:::
+
+:::note
+The Maven **groupId** is `app.marketdata` (the published Maven Central namespace), while the Java **package** you import from is `com.marketdata.sdk` — for example `import com.marketdata.sdk.MarketDataClient;`. The two are intentionally different.
+:::
+
+## Kotlin Support
+
+Although the SDK is written in Java, Kotlin consumers are a first-class audience:
+
+- The public API is null-annotated, so Kotlin sees real nullable/non-null types — not platform types (`String!`).
+- `MarketDataClient` is `AutoCloseable`, so you can use `client.use { ... }`.
+- Async methods return `java.util.concurrent.CompletableFuture`. If you use coroutines, bridge it with `kotlinx-coroutines-jdk8`'s `await()` — the SDK does **not** depend on coroutines itself.
+
+## Requirements & Dependencies
+
+The SDK keeps its dependency footprint small:
+
+- **No third-party HTTP client.** It uses the JDK's built-in `java.net.http.HttpClient` (HTTP/2).
+- **Jackson** (`jackson-databind`) for JSON decoding.
+- **JSpecify** nullability annotations — compile-time only, with no runtime cost.
+
+## Next Steps
+
+After installation, you'll need to:
+
+1. Set up your [authentication token](/sdk/java/authentication).
+2. Learn about the [client](/sdk/java/client) and how to make your first API requests.
+3. Configure [Settings](/sdk/java/settings) to customize output format, date format, and other universal parameters.
diff --git a/sdk/java/markets/index.mdx b/sdk/java/markets/index.mdx
new file mode 100644
index 0000000..d0785a1
--- /dev/null
+++ b/sdk/java/markets/index.mdx
@@ -0,0 +1,16 @@
+---
+title: Markets
+slug: /java/markets
+sidebar_position: 40
+---
+
+The Java SDK from Market Data provides methods to streamline your use of the Markets endpoints. These methods provide a typed interface over the underlying HTTP requests and responses, with both sync and async variants.
+
+Reach the resource through `client.markets()`. For CSV output use `client.markets().asCsv()`.
+
+## Markets Endpoints
+
+import DocCardList from "@theme/DocCardList";
+import { useCurrentSidebarCategory } from "@docusaurus/theme-common";
+
+
diff --git a/sdk/java/markets/status.mdx b/sdk/java/markets/status.mdx
new file mode 100644
index 0000000..dc2973a
--- /dev/null
+++ b/sdk/java/markets/status.mdx
@@ -0,0 +1,95 @@
+---
+title: Status
+sidebar_position: 1
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+Retrieve the exchange open/closed calendar — whether the market was (or will be) open on a given day.
+
+:::note
+This is distinct from [`utilities().status()`](/sdk/java/utilities/status), which reports the Market Data **API's** own service health.
+:::
+
+## Making Requests
+
+Use the `status()` method on the `markets` resource, built with `MarketStatusRequest`. Every parameter is optional; a bare request returns today's status for the US market.
+
+```java
+MarketStatusResponse status(MarketStatusRequest request)
+CompletableFuture statusAsync(MarketStatusRequest request)
+```
+
+### MarketStatusRequest
+
+```java
+MarketStatusRequest.of() // today, US
+MarketStatusRequest.builder()
+ .country(String country) // ISO 3166, 2-letter (default: US)
+ .date(LocalDate date) // a single day
+ .from(LocalDate from) // range start (inclusive)
+ .to(LocalDate to) // range end (inclusive)
+ .countback(int n) // N days before `to`
+ .build()
+```
+
+#### Returns
+
+`MarketStatusResponse` wrapping `List`:
+
+```java
+public record MarketStatus(
+ @Nullable ZonedDateTime date,
+ @Nullable String status) // "open" / "closed" / null (outside coverage)
+// also: boolean isOpen()
+```
+
+## Examples
+
+
+
+
+```java
+import com.marketdata.sdk.MarketDataClient;
+import com.marketdata.sdk.markets.MarketStatus;
+import com.marketdata.sdk.markets.MarketStatusRequest;
+import java.time.LocalDate;
+
+try (MarketDataClient client = new MarketDataClient()) {
+ var status = client.markets().status(
+ MarketStatusRequest.builder()
+ .from(LocalDate.now().minusDays(7))
+ .to(LocalDate.now())
+ .build());
+
+ for (MarketStatus day : status.values()) {
+ System.out.println(day.date().toLocalDate() + " " + day.status()
+ + (day.isOpen() ? " (trading)" : ""));
+ }
+}
+```
+
+
+
+
+```kotlin
+import com.marketdata.sdk.MarketDataClient
+import com.marketdata.sdk.markets.MarketStatusRequest
+import java.time.LocalDate
+
+MarketDataClient().use { client ->
+ val status = client.markets().status(
+ MarketStatusRequest.builder()
+ .from(LocalDate.now().minusDays(7))
+ .to(LocalDate.now())
+ .build())
+
+ for (day in status.values()) {
+ println("${day.date().toLocalDate()} ${day.status()}")
+ }
+}
+```
+
+
+
diff --git a/sdk/java/options/chain.mdx b/sdk/java/options/chain.mdx
new file mode 100644
index 0000000..99d5ed1
--- /dev/null
+++ b/sdk/java/options/chain.mdx
@@ -0,0 +1,131 @@
+---
+title: Chain
+sidebar_position: 5
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+Retrieve a full option chain for an underlying, with a rich set of filters. The chain is where most option workflows start.
+
+## Making Requests
+
+Use the `chain()` method on the `options` resource, built with `OptionsChainRequest`.
+
+```java
+OptionsChainResponse chain(OptionsChainRequest request)
+CompletableFuture chainAsync(OptionsChainRequest request)
+```
+
+### OptionsChainRequest
+
+The chain has two mutually-exclusive filter axes modeled as **sealed types**, so the compiler lets you pick exactly one variant of each:
+
+```java
+OptionsChainRequest.builder(String symbol)
+
+ // Expiration selection (sealed ExpirationFilter — pick one):
+ .expirationFilter(ExpirationFilter.onDate(LocalDate date))
+ .expirationFilter(ExpirationFilter.dte(int days)) // days-to-expiration
+ .expirationFilter(ExpirationFilter.between(LocalDate from, LocalDate to))
+ .expirationFilter(ExpirationFilter.all()) // every expiration
+ // (if omitted, the API narrows to the front month)
+
+ // Strike selection (sealed StrikeFilter — pick one):
+ .strikeFilter(StrikeFilter.exact(double price))
+ .strikeFilter(StrikeFilter.range(double min, double max))
+
+ // Additive filters:
+ .side(OptionSide.CALL) // CALL or PUT
+ .strikeRange(StrikeRange.ITM) // ITM / OTM / ATM
+ .strikeLimit(int limit) // max strikes per side
+ .delta(double delta)
+ .minOpenInterest(long oi)
+ .minVolume(long vol)
+ .date(LocalDate date) // historical chain
+ .build()
+```
+
+
+#### Returns
+
+`OptionsChainResponse` wrapping `List`:
+
+```java
+public record OptionQuote(
+ @Nullable String optionSymbol, @Nullable String underlying,
+ @Nullable ZonedDateTime expiration, @Nullable String side, @Nullable Double strike,
+ @Nullable ZonedDateTime firstTraded, @Nullable Integer dte, @Nullable ZonedDateTime updated,
+ @Nullable Double bid, @Nullable Long bidSize,
+ @Nullable Double mid, @Nullable Double ask, @Nullable Long askSize,
+ @Nullable Double last, @Nullable Long openInterest, @Nullable Long volume,
+ @Nullable Boolean inTheMoney, @Nullable Double intrinsicValue, @Nullable Double extrinsicValue,
+ @Nullable Double underlyingPrice, @Nullable Double iv,
+ // greeks:
+ @Nullable Double delta, @Nullable Double gamma, @Nullable Double theta,
+ @Nullable Double vega, @Nullable Double rho)
+```
+
+## Examples
+
+
+
+
+```java
+import com.marketdata.sdk.MarketDataClient;
+import com.marketdata.sdk.options.ExpirationFilter;
+import com.marketdata.sdk.options.OptionQuote;
+import com.marketdata.sdk.options.OptionSide;
+import com.marketdata.sdk.options.OptionsChainRequest;
+import com.marketdata.sdk.options.StrikeFilter;
+import java.util.List;
+
+try (MarketDataClient client = new MarketDataClient()) {
+
+ // The 5 calls nearest the money.
+ List chain = client.options().chain(
+ OptionsChainRequest.builder("AAPL")
+ .side(OptionSide.CALL)
+ .strikeLimit(5)
+ .build())
+ .values();
+
+ for (OptionQuote c : chain) {
+ System.out.println(c.optionSymbol() + " strike=" + c.strike()
+ + " bid/ask=" + c.bid() + "/" + c.ask() + " delta=" + c.delta());
+ }
+
+ // Sealed filters: contracts within 45 days, strikes between 150 and 250.
+ var filtered = client.options().chain(
+ OptionsChainRequest.builder("AAPL")
+ .expirationFilter(ExpirationFilter.dte(45))
+ .strikeFilter(StrikeFilter.range(150, 250))
+ .side(OptionSide.CALL)
+ .build());
+}
+```
+
+
+
+
+```kotlin
+import com.marketdata.sdk.MarketDataClient
+import com.marketdata.sdk.options.OptionSide
+import com.marketdata.sdk.options.OptionsChainRequest
+
+MarketDataClient().use { client ->
+ val chain = client.options().chain(
+ OptionsChainRequest.builder("AAPL")
+ .side(OptionSide.CALL)
+ .strikeLimit(5)
+ .build())
+ .values()
+
+ for (c in chain) {
+ println("${c.optionSymbol()} strike=${c.strike()} delta=${c.delta()}")
+ }
+}
+```
+
+
+
diff --git a/sdk/java/options/expirations.mdx b/sdk/java/options/expirations.mdx
new file mode 100644
index 0000000..bb2c820
--- /dev/null
+++ b/sdk/java/options/expirations.mdx
@@ -0,0 +1,67 @@
+---
+title: Expirations
+sidebar_position: 2
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+List the available expiration dates for an underlying.
+
+## Making Requests
+
+Use the `expirations()` method on the `options` resource, built with `OptionsExpirationsRequest`.
+
+```java
+OptionsExpirationsResponse expirations(OptionsExpirationsRequest request)
+CompletableFuture expirationsAsync(OptionsExpirationsRequest request)
+```
+
+### OptionsExpirationsRequest
+
+```java
+OptionsExpirationsRequest.of(String symbol)
+OptionsExpirationsRequest.builder(String symbol)
+ .strike(double strike) // only expirations that list this strike
+ .date(LocalDate date) // historical: the calendar as it stood on this date
+ .build()
+```
+
+#### Returns
+
+`OptionsExpirationsResponse` wrapping `List` — the expiration dates.
+
+## Examples
+
+
+
+
+```java
+import com.marketdata.sdk.MarketDataClient;
+import com.marketdata.sdk.options.OptionsExpirationsRequest;
+import java.time.ZonedDateTime;
+
+try (MarketDataClient client = new MarketDataClient()) {
+ var expirations = client.options().expirations(OptionsExpirationsRequest.of("AAPL"));
+ System.out.println("AAPL has " + expirations.values().size() + " expirations");
+ for (ZonedDateTime exp : expirations.values()) {
+ System.out.println(" " + exp.toLocalDate());
+ }
+}
+```
+
+
+
+
+```kotlin
+import com.marketdata.sdk.MarketDataClient
+import com.marketdata.sdk.options.OptionsExpirationsRequest
+
+MarketDataClient().use { client ->
+ val expirations = client.options().expirations(OptionsExpirationsRequest.of("AAPL"))
+ println("AAPL has ${expirations.values().size} expirations")
+}
+```
+
+
+
diff --git a/sdk/java/options/index.mdx b/sdk/java/options/index.mdx
new file mode 100644
index 0000000..0d732ee
--- /dev/null
+++ b/sdk/java/options/index.mdx
@@ -0,0 +1,16 @@
+---
+title: Options
+slug: /java/options
+sidebar_position: 20
+---
+
+The Java SDK from Market Data provides methods to streamline your use of the Options endpoints. These methods provide a typed interface over the underlying HTTP requests and responses, with both sync and async variants.
+
+Reach the resource through `client.options()`. For CSV output use `client.options().asCsv()`.
+
+## Options Endpoints
+
+import DocCardList from "@theme/DocCardList";
+import { useCurrentSidebarCategory } from "@docusaurus/theme-common";
+
+
diff --git a/sdk/java/options/lookup.mdx b/sdk/java/options/lookup.mdx
new file mode 100644
index 0000000..eba3486
--- /dev/null
+++ b/sdk/java/options/lookup.mdx
@@ -0,0 +1,63 @@
+---
+title: Lookup
+sidebar_position: 1
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+Turn a human-readable option description into a well-formed OCC option symbol.
+
+## Making Requests
+
+Use the `lookup()` method on the `options` resource, built with `OptionsLookupRequest`.
+
+```java
+OptionsLookupResponse lookup(OptionsLookupRequest request)
+CompletableFuture lookupAsync(OptionsLookupRequest request)
+```
+
+### OptionsLookupRequest
+
+```java
+OptionsLookupRequest.of(String userInput)
+```
+
+#### Returns
+
+`OptionsLookupResponse` wrapping a `String` — the OCC option symbol. Read it with `.values()`.
+
+## Examples
+
+
+
+
+```java
+import com.marketdata.sdk.MarketDataClient;
+import com.marketdata.sdk.options.OptionsLookupRequest;
+
+try (MarketDataClient client = new MarketDataClient()) {
+ String occSymbol = client.options()
+ .lookup(OptionsLookupRequest.of("AAPL 1/16/2026 $200 Call"))
+ .values();
+ System.out.println("Resolved to: " + occSymbol); // AAPL260116C00200000
+}
+```
+
+
+
+
+```kotlin
+import com.marketdata.sdk.MarketDataClient
+import com.marketdata.sdk.options.OptionsLookupRequest
+
+MarketDataClient().use { client ->
+ val occSymbol = client.options()
+ .lookup(OptionsLookupRequest.of("AAPL 1/16/2026 \$200 Call"))
+ .values()
+ println("Resolved to: $occSymbol")
+}
+```
+
+
+
diff --git a/sdk/java/options/quotes.mdx b/sdk/java/options/quotes.mdx
new file mode 100644
index 0000000..b9b693f
--- /dev/null
+++ b/sdk/java/options/quotes.mdx
@@ -0,0 +1,96 @@
+---
+title: Quotes
+sidebar_position: 4
+---
+
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+Retrieve quotes (bid, ask, last, greeks, etc.) for one or more option contracts by OCC symbol.
+
+## Making Requests
+
+The `options` resource offers two quote methods:
+
+- `quote(...)` — a single contract, built with `OptionsQuoteRequest`.
+- `quotes(...)` — several contracts. Unlike stocks, options quotes **fan out one request per symbol concurrently**, so the result is a `Map` keyed by OCC symbol (insertion order preserved).
+
+```java
+OptionsQuotesResponse quote(OptionsQuoteRequest request)
+CompletableFuture quoteAsync(OptionsQuoteRequest request)
+
+Map quotes(OptionsQuotesRequest request)
+CompletableFuture