diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2dc942..3586b1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## [Unreleased]
+## [0.3.1] — 2026-05-25
+
+### Added
+
+- `AuthApi.auth()` — exchange an API key for a short-lived JWT against `POST /auth/token` (OpenAPI spec 0.95.1). The client now ships `AuthTokenResponse` and `AuthTokenError` models and an `apiKeyAuth` (`X-API-Key`) security scheme.
+- `EquityPoint` model and `ResultMap.pnlTotalPercent` / `ResultMap.equityCurve` fields, carried forward from a prior spec bump that had not been regenerated here.
+
+### Fixed
+
+- README quickstart now reads the JWT from `QTSURFER_TOKEN` (was `JWT_API_TOKEN`), matching the TS and Python clients.
+
## [0.2.0] — 2026-05-17
### Changed
diff --git a/README.md b/README.md
index fc51e3a..868b677 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ Add the JitPack repository and the dependency:
com.qtsurfer
api-client-java
- 0.2.0
+ 0.3.0
```
@@ -49,12 +49,12 @@ For Gradle:
```gradle
repositories { maven { url 'https://jitpack.io' } }
-dependencies { implementation 'com.qtsurfer:api-client:0.2.0' }
+dependencies { implementation 'com.qtsurfer:api-client:0.3.0' }
```
### Via Maven Central (future)
-Once published to Central, the coordinate will be `com.qtsurfer:api-client:0.1.2`.
+Once published to Central, the coordinate will be `com.qtsurfer:api-client:0.3.0`.
## Quick start
@@ -66,18 +66,43 @@ import com.qtsurfer.api.client.model.Exchange;
import java.util.List;
ApiClient client = new ApiClient();
-client.updateBaseUri("https://api.qtsurfer.net/v1");
+client.updateBaseUri("https://api.qtsurfer.com/v1");
client.setRequestInterceptor(builder ->
- builder.header("Authorization", "Bearer " + System.getenv("JWT_API_TOKEN")));
+ builder.header("Authorization", "Bearer " + System.getenv("QTSURFER_TOKEN")));
ExchangeApi exchanges = new ExchangeApi(client);
List result = exchanges.getExchanges();
```
+### API key → JWT
+
+Every endpoint above expects a short-lived JWT in `Authorization: Bearer …`.
+Exchange a long-lived API key for one via `AuthApi.auth()`:
+
+```java
+import com.qtsurfer.api.client.api.AuthApi;
+import com.qtsurfer.api.client.invoker.ApiClient;
+import com.qtsurfer.api.client.model.AuthTokenResponse;
+
+ApiClient apikeyClient = new ApiClient();
+apikeyClient.updateBaseUri("https://api.qtsurfer.com/v1");
+apikeyClient.setRequestInterceptor(builder ->
+ builder.header("X-API-Key", System.getenv("QTSURFER_APIKEY")));
+
+AuthTokenResponse token = new AuthApi(apikeyClient).auth();
+String jwt = token.getAccessToken(); // feed to a Bearer-authed ApiClient
+```
+
+For production use, prefer the [`com.qtsurfer:sdk`](https://github.com/QTSurfer/sdk-java)
+`auth(apikey)` helper — it returns an `AuthenticatedClient` that refreshes the
+JWT transparently, reads `QTSURFER_APIKEY` from the environment, and supports
+pluggable token stores so callers don't reinvent that plumbing.
+
## API surface
| API class | Methods |
| --- | --- |
+| `AuthApi` | `auth()` — exchange API key for a short-lived JWT |
| `ExchangeApi` | `getExchanges()`, `getInstruments(exchangeId)` |
| `ExchangeBinaryDownloads` | `getTickersHour(...)`, `getKlinesHour(...)` — Lastra/Parquet streams (manual; see note below) |
| `StrategyApi` | `postStrategy(body, xCompileAsync)`, `getStrategyStatus(strategyId)` |
@@ -110,7 +135,7 @@ The class reuses the `ApiClient`'s `HttpClient` and request interceptor, so any
`ApiClient` exposes the underlying `HttpClient.Builder` and an `ObjectMapper`, plus hooks for request/response interceptors.
```java
-client.updateBaseUri("https://api.qtsurfer.net/v1");
+client.updateBaseUri("https://api.qtsurfer.com/v1");
client.setRequestInterceptor(builder ->
builder.header("Authorization", "Bearer " + token)
diff --git a/pom.xml b/pom.xml
index 4e1ff64..04537be 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.qtsurfer
api-client
- 0.2.0
+ 0.3.1
jar
QTSurfer API Client