This guide shows how to integrate an exchange rate API in Java without extra HTTP libraries. You’ll use java.net.http.HttpClient for requests, Jackson to map JSON to records, BigDecimal for conversion, and Spring’s @Cacheable with Caffeine to keep API calls to a minimum. Every example handles fxapi’s error codes explicitly.
Setup
Get a key from the free sign-up and expose it as FXAPI_KEY (or fxapi.key in Spring configuration). Dependencies for plain Java:
com.fasterxml.jackson.core:jackson-databind
com.fasterxml.jackson.datatype:jackson-datatype-jsr310
Spring Boot’s web starters already include both.
Records for the JSON response
/v1/latest, /v1/historical and /v1/convert return the same shape:
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
public record Rate(String code, BigDecimal value) {}
public record Meta(@JsonProperty("last_updated_at") Instant lastUpdatedAt) {}
public record RatesResponse(Meta meta, Map<String, Rate> data) {}
Mapping value to BigDecimal keeps the exact digits from the JSON instead of a binary double approximation.
An exchange rate API client in Java with HttpClient
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
public final class FxApiClient {
private static final String BASE = "https://api.fxapi.com/v1/";
private final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
private final ObjectMapper mapper = new ObjectMapper()
.findAndRegisterModules() // java.time support for Instant
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private final String apiKey;
public FxApiClient(String apiKey) {
this.apiKey = apiKey;
}
public RatesResponse latest(String base, String currencies) {
return get("latest", params("base_currency", base, "currencies", currencies));
}
public RatesResponse historical(LocalDate date, String base, String currencies) {
return get("historical", params("date", date.toString(), "base_currency", base, "currencies", currencies));
}
/** Requires the Basic plan or higher (HTTP 403 otherwise). */
public RatesResponse convert(BigDecimal value, String base, String currencies) {
return get("convert", params("value", value.toPlainString(), "base_currency", base, "currencies", currencies));
}
private static Map<String, String> params(String... kv) {
Map<String, String> map = new LinkedHashMap<>();
for (int i = 0; i < kv.length; i += 2) {
if (kv[i + 1] != null) map.put(kv[i], kv[i + 1]);
}
return map;
}
private RatesResponse get(String endpoint, Map<String, String> params) {
String query = params.entrySet().stream()
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
HttpRequest request = HttpRequest.newBuilder(URI.create(BASE + endpoint + "?" + query))
.header("apikey", apiKey)
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(10))
.GET()
.build();
try {
HttpResponse<String> res = http.send(request, HttpResponse.BodyHandlers.ofString());
return switch (res.statusCode()) {
case 200 -> mapper.readValue(res.body(), RatesResponse.class);
case 401 -> throw new FxApiException(401, "Invalid API key");
case 403 -> throw new FxApiException(403, "Endpoint or option not included in your plan");
case 422 -> throw new FxApiException(422, "Validation error: " + res.body());
case 429 -> throw new FxApiException(429, "Rate limit reached, monthly quota left: "
+ res.headers().firstValue("X-RateLimit-Remaining-Quota-Month").orElse("?"));
default -> throw new FxApiException(res.statusCode(), "Unexpected status");
};
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while calling fxapi", e);
}
}
}
public class FxApiException extends RuntimeException {
private final int status;
public FxApiException(int status, String message) {
super(status + ": " + message);
this.status = status;
}
public int status() { return status; }
}
A 422 body contains an errors object keyed by parameter (for example a date in the future), and it doesn’t count against your quota. 429 means the monthly quota is used up, or on the Free plan more than 10 requests per minute were sent.
Latest and historical exchange rates in Java
var fx = new FxApiClient(System.getenv("FXAPI_KEY"));
RatesResponse latest = fx.latest("EUR", "USD,GBP,CHF");
System.out.println(latest.meta().lastUpdatedAt() + " EUR/USD " + latest.data().get("USD").value());
RatesResponse hist = fx.historical(LocalDate.of(2024, 12, 31), "USD", "EUR,JPY");
System.out.println(hist.data().get("JPY").value());
Historical values are end-of-day rates in UTC, available from 1999-01-01 up to yesterday. Pass null for currencies to get all 190+ in one response. More on the historical exchange rates API.
Currency conversion with BigDecimal
On Basic and higher plans, fx.convert(new BigDecimal("250"), "USD", "EUR") returns the converted amount. On the Free plan, or when you already have cached rates, compute it locally:
import java.math.RoundingMode;
BigDecimal rate = fx.latest("USD", "EUR").data().get("EUR").value();
BigDecimal eur = new BigDecimal("250.00").multiply(rate).setScale(2, RoundingMode.HALF_EVEN);
Use the currency’s decimal_digits from /v1/currencies instead of a hard-coded 2 – JPY has 0 and KWD 3. The rounding guide covers half-even versus half-up and cross rates.
Spring Boot: caching with @Cacheable
Add spring-boot-starter-cache and com.github.ben-manes.caffeine:caffeine, then set the TTL to your plan’s update interval – daily on Free, hourly on Basic, 60 seconds on Professional and Enterprise (pricing):
# application.properties
fxapi.key=${FXAPI_KEY}
spring.cache.cache-names=fx-latest,fx-historical
spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=1h
@Configuration
@EnableCaching
class FxApiConfig {
@Bean
FxApiClient fxApiClient(@Value("${fxapi.key}") String key) {
return new FxApiClient(key);
}
}
@Service
class ExchangeRateService {
private final FxApiClient fx;
ExchangeRateService(FxApiClient fx) { this.fx = fx; }
@Cacheable("fx-latest")
public RatesResponse latest(String base) {
return fx.latest(base, null); // all currencies, one request per base and TTL
}
@Cacheable("fx-historical")
public RatesResponse historical(LocalDate date, String base) {
return fx.historical(date, base, null);
}
}
@Service
class CurrencyConverter {
private final ExchangeRateService rates;
CurrencyConverter(ExchangeRateService rates) { this.rates = rates; }
public BigDecimal convert(BigDecimal amount, String from, String to, int digits) {
BigDecimal rate = rates.latest(from).data().get(to).value(); // goes through the cache proxy
return amount.multiply(rate).setScale(digits, RoundingMode.HALF_EVEN);
}
}
Two details matter here. First, the converter lives in a separate bean: a @Cacheable method called from inside its own class skips the proxy and hits the API every time. Second, @Cacheable evicts on expiry and has no “serve stale on error” mode – if you need that, keep a last-known-good copy as shown in caching exchange rates, and store lastUpdatedAt with each booked transaction.
Official SDK
fxapi’s official SDKs cover JavaScript, Python, PHP, Go, Ruby, C# and Rust – there is no Java package. If you prefer generated code, run OpenAPI Generator against the OpenAPI spec; otherwise the ~80 lines above are all you need.
Next steps
- Read the API documentation for every endpoint and parameter.
- Need daily, hourly or minute series? See the time series API.
- Integrating with SAP, Dynamics or a custom ledger: ERP integration.
The examples in this guide use fxapi, a FX rates API with a free plan of 300 requests a month – no credit card required.
Frequently asked questions
Which Java version do I need?
java.net.http.HttpClient (Java 11+), records and switch expressions (Java 17+). Jackson 2.12 or newer can deserialize records.Why does @Cacheable not cache my exchange rates?
@Cacheable method from another method in the same class bypasses the cache. Call it from a different bean, as the converter on this page does.How do I set a TTL for @Cacheable?
@Cacheable itself has no expiry. Configure it in the cache provider, for example Caffeine with spring.cache.caffeine.spec=expireAfterWrite=1h, matching your plan’s update interval.Should exchange rates be double or BigDecimal in Java?
BigDecimal. Jackson reads JSON numbers directly into BigDecimal fields, and setScale(digits, RoundingMode.HALF_EVEN) rounds the result to the currency’s minor units.Is there an official Java SDK?
HttpClient code on this page.